КулЛиб - Классная библиотека! Скачать книги бесплатно 

Программирование трехмерных игр для Windows. Советы профессионала по трехмерной графике и растеризации [Андре Ламот] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]
TRICKS OF THE
3D GAME PROGRAMMING
GURUS
ADVANCED 3D GRAPHICS AND
RASTERIZATION

Andre LaMothe

SAMS

2O1 West 103rd Street,
Indianapolis, Indiana 462ЭО

ПРОГРАММИРОВАНИЕ
ТРЕХМЕРНЫХ ИГР
ДЛЯ WINDOWS
СОВЕТЫ ПРОФЕССИОНАЛА ПО
ТРЕХМЕРНОЙ ГРАФИКЕ И РАСТЕРИЗАЦИИ

ПРОГРАММИРОВАНИЕ
ТРЕХМЕРНЫХ ИГР
ДЛЯ WINDOWS
СОВЕТЫ ПРОФЕССИОНАЛА ПО ТРЕХМЕРНОЙ
ГРАФИКЕ И РАСТЕРИЗАЦИИ

Андре Ламот

Москва • Санкт-Петербург • Киев
2004

ББК 32.973.26-018.2.75
Л21
УДК 681.3.07
Издательский дом "Вильяме"
Зав, редакцией С.Н, Тригуб
Перевод с английского Р.Г. Имамутдиновой, канд. техн. наук И.В. Красикова,
А. Наумовца, Н.А. Ореховой, В.Н. Романова
Под редакцией канд. техн. наук Я. А Красикова
По общим вопросам обращайтесь в Издательский дом "Вильяме" по адресу:
info@williamspublishing.com, http://www.williamspublishing.com
Ламот, Андре.
Л21 Программирование трехмерных игр для Windows. Советы профессионала по
трехмерной графике и растеризации. : Пер. с англ. — М. : Издательский дом
"Вильяме", 2004. — 1424 с.: ил. — Парал. тит. англ.
ISBN 5-8459-0627-Х (рус,)
Данная книга представляет собой продолжение книги Андре Ламота Программирование игр
для Windows. Советы профессионала и посвящена созданию трехмерных игр. В книге освещены
различные аспекты разработки трехмерных игр, однако основное внимание уделяется вопросам программирования трехмерных игр — в частности, представления трехмерных объектов, их
визуализации с учетом свойств материала объектов, освещения, перспективы, а также таким
специфическим вопросам трехмерной визуализации, как создание различных визуальных спецэффектов и т.п. В книге также рассматриваются многие сопутствующие вопросы — создание
и применение звуковых эффектов и музыкального сопровождения, использование различных
форматов файлов и соответствующего инструментария.
Книга написана выдающимся специалистом в области программирования игр с многолетним стажем, и полезна как начинающим, так и профессиональным разработчикам игр для
Windows. Однако следует учесть, что она рассчитана в первую очередь на опытного специалиста, владеющего языком программирования C++, а также имеющего определенную математическую подготовку. Хотя данная книга может рассматриваться как отдельное издание, желательно приступать к ней после ознакомления с упомянутой ранее книгой.
ББК 32.973.26-018.2.75
Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм.
Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни
было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства
Sams Publishing.
Authorized translation from the English language edition published by Sams Publishing, Copyright © 2003
All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without
permission from the Publisher.
Russian language edition published by Williams Publishing House according to the Agreement with R&I Enterprises International, Copyright 2004
ISBN 5-8459-0627-Х (рус.)
ISBN 0-672-31835-0 (англ.)

© Издательский дом "Вильяме", 2004
© by Sams Publishing, 2003

Оглавление
Предисловие
Об авторе
О техническом редакторе
Благодарности
Введение

17
18
18
19
21

Часть I. Введение в программирование трехмерных игр

27

Глава 1, Основы программирования трехмерных игр
Глава 2. Краткий курс Windows и DirectX
Глава 3. Виртуальный компьютер для программирования трехмерных игр

29
79
119

Часть 11. Трехмерная математика и преобразования

219

Глава 4. Запутанный мир математики
Глава 5. Создание математической библиотеки
Глава 6. Введение в трехмерную графику
Глава 7. Визуализация трехмерных каркасных объектов

221
309
409
515

Часть 111. Основы трехмерной визуализации

627

Глава 8. Основы моделирования освещения и поверхностей тел
Глава 9. Интерполяционные методы затенения и аффинное отображение текстур
Глава 10. Отсечение в трехмерном пространстве
Глава 11. Организация буфера глубины и видимость

629
749
879
957

-.
Часть IV. Секреты трехмерной визуализации

1013

Глава 12. Методы сложного текстурирования
Глава 13. Алгоритмы разбиения пространства и определения видимости
Глава 14. Освещение и тени

1015
1143
1263

Часть V. Анимация, физическое моделирование и оптимизация 1309
Глава 15. Анимация, движение и обнаружение столкновений
Глава 16. Технологии оптимизации
Предметный указатель

1311
1359
1407

Содержание
Предисловие
Об авторе
О техническом редакторе
Благодарности
Введение

17
]8
18
J9
21

Часть I. Введение в программирование трехмерных игр

27

Глава 1. Основы программирования трехмерных игр
Краткое введение
Элементы двумерных и трехмерных игр
Общие советы по программированию игр
Использование инструментов
Редакторы трехмерных уровней
Использован ие ком п илятора
Пример трехмерной игры: Raiders 3D
Цикл событий
Внутренняя логика игры
Трехмерные проекции
Звездное поле
Лазерные пушки и обнаружение попаданий
Взрывы
Как играть в Raiders3D
Резюме

29
30
31
36
40
43
44
48
73
73
75
77
77
77
77
78

Глава 2. Краткий курс Windows и DirectX
Модель программирования Win32
Минимальный курс программирования для Windows
Все начинается с WinMainQ
Базовое приложение для Windows
Класс Windows
Регистрация класса Windows
Создание окна
Обработчик событий
Главный цикл событий
Создание цикла событий реального времени
Краткий курс DirectX и СОМ
HELnHAL
Подробнее о базовых классах DirectX
Краткое введение в СОМ
Что такое СОМ-объект?
Создание и использование СОМ-интерфейсов DirectX

79
SO
81
81
87
87
92
93
95
100
104
106
107
108

11J
j 14

Запрос интерфейсов
Резюме

115
116

Глава 3. Виртуальный компьютер для программирования трехмерных игр
Введение в интерфейс виртуального компьютера
Построение интерфейса виртуального компьютера
Буфер кадра и видеосистема
Работа с цветом
Анимация
Полная виртуальная графическая система
Ввод-вывод, звук и музыка
Консоль игры T3DL1B
Обзор T3DL1B
Базовая консоль игры
Библиотека T3DL1В1
Архитектура графического процессора DirectX
Основные определения
Макросы
Типы и структуры данных
Прототипы функций
Глобальные переменные
Интерфейс DirectDraw
Функции для работы с двумерными многоугольниками
Двумерные графические примитивы
Математические функции и функции обработки ошибок
Функции для работы с растровыми изображениями
Функции для работы с 8-битовыми палитрами
Вспомогательные функции
Процессор для работы с объектами блиттера
Система ввода DirectX
Звуковая и музыкальная библиотека T3DLIB3
Заголовочный файл
Типы
Глобальные переменные
API оболочки DirectSound
API оболочки DirectMusic
Окончательная версия консоли игры
Графика — реальная и виртуальная
Консоль игры — окончательный вариант
Образцы приложений T3DLIB
Оконные приложения
Полноэкранное приложение
Звук и музыка
Работа с устройствами ввода
Резюме

119
120
122
122
126
128
130
131
132
132
132
138
139
139
141
142
145
151
152
156
159
163
165
169
172
174
182
188
189
189
190
190
194
197
197
200
211
211
211
213
213
216

Часть И. Трехмерная математика и преобразования
Глава 4. Запутанный мир математики
Математические обозначения
СОДЕРЖАНИЕ

219
221
222
7

Двумерные системы координат
Двумерные декартовы координаты
Двумерные полярные координаты
Трехмерные системы координат
Трехмерные декартовы координаты
Трехмерные цилиндрические координаты
Трехмерные сферические координаты
Тригонометрия
Прямоугольный треугольник
Обратные тригонометрические функции
Тригонометрические тождества
Векторы
Длина вектора
Нормализация
Умножение на скаляр
Сложение векторов
Вычитание векторов
Скалярное произведение
Векторное умножение
Нулевой вектор
Радиус-вектор и вектор перемещения
Векторы как линейные комбинации
Матрицы и линейная алгебра
Единичная матрица
Сложение матриц
Транспонирование матрицы
Умножение матриц
Свойства матричных операций
Обращение матриц и решение систем линейных уравнений
Правило Крамера
Преобразования с использованием матриц
Однородные координаты
Применение матричных преобразований
Фундаментальные геометрические объекты
Точки
Прямые линии
Прямые в трехмерном пространстве
Плоскости
Использование параметрических уравнений
Двумерные и трехмерные параметрические прямые
Параметрическое представление отрезка с помощью стандартного вектора
направления
Параметрическое представление отрезка с помощью единичного вектора
направления
Параметрическое представление трехмерных прямых
Вычисление пересечения параметризованных прямых
Вычисление пересечения отрезка и плоскости
Введение в кватернионы
Теория комплексных чисел

223
223
225
228
229
230
233
234
235
237
237
238
239
240
240
241
241
242
245
246
246
247
247
249
250
250
250
252
252
253
255
256
257
263
263
263
265
267
271
271
272
273
273
274
277
278
278

СОДЕРЖАНИЕ

Гиперкомплексные числа
Применение кватернионов
Дифференциальное исчисление
Концепция бесконечности
Пределы
Суммы и конечные ряды
Бесконечные ряды
Производные
Интегралы
Резюме

282
287
291
291
293
294
296
297
303
308

Глава 5. Создание математической библиотеки
Краткий обзор математической библиотеки
Структура математической библиотеки
Соглашения об именах
Обработка ошибок
Заключительное слово о C++
Типы и структуры данных
Векторы и точки
Параметризованные прямые
Трехмерные плоскости
Матрицы
Кватернионы
Угловые системы координат
Двумерные полярные координаты
Трехмерные цилиндрические координаты
Трехмерные сферические координаты
Числа с фиксированной точкой
Математические константы
Макросы и встраиваемые функции
Утилиты общего назначения и преобразование величин
Точки и векторы
Матрицы
Кватернионы
Математика с фиксированной точкой
Прототипы
Глобальные переменные
API математической библиотеки
Тригонометрические
функции
Функции для работы с системами координат
Функции для работы с векторами
Функции для работы с матрицами
Функции для работы с двумерными и трехмерными параметрическими
прямыми
Функции для работы с трехмерными плоскостями
Функции для работы с кватернионами
Функции для работы с числами с фиксированной точкой
Функции для решения систем уравнений
Работа математического сопроцессора

309
310
310
311
312
312
313
313
314
316
317
320
321
322
322
323
323
324
327
331
332
333
335
336
336
341
341
341
342
345
353

СОДЕРЖАНИЕ

365
369
382
387
390

Архитектура сопроцессора
Стек сопроцессора
Набор команд сопроцессора
Классический формат команд
Формат работы с памятью
Формат работы с регистрами
Формат работы с регистрами и снятие со стека
Примеры команд сопроцессора
Замечания по использованию математической библиотеки
Замечания об оптимизации
Резюме
Глава 6. Введение в трехмерную графику
Философия трехмерного игрового процессора
Структура трехмерного игрового процессора
Трехмерный процессор
Игровой процессор
Система ввода и работы в сети
Анимация
Система навигации и обнаружения столкновений
Физический процессор
Система искусственного интеллекта
База данных трехмерных моделей и изображений
Трехмерные системы координат
Координаты модели (локальные координаты)
Мировые координаты
Координаты камеры
Дополнительные вопросы
Отбраковка скрытых объектов и поверхностей
Аксонометрические
координаты
Экранные координаты
Базовые трехмерные структуры данных
Представление трехмерных многоугольников
Определение многоугольников
Определение объектов
Представление миров
Инструментарий трехмерного моделирования
Анимационные данные
Загрузка внешних данных
PIG
NFF
3D Studio
СОВ-файлы Caligari
.Х-файлы Microsoft DirectX
Краткое резюме
Основы преобразований твердых тел и анимации
Трехмерное перемещение
Трехмерное вращение
Морфинг

/

390
391
393
396
396
397
397
398
406
407
408
409
410
410
411
411
412
412
418
419
420
421
423
424
427
430
439
441
447
458
467
468
470
476
481
483
484
484
485
488
492
499
501
50]
502
502
503
506

СОДЕРЖАНИЕ

Обзор конвейера визуализации
Типы трехмерных игровых процессоров
Космические процессоры
Наземные процессоры
FSP- процессоры
Трассировка лучей и вексельные процессоры
Гибридные процессоры
Сборка игрового процессора
Резюме

507
508
508
510
511
512
513
514
514

Глава 7. Визуализация трехмерных каркасных объектов
Общая архитектура каркасного игрового процессора
Структуры данных и трехмерный игровой конвейер
Основной список многоугольников
Новые программные модули
Создание простого загрузчика файлов с трехмерными моделями
Загрузчик файлов в формате .PLG/X
Разработка трехмерного игрового конвейера
Функции преобразования общего вида
Преобразование из локальной системы координат в мировую
Эйлерова модель камеры
Модель UVN камеры
Преобразование мировых координат в координаты камеры
Отбраковка объектов
Удаление обратных поверхностей
Аксонометрическое преобразование
Преобразование аксонометрических координат в экранные
Аксонометрическое преобразование в экранные координаты
Визуализация трехмерного игрового мира
Трехмерный игровой конвейер
Трехмерные демонстрационные программы
Вывод отдельного треугольника
Вывод трехмерного каркаса куба
Вывод трехмерного каркаса куба с удалением обратных поверхностей
Трехмерные танки
Трехмерные танки и перемешаемая камера
Перемещение по зоне боевых действий
Резюме

515
516
517
521
524
524
528
536
537
545
550
554
569
574
579
583
589
596
600
600
605
605
608
612
614
618
620
626

Часть III. Основы трехмерной визуализации
Глава 8. Основы моделирования освещения и поверхностей тел
Основные модели освещения в компьютерной графике
Цветовые модели и материалы
Виды освещения
Освещение и растеризация треугольников
Подготовка к моделированию освещения
Моделирование материалов
Определение источников освещения
СОДЕРЖАНИЕ

627
629
630
633
643
652
657
659
664
11

Затенение в реальном мире
16-битовое затенение
8-битовое затенение
Сложная RGB-модель для 8-битового режима
Упрошенная модель интенсивности освещения для 8-битовых режимов
Постоянное затенение
Плоское затенение
Плоское затенение в 8-битовом режиме
Затенение по Гуро
Затенение по Фонгу
Сортировка по глубине и алгоритм художника
Работа с новыми форматами моделей
Класс-анализатор
Формат .ASC 3D Studio MAX
Текстовый формат .СОВ trueSpace
Первое знакомство с бинарным форматом Quake II .MD2
Обзор программных инструментов для создания трехмерных моделей
Резюме

671
671
672
672
678
684
687
707
710
713
714
721
721
728
732
743
744
748

Глава 9. Интерполяционные методы затенения и аффинное
отображение текстур
Особенности нового трехмерного процессора
Обновление и разработка структур трехмерных данных
Новые директивы #define
Добавление математических структур
Вспомогательные макросы
Новые возможности представления трехмерных каркасов
Обновление структур объектов и списка визуализацииОбзор списка функций и их прототипов
Новые версии загрузчиков
Обновление загрузчика .PLG/PLX-файлов
Обновление загрузчика файлов в формате .ASC
Обновление загрузчика файлов в формате Caligari .COB
Обзор растеризации многоугольников
Растеризация треугольников
Соглашение о заполнении
Отсечение графического изображения
Новые функции, связанные с обработкой треугольников
Оптимизация
Реализация затенения по Гуро
Затенение по Гуро без освещения
Добавление освещения вершин в функцию, выполняющую затенение по Гуро
Основы теории дискретизации
Дискретизация в одном измерении
Билинейная интерполяция
Интерполяция координат и и v
Реализация
аффинного
отображения
текстуры
Обновление процессора освещения и растеризации для работы с текстурами
Добавление освещения для визуализации текстуры в 16-битовом режиме

12

749
750
751
752
755
756
757
766
772
780
781
797
798
805
805
810
814
815
821
823
825
837
849
850
853
854
858
861
862

СОДЕРЖАНИЕ

Вопросы оптимизации для 8- и 16-битовых режимов
Таблицы соответствия
Связность вершин каркаса
Кэширование математических результатов
Использование возможностей SIMD
Итоговые демонстрационные программы
Raiders 3D II
Резюме

869
870
870
871
872
872
873
877

Глава 10. Отсечение в трехмерном пространстве
Обшее представление об отсечении
Отсечение в пространстве объекта
Отсечение в пространстве изображений
Теоретические основы алгоритмов отсечения
Основы отсечения
Отсечение по Кохену-Сазерленду
Отсечение по Сайрусу-Беку и Лянгу-Барскому
Отсечение по Вейлеру-Азертону
Дальнейшее изучение отсечения
Практическое отсечение по границам области обзора
Конвейер обработки геометрии и структуры данных
Добавление отсечения в игровой процессор
Игры с ландшафтами
Функция, генерирующая ландшафты
Генерация данных ландшафта
Демонстрационная программа
Резюме

879
880
880
884
884
886
891
893
897
900
900
902
903
935
936
949
949
955

Глава 11. Организация буфера глубины и видимость
Буфер глубины и определение видимости
Основные принципы работы с Z-буфером
Проблемы, связанные с Z-буфером
Примеры Z-буфера
Метод уравнения плоскости
Интерполяция координаты г
Проблемы, связанные с Z-буфером, и l/Z-буферизация
Пример интерполяции по Z и 1/Z
Создание системы Z-буферизации
Добавление поддержки Z-буфера в функцию растеризации
Возможные оптимизации Z-буфера
Сокращение объема используемой памяти
Менее частая очистка Z-буфера
Смешанная Z-буферизация
Сложности при работе с Z-буфером
Демонстрационные программы, использующие Z-буферизацию
Первая демонстрационная программа: визуализация Z-буфера
Вторая демонстрационная программа: водный мотоцикл
Резюме

957
958
961
963
964
967
968
971
972
975
979
998
998
998
1001
1001
1002
1002
1004
1011

СОДЕРЖАНИЕ

13

Часть/V. Секреты трехмерной визуализации

1013

Глава 12. Методы сложного текстурирования
Текстурирование — вторая волна
Структуры данных заголовочного файла
Построение нового базового растеризатора
Работа с числами с фиксированной точкой
Новые растеризаторы без Z-буферизации
Новые растеризаторы с 2-буфером
Текстурирование с затенением по Гуро
Прозрачность и альфа-смешивание
Использование таблиц поиска для альфа-смешивания
Поддержка альфа-смешивания на уровне объектов
Добавление поддержки альфа-смешивания в генератор ландшафта
Текстурирование с корректной перспективой и l/z-буферизация
Математические основы отображения текстур с корректной перспективой
Добавление J/z-буферизации в растеризаторы
Реализация отображения с точной перспективой
Отображение текстуры с линейно-кусочной перспективой
Квадратичные аппроксимации для текстурирования с перспективой
Оптимизация текстурирования с помощью гибридных подходов
Билинейная фильтрация текстуры
Множественное отображение и трилинейная фильтрация текстур
Введение в Фурье-анализ
Создание цепочки множественного отображения
Выбор метода множественного отображения
Трилинейная фильтрация
Многопроходная визуализация и Текстурирование
Все в одном вызове
Новый контекст визуализации
Настройка контекста визуализации
Вызовфункции визуализации
Резюме

1015
1016
1017
1026
1027
I027
1032
1034
1043
1043
1057
1065
106S
1069
1078
1086
1090
1097
1101
1103
1108
1111
1114
1126
(132
1133
1134
1135
1138
1140
1141

Глава 13. Алгоритмы разбиения пространства и определения видимости
Новый модуль игрового процессора
Разбиение пространства и определение видимости
Двоичное разбиение пространства
Двоичное разбиение пространства плоскостями, параллельными осям
Разбиение пространства произвольными плоскостями
Разбиение с помощью плоскостей, определяемых многоугольниками
Отображение/посещение каждого узла BSP-дерева
Функции и структуры данных BSP-деревьев
Создание BSP-дерева
Стратегии расщепления
Обход и отображение BSP-дерева
Интеграция BSP-дерева в графический конвейер
Редактор уровня
Недостатки BSP

I ИЗ
1144
1144
\ (48
1150
1151
1152
1 ] 56
1159
1 (61
1165
] 177
1191
] 193
1205

14

СОДЕРЖАНИЕ

Стратегии с нулевым перерисовыванием на основе BSP
Использование BSP-деревьев для отбраковки
Использование BSP-деревьев для выявления столкновений
Интеграция BSP-деревьев в стандартную визуализацию
Потенциально видимые множества
Использование потенциально видимого множества
Методы представления потенциально видимого множества
Более точное вычисление PVS
Порталы
Ограничивающие иерархические объемы и октадеревья
Использование BHV-дерева
Производительность
Стратегии выбора
Октадеревья
Отбор с учетом препятствий
Перекрытые пространства
Выбор преград
Гибридный метод выбора преград
Резюме

1205
1208
1219
1219
1225
1228
1230
1231
1234
1237
1240
1241
1242
1255
1257
1258
1260
1260
1261

Глава 14. Освещение и тени
Новый модуль игрового процессора
Введение и план игры
Упрощенная физика теней
Движение фотонов и вычислительная интенсивность
Имитация теней с помощью проецирования изображений и макетов
Растеризация с поддержкой прозрачности
Новый библиотечный модуль
Простые тени
Масштабирование теней
Отслеживание источника света
Последние замечания об имитации теней
Отображение тени на плоскую сетку
Вычисление векторных преобразований для проекции
Оптимизация плоских теней
Введение в отображение освещения и кэширование поверхности
Кэширование поверхности
Генерация карт освещения
Реализация отображения освещения
Отображение темноты
Получение специальных эффектов с помощью карт освещения
Оптимизация кода отображения освещения
Резюме

1263
1264
1264
1264
1.265
1269
1271
1275
1276
1279
1283
1289
1290
1290
1295
1296
1299
1300
1301
1305
1307
1308
1308

Часть V. Анимация, физическое моделирование и оптимизация 1309
Глава 15. Анимация, движение и обнаружение столкновений
Новый модуль игрового процессора
Введение в трехмерную анимацию
СОДЕРЖАНИЕ

1311
1312
1312
15

Формат Quake IIMD2
Заголовок. MD2
Загрузка .МО2-файлов Quake 11
Анимация файлов .MD2
Простая анимация без участия персонажа
Вращательное и поступательное движение
Сложное параметрическое и криволинейное движение
Использование сценариев движения
Обнаружение трехмерных столкновений
Ограничивающие сферы и цилиндры
Использование структур данных для ускорения обнаружения столкновений
Техника следования по поверхности
Резюме

1312
1315
1325
1336
1349
1349
1350
1352
1354
1355
1356
1357
1358

Глава 16. Технологии оптимизации
Введение в оптимизацию
Профилирование кода программы
Профилирование с помощью Visual C++
Анализ результатов профилирования
Оптимизация с помощью VTune
Использование компилятора Intel C++
Загрузка оптимизирующего компилятора Intel
Использование компилятора Intel
Использование дополнительных возможностей компилятора
Выбор компилятора для исходных файлов
Стратегии оптимизации
Введение в SIMD
Базовая архитектура SIMD
Работа с SIMD в реальном мире
Класс трехмерных векторов с поддержкой SIMD
Некоторые оптимизационные приемы
Прием 1. Избаачение от _ftol()
Прием 2. Задание управляющего слова FPU
Прием 3. Быстрое обнуление значений с плавающей точкой
Прием 4. Быстрое извлечение квадратного корня
Прием 5. Линейно-кусочный арктангенс
Прием 6. Увеличение указателя
Прием 7. Вынесение if из циклов
Прием 8. Ветвление конвейера
Прием 9. Выравниваниеданных
Прием 10. Все короткие функции нужно сделать встраиваемыми
Резюме

1359
] 360
1360
1361
1364
1365
1372
1373
1374
1375
1376
1377
1377
1379
1379
1392
1400
1400
1400
1401
J402
1402
1402
1403
1404
1404
1404
1405

Предметный указатель

1407

16

СОДЕРЖАНИЕ

Предисловие
Для меня большая честь написать предисловие к этой важной работе, с базового
уровня обучающей навыкам программирования, необходимым для создания трехмерных
видеоигр нового поколения. Существует не так уж много книг, которые учат создавать
трехмерную виртуальную машину, работающую в реальном времени, начиная с простейшего вывода пикселей на экран. Как все же далеко продвинулись технологии игр
с тех пор, как были созданы первые простейшие игры для Atari. В те времена игры, кажущиеся примитивными сейчас, потрясали воображение.
Сегодня очевидно, что первые игры с технической точки зрения не были компьютерными играми. На самом деле они были всего лишь забавными генераторами сигналов и конечными автоматами, использующими счетчики и регистры сдвигов, основанные на простой булевой логике. Они были не чем иным, как совокупностью логических схем среднего
уровня интеграции. Моя первая игра Computer Space была создана в 1970г., за четыре года до
появления микропроцессора Intel 4004 и за шесть лет до появления процессора 8080. Мне
так хотелось иметь микропроцессор, чтобы делать настоящие игры! Даже по тем временам
типичная рабочая частота была слишком мала, чтобы производить расчеты в реальном времени. Впервые мы использовали микропроцессор в игре Asteroids. И даже тогда для ускорения работы программы использовалась масса дополнительного оборудования, поскольку
микропроцессор не мог справиться со всеми поставленными задачами.
Сегодня мы движемся к возможности создания сцен фотографического качества (если
еще не пришли к ней). Процесс создания таких сцен в реальном времени приносит истинное наслаждение. Реализм и возможность создания любых миров, окружающей среды
и персонажей, достигаемые благодаря современному программному обеспечению и оборудованию, дают создателю игр потрясающую власть. Эти возможности позволяют сжимать
время, увеличивать богатство и реализовать новые, доселе немыслимые проекты.
Андре Ламот не только глубоко понимает технологии, использующиеся при разработке игр, но и имеет экстраординарное "чутье" игры. Мой многолетний опыт показывает,
что нередко разработчики являются великими знатоками своего дела, но им не хватает
чувства времени или напряжения, которые так необходимы для увлекательной игры.
Другие хорошо чувствуют игру, но они слабые программисты. Андре сочетает в себе качества увлеченного игрока и профессионального программиста, и это видно во всех написанных им книгах.
В последнем нашем совместном проекте на меня произвел впечатление не только его
профессионализм, но и знание истории и близкое знакомство даже с малоизвестными
самыми первыми играми (до этого я считал, что о них помню только я). Не могу не упомянуть о том, что он написал для меня игру всего за 19 дней! Легко помнить о хитах, иное
дело — знать неудачные игровые программы (надо сказать правду — Atari сделала несколько откровенно слабых проектов). А мы, как известно, сделали много игр, которые
стали, не побоюсь этого слова, классическими.
Надеюсь, что вам понравится эта книга. Используйте ее как стартовую площадку для
создания новых великих игр, способных осчастливить человечество.
Нолан Башнелл
Учредитель Atari, Inc.

Об авторе
Андре Ламот связан с компьютерной индустрией и информационными технологиями
более четверти столетия. Он является обладателем многочисленных дипломов в области
математики, компьютерных наук и электротехники. Он — один из тех редких людей, которые действительно делают серьезную работу в NASA. Уже с молодых лет Андре занимался консультированием многочисленных компаний Силиконовой Долины, где он на
практике осваивал бизнес и использовал свои разносторонние знания в таких областях,
как телекоммуникации, виртуальная реальность, робототехника, разработка компиляторов, трехмерные виртуальные машины, искусственный интеллект, и в других областях
информационных технологий и проектирования.
Его компания Xtreme Games LLC была одной из первых (и последних) небольших
компаний "со своей душой". Позднее он основал Xtreme Games Developer Conference
(XGDC) в качестве дешевой альтернативы GDC (Games Developer Conference — конференция разработчиков игр).
Ламот работал над рядом известных проектов, включая eGamezone Networks, который
представляет собой интерактивную систему распространения игр — честную, с юмором и
без рекламы. Андре основал новую компанию, Nurve Networks LLC, которая занялась
разработкой переносных игровых видеосистем для пользователей с высокими требованиями к играм. Наконец, он является редактором крупнейшего в мире периодического
издания по разработке игр.
В свободное время он увлекается всем экстремальным — от тяжелой атлетики и мотоциклов до боевых единоборств. Одно время он даже интенсивно тренировался в составе
команды Shamrock Submission Fighting Team под руководством именитых Крэйзи Боба
Кука (Crazy Bob Cook), Фрэнка Шэмрока (Frank Shamrock) и Жавье Мендеса (Javier
Mendez). Я думаю, вам не захочется вступать с ним в дискуссию по поводу DirectX или
OpenGL — прав он или нет, последнее слово все равно останется за ним!

О техническом редакторе
Дэвид Фрэнсон (David Franson) профессионально работает в области сетевых
технологий, программирования, двумерной и трехмерной компьютерной графики
с 1990г, В 2000г. он ушел с должности директора информационных систем одной из
крупнейших юридических фирм Нью-Йорка, чтобы полностью посвятить себя разработке игр. Он также является автором книги 2 D Artwork and 3D Modeling for Game
Artists, изданной Premier Press, и в настоящее время он работает над книгой, посвященной Xbox Hackz and Mod?..

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

Благодарности
Прежде всего мне хотелось бы выразить свою благодарность сотрудникам Sams Publishing. Я позволю себе заметить, что в этом издательстве сложился довольно стандартный подход к изданию книг, и серия моих книг выводит их из равновесия. Мы должны
действительно поаплодировать умению этих людей отойти от корпоративного стиля и
предоставить авторам возможность создать нечто особенное. Помните об этом, когда будете читать их имена.
Я бы хотел выразить свою признательность издателю Майклу Стефенсу (Michael Stephens), редактору по работе с авторами Киму Спилкеру (Kim Spilker), редактору по подготовке текста Марку Ренфроу (Mark Renfrew) и, конечно, редактору проекта, который
отвечает за то, чтобы весь механизм издания книги работал слаженно, — Джорджу Недеффу (George NedefT). Нельзя не упомянуть об основных "двигателях" этого механизма — редакторе Сете Керни (Seth Kerney) и техническом редакторе Дэвиде Фрэнсоне
(David Franson) (Дэвид также предоставил некоторые трехмерные модели, включая Jet
Ski, а также многие текстуры, использованные в демонстрационных программах).
И наконец, поскольку сегодня никакая книга не выглядит завершенной без компакт-диска, я выражаю признательность мультимедиа-разработчику Дэну Шерфу
(Dan Scherf), а также Эрике Миллен (Erika Millen), благодаря которой был создан
подробный и исчерпывающий предметный указатель книги, что важно, когда вам
нужно что-то найти в тексте.
Есть еще один особенный человек, внесший значительный вклад на начальном этапе
создания книги. Его имя Анжела Козловски (Angela Kozlowski). Она работала со мной на
начальном этапе создания книги и по-настоящему помогла мне определить ее общую
структуру. Она помогла всем понять, как важна и как необычна эта книга. Она не смогла
увидеть завершающий этап создания книги, поскольку перешла на другую работу, но,
я уверен, она бы этого хотела,
Следует также упомянуть компании и людей, которые дали, одолжили или каким-то
иным способом обеспечили мне доступ к программному обеспечению и оборудованию.
Первый из них— это Дэвид Хэксон (David Hackson) из Intel Corporation, который дал
мне последние версии компиляторов C++ и Fortran, а также VTune. Кристин Гарднер
(Kristine Gardner) из Caligari Corporation дала мне все когда-либо созданные версии
trueSpace. Моя давняя знакомая Стэси Цурусаки (Stacey Tsurusaki) из Microsoft предоставила мне возможность "взглянуть" на Microsoft изнутри. И конечно, были компании,
предоставившие мне пробные версии своих продуктов или давшие возможность тем или
иным способом записать их программы на компакт-диски. — это JASC Inc, (Paint Shop
Pro) и Sonic Foundry (Sound Forge). Мэри Элис Краецки (Mary Alice Krayecki) из Right
Hemisphere прислала мне копии Deep Exploration и Deep Paint UV.

Мне хотелось бы также перечислить людей, которые поддерживали со мной дружеские отношения, пока я работал над книгой. Я знаю, что я "интенсивная" личность, но
именно это делает меня столь забавным на вечеринках!
В любом случае, мне хотелось бы выразить признательность Майку Перону (Mike
Perone) за помощь в приобретении программного обеспечения, а также при работе с сетевыми вопросами.
Марку Беллу (Mark Bell) за выслушивание моих жалоб — только бизнесмен способен
понять тот ад, через который мы прошли, — спасибо, Марк.
Селламу Исмаилу (Sellam Ismail) из Vintage Computer Festival за массу бесподобного
ретро-материала.
Джону Ромеро (John Romero) за его частое общение со мной и за то, что в нашем бизнесе есть хоть кто-нибудь, кого можно было бы назвать забавным!
Нолану Башнеллу (Nolan Bushnell) за то, что он принял меня в uWink Inc. и нашел
время для разговора со мной о видеоиграх. Ведь не каждый день вы проводите время
в обществе человека, создавшего Atari! И спасибо за написание предисловия!
Алексу Варанесу (Alex Varanese), моему новому ученику, за его готовность следовать
моим постоянным проповедям о совершенствовании...
И наконец, людям, которые по-настоящему испытали на себе остроту моей агрессивной и
требовательной личности — маме, отцу и моей терпеливой подруге Аните.

Ждем ваших отзывов!
Вы, уважаемый читатель, и есть главный критик и комментатор этой книги. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и
любые другие замечания, которые вам хотелось бы высказать в наш адрес.
Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумажное
или электронное письмо, либо просто посетить наш Web-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится или
нет вам эта книга, а также выскажите саое мнение о том, как сделать наши книги более
интересными для вас,,
Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также
ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязательно учтем
его при отборе и подготовке к изданию последующих книг. Наши координаты:
E-mail:
info@wHliamspubLishing.com
WWW:
http://www. wiUiamspublishing.com
Информация для писем:
из России:
из Украины:

20

115419, Москва, а/я 783
03150, Киев, а/я 152

БЛАГОДАРНОСТИ

ВВЕДЕНИЕ

Принципы и практика программирования игр
Давным-давно, далеко-далеко отсюда я написал книгу под названием Программирование игр для Windows. Советы профессионала. Для меня это была возможность г делать то,
о чем я давно мечтал, — создать книгу, которая учит делать игры. Прошло несколько лет,
я стал немного старше, немного мудрее и узнал массу новых приемов. Вы можете обойтись и без чтения упомянутой книги, однако я предупреждаю — книга, которую вы держите в руках, намного более сложная. Она сфокусирована на трехмерной графике
и предполагает определенную подготовку у ч итателя.
Эта книга продолжает разговор с того места, на котором он был прерван в первой
книге, и подробно рассматривает принципы трехмерной графики. Я собираюсь рассмотреть все основные темы программирования трехмерных игр — все, которые мне удастся
"уложить" в отведенные рамки объема книги и сроков ее написания.
Я не рассчитываю, что вы являетесь асом программирования или что вы знаете, как
делать игры. Однако эта книга— определенно не для новичков, она ориентирована на
разработчиков игр и программистов среднего или высокого уровня. Если вы не знаете,
как программировать на С+'+, — вам здесь нечего делать. Так что, новички, хорошенько
подумайте — не стоит ли потратить деньги на хороший учебник по C++ (я бы предложил
что-нибудь, написанное Стефеном Прата (Stephen Praia) или Робертом Лафором (Robert
Lafore), по моему мнению, это лучшие авторы по данной теме).
Сейчас, вероятно, самое горячее время в истории игрового бизнеса. Сегодня в руках программистов оказались технологии создания игр, которые выглядят совершенно
реалистично. Представьте, каким будет следующее поколение игр (а вы все еще думаете, что PlayStation 2, Xbox, и GameCube — это круто?). Однако все эти технологии нетривиальны и сложны для понимания — они возникли в результате упорной работы
многих умных людей.
Уровень сложности программирования игр сегодня существенно вырос. Но если вы
это читаете, то, вероятно, вы один из тех, кто любит бросать вызов судьбе, не так ли?
Ну что же, тогда вы обратились по адресу, ведь если вы одолеете эту книгу, то сможете
самостоятельно создать игру полностью трехмерную, с текстурами с полной проработкой освещения и программной растеризацией. Кроме того, вы изучите базовые принципы трехмерной графики и будете лучше понимать и уметь использовать оборудова-

ние для трехмерной графики как сегодняшнее, так и то, которое появится в ближайшем будущем.

Что вы сможете изучить
Эта книга содержит огромный объем информации! Если заполнить ею ваш мозг, у вас
начнется утечка нейронов! :-) А если серьезно, то эта книга излагает все сведения, необходимые для создания игр для платформы Windows 9x/2000, включая следующие разделы.
• Принципы создания игрового процессора из первой книги.
• Программирование 32 и DirectX.
• Высшая математика, включая кватернионы.
• Двумерная и трехмерная графика и алгоритмы.
• Трехмерные проекции и работа с камерой.
• Визуализация каркасов и сплошных тел.


Работа со светом и текстурами.

• Сложные алгоритмы визуализации.
• Методы трехмерной анимации.
И многое другое...
Первое, о чем меня можно спросить, — рассматриваются ли в этой книге вопросы использования оборудования для трехмерной графики, или показывает ли она, как реализовать трехмерную программную растеризацию? Ответ — книга в первую очередь посвящена написанию программ. Только дилетанты полагаются на аппаратное обеспечение.
Настоящий программист игр может написать трехмерный процессор с нуля даже под
артобстрелом, когда лопаются барабанные перепонки. При этом он сможет использовать
и оборудование— если оно есть. Поэтому в данной книге мы сосредоточимся на программировании трехмерных игр. Обладая этими знаниями, можно изучить любой API для
трехмерной графики в течение пары недель.
Моя философия такова: если вы знаете, как написать программу отображения текстуры или систему визуализации сцены, то это дает вам значительно больше, чем знания
оборудования. Кроме того, потребуется какое-то время для того, чтобы в каждом компьютере было установлено хорошее оборудование для трехмерной графики. Неправильно
считать, что оно установлено на любом компьютере, и вычеркивать из своего целевого
рынка компьютеры только потому, что у них нет такого оборудования. (Средний Запад и
Европа — великолепные примеры регионов, в которых нет последних новинок технологий). А если к тому же у вас нет миллионов долларов для завоевания игрового рынка, то
вам прямая дорога на рынок программных игр, которые рассчитаны на компьютеры без
ускорителей ЗО-графики.
Наконец, я уверен, что вы испытываете некоторое беспокойство насчет всей этой
затеи с "Windows-DirectX". Дело в том, что при правильном подходе программирование для Windows — занятие очень простое и забавное, которое снимает многие
проблемы, которые имеют место в случае с DOS32. Не следует думать о программировании под Windows как о проблеме — думайте об этом как о еше одной возможности посвятить свое время работе над игровым кодом, а не над такими вещами, как
графический интерфейс пользователя, система ввода-вывода и графические драйверы. Поверьте мне, вы будете работать круглосуточно, если захотите написать графи-

22

ВВЕДЕНИЕ

ческие драйверы для всех двумерных или трехмерных ускорителей, выходящих на
рынок. Кроме того, есть еще звуковые карты, джойстики и много чего еще...

Что необходимо знать
Эта книга предполагает, что вы умеете программировать, и программировать
очень хорошо. Вы здесь просто потеряетесь, если не сможете написать код на С или
не будете отлично знать, как использовать компилятор. Кроме того, в книге используется C++, чего уже достаточно, чтобы С-кодировшик чувствовал себя немного неуютно. Я сообщу вам, когда буду делать что-то сверхъестественное. В приложениях
содержится дополнительный учебный материал по C++, поэтому воспользуйтесь им,
если вам необходим начальный курс. Впрочем, C++ в основном нужен только при
использовании DirectX.
Тем не менее, я решил, что в этой книге буду использовать C++ немного шире, поскольку в программировании игр есть довольно много объектно-ориентированных вещей, и в таких случаях использовать структуры языка С — это просто кощунство. Подвожу итог: если вы умеете писать программы на С, вы все поймете. Если умеете писать их
и на С, и на C++ — вам вообще не о чем беспокоиться.
Каждый знает, что компьютерная программа — это просто логика и математика.
Трехмерные видеоигры делают упор именно на математику! ЗО-графика — это только
математика. К счастью для нас, это красивая математика. (Да, математика может быть
красивой.) Дело в том, что все, что вам нужно знать, — это алгебра и геометрия. Векторам, матрицам и дифференциальному исчислению я обучу вас по ходу дела. Если вы
умеете складывать, вычитать, умножать и делить, вы сможете понять большую часть материала, даже если не сможете повторить ход рассуждений. Но если при этом вы сможете
использовать представленный код — то мы добились нашей конечной цели.
Фактически это все, что нужно знать. Конечно, вам лучше позвонить своим друзьям и
сказать, что они не смогут видеться с вами около двух лет, поскольку вы будете немного
заняты. Но вы толькоподумайте обо всех тех новых фильмах, которые сможете посмотреть после окончания обучения!

Структура книги
Эта книга состоит из шести основных частей.
• Часть 1, "Введение в программирование трехмерных игр". В этой части излагаются
основы программирования игр, Windows, DirectX, здесь мы также построим виртуальный компьютерный интерфейс для разработки демонстрационных программ.


Часть II, "Трехмерная математика и преобразования". В этом разделе описываются используемые в дальнейшем математические концепции, а также создается
математическая библиотека, используемая в данной книге. Последние главы
раздела посвящены трехмерной графике, структурам данных, камерам и каркасной визуализации.
• Часть III, "Основы трехмерной визуализации". Этот раздел охватывает такие темы,
как освещение, основы наложения теней и удаление невидимых поверхностей.
Рассматривается также полное ЗО-отсечение.
• Часть IV, "Секреты трехмерной визуализации". Рассматриваются наложение текстур, сложное освещение, сложные тени, а также алгоритмы пространственного
разделения, такие как BSP-деревья, порталы и другие.

ВВЕДЕНИЕ

23

• Часть V, "Анимация, физическое моделирование и оптимизация". В этой части
описываются анимация, движение объектов, обнаружение столкновений и простое физическое моделирование. Рассматривается также иерархическое моделирование вместе с загрузкой больших игровых миров. Здесь также описаны многочисленные методы оптимизации.
Шестая часть представляет собой находящиеся на прилагаемом компакт-диске
приложения.

Установка содержимого CD-ROM
CD-ROM содержит все исходные тексты, исполняемые модули, примеры программ,
библиотеку графических объектов, программы трехмерного моделирования, звуковые
эффекты, а также технические статьи, которые дополняют книгу. Структура каталогов
диска представлена ниже.
T3DIIGAME\
SOURCE\
T3DIICHAP01\
T3DIICHAP02\

T3DIICHAP16\
TOOLS\
GAME5\
MEDIA\
BITMAPS\
3DMODELS\
SOUND\
DIRECTX\
ARTICLES\

В каждом каталоге имеются различные необходимые материалы. Далее следует их более подробное'описание.


T3DGAMEII— корневой каталог, в котором находятся все остальные каталоги.
Прочитайте файл README. TXT, в котором описаны все последние изменения.

• SOURCE — содержит все каталоги с исходными кодами книги в порядке следования
глав. Просто перетяните мышью весь каталог SOURCE\ на ваш жесткий диск и работайте с ним оттуда.
• MEDIA— содержит набор двумерных изображений, трехмерных моделей и звуков,
которые вы можете бесплатно использовать в своих играх.
• DIRECTX — содержит последнюю версию DirectX SDK.


GAMES — содержит несколько двумерных и трехмерных условно-бесплатных игр,
которые, по моему мнению, просто превосходны!

• ARTICLES — содержит статьи, написанные для вашего развития различными специалистами в области программирования игр.
24

ВВВДЕНИЕ

• TOOLS — содержит различные полезные приложения и инструменты. Большинство из них можно загрузить из Internet, однако они довольно большие, и данные
файлы позволят вам сэкономить на времени загрузки.
Для компакт-диска нет единой программы установки, поскольку здесь находится много
различных программ и данных. Оставляю установку на ваше усмотрение. В большинстве с.тучаев можно просто скопировать каталог SOURCE\ на жесткий диск и работать с ним оттуда:
Что касается других программ и данных, установите те из них, которые вам нужны.

Установка DirectX
Пожалуй, единственная часть компакт-диска, которую нужно установить обязательно, — это DirectX SDK и файлы времени выполнения. Программа установки находится в каталоге DIRECTX\ вместе с файлом README.ТХТ Ь в котором указаны все последние изменения.
Для работы с демонстрационными программами или исходными файлами этой книги
вам необходимо установить DirectX8.1 SDK или более позднюю версию (на компактдиске находится DirectX 9.0), поэтому если вы точно не знаете, какая версия файлов
у вас установлена, — запустите программу установки, и она сообщит вам об этом.

Компиляция программ
Я написал программы для этой книги с помощью Microsoft Visual C++ 6.0. Тем не менее, в большинстве случаев программы будут работать с любым компилятором, совместимым с Win32. И все же я рекомендую Microsoft Visual C++ или .NET, поскольку с ними
программы работают лучше всего.
Если вы никогда не работали с интегрированной средой разработки своего компилятора, то уделите время изучению компилятора — по крайней мере, научитесь компилировать консольное приложение "Hello World" или что-то подобное, прежде чем приступать к компиляции серьезных программ.
Для компиляции Win32 .ЕХЕ-модулей программ для Windows все, что нужно сделать, — это установить соответствующим образом опцию целевого программного проекта и приступить к компиляции. Однако для создания DirectX-программ необходимо
включить в проект библиотеки импорта DirectX. Можно подумать, что достаточйо просто.
добавить библиотеки DirectX в пути поиска компилятора, однако это не так. Поберегите
свои нервные клетки и добавьте DirectX . LIB-файлы в проект или рабочую область
вручную. .LIB-файлы можно найти в каталоге ЫВ\, в основном каталоге DirectX SDK,
который вы установили. В этом случае при компоновке не будет никаких неприятных
неожиданностей. В большинстве случаев вам необходимы следующие файлы.


DDRAW. LIB — библиотека импорта DirectDraw

» DINPUT. LIB — библиотека импорта Directlnput


DINPUT8 . LIB — библиотека импорта Directlnput



DSOUND. LIB — библиотека импорта DirectSound

• WINMM. LIB — Windows Multimedia Extensions
Я расскажу об этих файлах подробнее, когда мы начнем работать с ними. Сейчас же
о них просто нужно помнить в случае сообщений компоновщика типа "unresolved

ВВЕДЕНИЕ

25

symbol". Я не хочу получать от новичков никаких электронных писем по этой теме и тем
более не намерен отвечать на них!
Кроме . ыв-файлов DirectX, следует включить в путь поиска заголовочных файлов
соответствующие . Н-файлы. Убедитесь при этом, что каталоги DirectX SDK стоят первыми в пути поиска, поскольку многие C++-компиляторы содержат в себе старые версии DirectX, и в их каталоге заголовочных файлов могут находиться файлы старых версий DirectX, что неприемлемо. Надлежащее место — это каталог заголовочных файлов
DirectX SDK.
Наконец, тем, кто пользуется продуктами Borland, следует убедиться, что у вас установлены Borland-версии .LIB-файлов DirectX. Их можно найти в каталоге BORLAND\
DirectX SDK.

26

ВВЕДЕНИЕ

ЧАСТЬ I

Введение
в программирование
трехмерных игр
В этой части...
Глава 1
Основы программирования трехмерных игр

29

Глава 2
Краткий курс Windows и DirectX

79

Глава 3
Виртуальный компьютер для программирования
трехмерных игр

119

ГЛАВА 1

Основы
программирования
трехмерных игр
В этой главе...
• Краткое введение

30

• Элементы двумерных и трехмерных игр

31

• Общие советы по программированию игр

36

• Использование инструментов

40

• Пример трехмерной игры: Raiders 3D

48

Для того чтобы разогреть ваш интерес, в этой главе мы рассмотрим некоторые общие темы программирования игр, такие как игровые циклы и различия между двумерными и трехмерными играми.
В конце мы создадим небольшую трехмерную игру, чтобы правильно
настроить компилятор и DirectX. Если вы уже прочитали мою первую
книгу Программирование игр для Windows. Советы профессионала, то,
наверное, можете бегло просмотреть эту главу и сосредоточиться на
материале, изложенном в самом конце. Если же нет, вам определенно стоит прочитать ее — даже если вы игровой программист среднего
или высокого уровня. Итак, вот содержание главы:
• краткое введение в программирование игр;
• элементы двумерных и трехмерных игр;
• общие советы по программированию игр;
• использование инструментов;

• пример игры для Windows: Raiders 3D.

Краткое введение
Эта книга на самом деле представляет собой второй том серии (вероятно, трехтомной), посвященной программированию двумерных и трехмерных игр. В первом томе
Программирование игр для Windows. Советы профессионала рассматриваются в первую очередь следующие темы.
• Программирование для Windows
• Интерфейс прикладного программирования (API) Win32


Основы DirectX



Искусственный интеллект



Основы физического моделирования

• Звук и музыка
• Алгоритмы
• Программирование игр
• Двумерная растровая и векторная графика
Эта книга продолжает предыдущую. Тем не менее, я попытался написать ее так, что
если вы не читали первую, то все равно сможете получить изданной книги массу информации по трехмерной графике реального времени и ее применению для создания трехмерных игр. Следовательно, идеальный читатель данной книги — это тот, кто прочел
первую книгу серии и интересуется программированием трехмерных игр, либо тот, кто
уже умеет делать двумерные игры и стремится освоить трехмерные методы с точки зрения написаний программ и алгоритмов.
Помня об этом, я собираюсь сосредоточиться в этой книге на трехмерной математике
и графике, и в меньшей степени касаться всего, что относится к программированию игр.
Я исхожу из того, что это вы уже умеете, — если нет, я в очередной раз советую вам прочитать Программирование игр для Windows. Советы профессионала (либо любую другую
книгу по программированию игр) и посидеть за компьютером (до ощущения усталости),
изучая Windows, DirectX и игровое программирование в целом.
С другой стороны, даже если вы не читали предыдущую книгу и ничего не знаете
о программировании игр, вы все равно кое-что почерпнете для себя из этой книги — хотя
бы потому, что здесь много иллюстраций. Мы собираемся заниматься от главы к главе
созданием трехмерного игрового процессора, однако чтобы сэкономить время (и около
1500 страниц), мы начнем с базового процессора DirectX, разработанного в первой книге.
Конечно, этот процессор использовал DirectX версий 7 и 8; сейчас DirectX изменился и в
версии 8.0+ поддержка двумерных приложений стала сложнее, поскольку DirectDraw теперь объединен с Direct3D. Мы собираемся продолжить работу с интерфейсами DirectX 7
и 8, но компилировать наши приложения с DirectX 9.O.
Это книга о программном обеспечении и алгоритмах, а не о DirectX. Поэтому я мог
бы написать программу под DOS, и такой материал был бы вполне уместен. Нам необходимы только в меру сложная графика, система ввода/вывода и работа со звуком — для этих целей более чем достаточно DirectX 8.0+. Другими словами, если вы
приверженец Linux, перенос игрового процессора под SDL пройдет без проблем!

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

30

ЧАСТЬ1. ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

рого, впрочем, прилагается), поскольку в данной книге мы будем лишь добавлять трехмерный аспект к тому, что уже было сделано ранее.
Не беспокойтесь, в следующей главе мы полностью рассмотрим API и структуру двумерного игрового процессора, а также все функции, которые он выполняет. Я также покажу вам ряд демонстрационных программ, чтобы заинтересовать вас в использовании
базового процессора DirectX, созданного в первой книге.
В этой книге я хочу попытаться представить максимально общую, "виртуальную" точку
зрения на графическую систему. Хотя код будет основан на игровом процессоре из первой
книги, главное в том, что этот игровой процессор не делает ничего, кроме настройки графической системы с двойной буферизацией со стандартной линейной адресацией.
Таким образом, если вы хотите перенести такой код на платформу Мае или Linux, он
должен нормально заработать — после нескольких часов работы (максимум — до недели). Моя цель — научить вас работать с трехмерной графикой и математикой в целом.
Так сложилось, что доминирующей вычислительной системой в настоящий момент является DirectX на платформе Windows, поэтому именно на ней я и построил низкоуровневую обработку. И мы посвятим наше время рассмотрению именно этих концепций
верхнего уровня.
Мне уже приходилось писать о двумерных и трехмерных игровых процессорах,
и у меня на жестком диске есть соответствующий материал. Однако когда я пишу книгу,
я предпочитаю создавать новый игровой процессор, т.е. я пишу игровой процессор для
книги, а не книгу для игрового процессора. В этой книге я не использую трехмерный игровой процессор повторно, а создаю его. Поэтому я не вполне уверен, что мне удастся эта
книга! Мне даже интересно узнать, что же выйдет из этой затеи. Вы узнаете все необходимое для того, чтобы создать собственный игровой процессор Quake, но я могу по ходу
дела переключиться на процессор для игр на открытом воздухе или какой-то еще — кто
знает, куда меня занесет? Я исхожу из того, что работа над процессором гораздо полезнее, чем коде примечаниями автора.
Наконец, читатели моей первой книги могут заметить, что часть материала одинакова
в обеих книгах. В самом деле, я не могу сразу начать с ЗО-графики без использования материала из предыдущей книги. Я не могу рассчитывать, что у каждого есть моя первая
книга и не могу заставить купить ее. В любом случае, в первых нескольких главах материал будет несколько перекликаться с первой книгой— по крайней мере, в отношении
DirectX, игрового процессора и Windows.

Элементы двумерных и трехмерных игр
Для начала давайте посмотрим, чем видеоигра отличается от любого другого вида
программ. Видеоигры— очень сложный вид программ, писать которые труднее всего.
Конечно, написать что-нибудь типа MS Word труднее, чем игру типа Asteroids, но написать что-то вроде Unreal, Quake Arena или Halo труднее, чем любую программу, которую
можно себе представить, включая военную программу управления вооружениями!
Это означает, что вам нужно изучить новый способ программирования, который более подходит для написания моделирующих приложений реального времени, чем программ, управляемых событиями, или последовательных логических программ. Видеоигра— это непрерывный цикл, в котором исполняется логический сценарий и рисуется
картинка на экране — обычно с частотой не менее 30-60 кадров в секунду. Это напоминает фильм, но фильм, которым можно управлять.
Давайте начнем с рассмотрения упрощенной схемы игрового цикла (рис. 1.1). Вот
краткое описание стадий цикла.
ГЛАВА 1. Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

31

Возврат в операционную систему

Визуализация
следующего
кадра

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

Рис. 1.1. Общая архитектура цикла игры

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

Стадия 2. Вход в игровой цикл
На этой стадии выполнение кода достигает входа в игровой цикл. Здесь начинается
действие игры, и оно продолжается, пока пользователь не выйдет из основного цикла.

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

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

ЧАСТЬ I. ВВЕДЕНИЕ В ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Стадия 5. Визуализация следующего кадра
На этой стадии данные, полученные от игрока, и результаты исполнения программы
искусственного интеллекта и логики используются для формирования следующего кадра
игрового изображения. Это изображение обычно формируется во внеэкранной буферной
зоне. Его визуализацию невозможно увидеть. Затем происходит быстрое копирование
изображения на экран, в результате чего возникает анимационный эффект. В случае
трехмерного программного игрового процессора происходит визуализация тысяч (в некоторых случаях миллионов) многоугольников, формирующих игровую сцену. В случае
трехмерного игрового процессора с аппаратным ускорением, основанного на OpenGL
или DirectSD, значительная часть работы перекладывается на аппаратный ускоритель.

Стадия 6. Синхронизация экрана
Скорость, с которой компьютер исполняет игровой код, зависит от уровня сложности
игры. Например, если на экране находится 1000 движущихся объектов, нагрузка на центральный процессор будет значительно больше, чем в случае только с 10 объектами. Следовательно, частота кадров в игре будет меняться, что неприемлемо. Поэтому нужно
обеспечить постоянную частоту кадровъ игре с помощью функций для работы со временем и функций ожидания. В настоящее время 30fps (кадров в секунду, frame per second)
считается минимально допустимым значением, a 60fps— идеапъным. Частоты выше
60fps вряд ли оправданны, поскольку мозг с трудом обрабатывает информацию, поступающую с такой высокой скоростью.
Хотя в ряде игр и достигается идеальная скорость 30-60 кадров в секунду, она может
упасть с увеличением уровня сложности визуализации. Тем не менее, с помощью
контролируемых по времени расчетов в разделах игровой логики, физики и искусственного интеллекта можно, по крайней мере, попытаться обеспечить согласование
сцены во времени, т.е., например, при меньшей частоте обновления кадров объекты
будут проходить большее расстояние между двумя кадрами.

Стадия 7. Начало нового цикла
Эта стадия довольно проста — всего лишь возврат к началу игрового цикла и повторение его заново.

Стадия 8. Завершение работы
Это конец игры, в том смысле, что пользователь выходит из основного раздела кода
или игрового цикла и желает возвратиться в операционную систему. Однако, прежде чем
сделать это, необходимо освободить все ресурсы и очистить систему так же, как это делается в случае исполнения любой программы.
Следует отметить, что приведенное объяснение немного упрощенное, однако оно
верно отображает суть происходящего. В действительности игровой цикл в большинстве
случаев является конечным автоматом, содержащим ряд состояний. Например, ниже
Приведен более подробный код, иллюстрирующий, как может выглядеть игровой цикл
/S 'Состояния ЦИКЛЗ ИГрЫ

#defineGAME_INIT //Инициализация игры
#define GAME_MENU // Режим меню
tfdefine GAME_5TART // Подготовка к запуску
tfdefine GAME_RUN
// Работа игры запущена
^define GAME_RESTART // Подготовка к перезапуску
tfdefine GAME^EXIT // Выход из игры
ГЛАВА 1 . Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

33

// Глобальные переменные игры
int game_state = GAME_JNIT; // Начинаем с этого состояния
int error = 0;
// Используется для возврата
// сообщения об ошибке
// операционной системе
//Основная программа-Функция main
void main()

1

// Реализация основного цикла игры
while (game^state!=GAME_EXIT)
//Состояние игры
switch (ga me_state)
case GAMEJNJT:// Инициализация игры
// Выделение памяти и ресурсов
// Возврат и состояние меню
game_state-GAME_MENU;
} break;
case GAME_ME:NU: // Режим меню

{
// Вызов основного меню и изменение
//состояния игры
game_5tate = Menu();
// Примечание: здесь можно перейти в
//состояние RUN
} break;
case GAME_START: // Игра готова к запуску

{
// Это состояние необязательно, но
// обычно оно используется для настройки
// состояния готовности. Здесь можно
//заняться вспомогательными мероприятиями
Setup_For_RLm();
// Переключаемся в состояние запуска
game_stat:e = GAME_RUN;
} break;
caseGAME_RUN: //Игра запущена

{
// Здесь содержится полный цикл
//логики игры
// Очистка экрана
OearQ;
// Получение входных данных

ЧАСТЬ!. ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Get_Input{);
// Выполнение программы логики и
// искусственного интеллекта
Do_Logic();
// Отображение следующего кадра анимации,
// визуализация двумерного или
//трехмерного мира
Render_Frame();
// Стабилизируем частоту вывода
WaitQ;
// Единственный способ изменить
// состояние - это взаимодействие с
// пользователем в разделе ввода данных
// или, возможно, прерывание игры
} break;
case GAME_RESTART: // Перезапуск игры
{
// Здесь выполняется очистка для решения
// всех проблем для нового запуска игры
FixupQ;
// Возврат в меню
game_state = GAME_MENU;
} break;
case GAME_EXIT: // Выход из игры
{
// Если игра находится в этом состоянии,
// пришло время освободить ресурсы и
// выйти из игры
Release_And_Cleanup();
// Устанавливаем код ошибки
error = 0;
// Примечание: переключать состояние не
//требуется,т.к. на следующей стадии
// код выходит из основного цикла
// и возвращается в ОС
} break;
default: break;
} // switch
} // while
// Возврат кода ошибки операционной системе
return(error);
} // main

ГЛАВА 1. Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ игр

35

Хотя этот код нефункционален, я думаю, что изучив его, вы поймете идею структуры
реального цикла игры. Все циклы игр — не имеет значения, трехмерных или двумерных, — довольно точно повторяют приведенную структуру. На рис. 1.2 показана диаграмма переходов между состояниями игрового цикла. Как видно, переходы между состояниями довольно последовательны.
Состояние 2
Состояние О
I Game_Start Г

"^^^
Состояние 3

Возврат в ОС

Рис. 1.2. Диаграмма переходов между логическими состояниями цикла игры

Общие советы по программированию игр
Следующее, о чем я хочу поговорить, — это общепринятые методы и философия программирования игр, о которых вам следует подумать и которые следует принять (если, конечно, вы это сможете). Они значительно облегчают процесс программирования.
Начнем с того, что видеоигры — это компьютерные программы, требующие высокой
производительности компьютера. Это означает, что для разделов кода с высоким требованием к машинному времени и памяти больше нельзя использовать высокоуровневые
API. В большинстве случаев следует самому писать все, что касается внутреннего цикла
игрового кода, иначе игра будет работать ужасающе медленно. Конечно, это не означает,
что нельзя полагаться на такие API, как DirectX, поскольку DirectX специально написан
так, чтобы быть максимачьно быстрым при минимальном размере кода. Но в целом следует избегать вызовов высокоуровневых функций Win32 API. Например, вы можете решить, что функция memsetQ достаточно быстрая, однако она заполняет память побайтово. Значительно лучше использовать версию функции, которая заполняет память сразу
по четыре байта (одно слово). Вот пример функции на встроенном ассемблере, которую
я использую для заполнения памяти по четыре байта.
inline void Mem__Set_QUAD(void *dest UINT data, int count)

'
// Заполнение памяти по 32 бита, count— размер памяти
// в четырехбайтовых словах

36

ЧАСТЬ!. ВВЁДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

mov edi, dest ; edi указывает адрес памяти
mov ecx, count; Количество 32-битовых слов
mov eax, data ; 32-битовые данные
rep stosd ; Перенос данных

} //asm
} // Mem_Set_QUAD
А вот версия для двухбайтового слова.
inline void Mem_SetJ40RD(void *dest, USHORT data, int count)

'. // Заполнение памяти по 16 битов, count— размер памяти
// в двухбайтовых словах

_asm
{

mov edir dest ; edi указывает адрес памяти
mov ecx, count; Количество 16-битовых слов
mov ax, data ; 16-битовые данные
rep stosw ; Перенос данных

} //asm
} // MemJSetJVORD
Эти несколько строк кода могут в некоторых случаях ускорить игру в два или четыре
раза! Поэтому вам обязательно следует знать, что находится внутри функции API, если
вы собираетесь ее использовать.
Кроме того, Pentium III, 4 и последующие версии поддерживают SIMD-команды
(Single Instruction Multiple Data — один поток команд и множество потоков данных),
которые позволяют распараллеливать обработку простых математических операций, поэтому здесь открывается большое поле деятельности для оптимизации базовых математических операций, таких как векторная математика и умножение матриц. К этому мы вернемся позднее.
Давайте взглянем на перечень приемов, о которых следует помнить во время программирования.
Не бойтесь использовать глобальные переменные. Многие видеоигры не используют
передачу параметров в критических по времени выполнения функциях, вместо этого
применяя глобальные переменные. Рассмотрим, например, следующую функцию,
void PLot(int x, int у, int color)
(
// Вывод точки на экран
video_buffer[x-t-y*MEMORY_PITCH]« color;
}//Plot
В данном случае тело функции выполняется гораздо быстрее, чем ее вызов, вследствие необходимости внесения параметров в стек и снятия их оттуда. В такой ситуации более эффективным может оказаться создание соответствующих глобальных
переменных и передача информации в функцию путем присвоения им соответствующих значений,

ГЛАВА 1. Основы ПРОГРАММИРОВАНИИ ТРЕХМЕРНЫХ игр

37

int gx, gy, gcotor; // Глобальные переменные
void Plot_G(void)
\
// Вывод точки на экран
v?deo_buffer[gx + gy*MEMORY_PITCH] -gcolor;
}//Plot_G
Используйте встраиваемые функции. Предыдущий фрагмент можно еще больше
улучшить с помощью директивы inline, которая полностью устраняет код вызова
функции, указывая компилятору на необходимость размещения кода функции непосредственно в месте ее вызова. Конечно, это несколько увеличивает программу, но
1
скорость для нас гораздо важнее .
inline void Plotjfint x, int у, int color)

I

// Вывод точки на экран
videoJbufrer[x + y*MEMORy_PITCH] -color;

Заметим, что здесь не используются глобальные переменные, поскольку компилятор сам заботится о том, чтобы для передачи параметров не использовался стек.
Тем не менее, глобальные переменные могут пригодиться, если между вызовами
изменяется только один или два параметра, поскольку при этом не придется заново
загружать старые значения.
Всегда используйте 32-битовые переменные вместо 8- или 16-битовых. Pentium и более
поздние процессоры — 32-битовые, а это означает, что они хуже работают со словами
данных размером 8 и 16 битов и их использование может замедлить работу в связи с эффектами кэширования и другими эффектами, связанными с адресацией памяти. Например, вы можете создать структуру, которая выглядит примерно следующим образом.
struct CPOINT
{
short x, у;
unsigned chare;
}//CPOINT
Хотя использование такой структуры может показаться стоящей идеей, на самом
деле это вовсе не так! Эта структура имеет размер 5 байтов;
(2*si2eof(short)+sizeof(unsigned char)) -5. Это не очень хорошо в силу особенностей адресации памяти у 32-битовых процессоров. Гораздо лучше использовать следующую
структуру.
struct CPOINT
i
intx,y;
int с;
}//CPOINT
Такая структура гораздо лучше. Все ее элементы имеют одинаковый размер —
4 байта, а следовательно, все они выровнены в памяти на границу DWORD. Несмотря
на выросший размер данной структуры, работа с ней осуществляется эффективнее,
чем с предыдущей.

' Теоретически возможна ситуация, когда из-за использования встроенных функций внутренний цикл может вырасти и перестать полностью помещаться в кэше процессора, что приведет к
снижению производительности по сравнению с использованием обычных функций. Злоупотреблять встроенными функциями не следует, и не только по этой причине. — Прим. ред.
ЧАСТЬ!. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ игр

В действительности вы можете доводить размер всех своих структур до величины,
кратной 32 байтам, поскольку это оптимальный размер для стандартного кэша процессоров класса Pentium. Довести размер до этого значения можно путем добавления
дополнительных искусственных членов структур либо посредством соответствующих
директив компилятора. Конечно же, такой рост размера структур приведет к перерасходу памяти, но он может оказаться оправданным увеличением скорости работы.
struct в C++ представляет собой аналог class, у которого все члены по умолчанию открыты (public).
Тщательно комментируйте ваш код. Программисты, работающие над играми, попьзуются дурной славой в связи с тем, что не комментируют свой код. Не повторяйте их
ошибку. Ясный, хорошо комментированный код стоит лишней работы с клавиатурой.
Программируйте в стиле RISC. Другими словами, делайте ваш код как можно более
простым. В частности, в силу особенностей архитектуры процессоры класса Pentium
предпочитают простые инструкции сложным, да и компилятору работать с ними и
оптимизировать их легче. Например, вместо кода
if ( (х +- (2*buffer[inde*H-])) > Ю )
I
// Выполняем некоторые действия
используйте код попроще

x+=2*buffer[index];
index-H-;
if (x > 10)
!
// Выполняем некоторые действия
На то есть две причины. Во-первых, такой стиль позволяет при отладке вставлять
дополнительные точки останова между разными частями кода. Во-вторых, такой
подход облегчает работу компилятора по оптимизации этого кода.
Вместо умножения целых чисел на степень двойки, используйте побитовый сдвиг.
Поскольку данные в компьютере хранятся в двоичном виде, сдвиг влево или вправо
эквивалентен, соответственно, умножению или делению, например:
inty_pos = 10;
// Умножаем y_pos на 64
y_pos = {y_pos « 6); // 2 Л 6 = 64
// Делим y_pos на 8

y_pos = (y_pos » 3); // (1/3)Л3 - 1/8
Вы еще встретитесь с подобными советами в главе, посвященной оптимизации.
Используйте эффективные алгоритмы. Никакой ассемблерный код не сделает алгоритм о(п:1 более быстрым. Лучше использовать более эффективные алгоритмы,
чем пытаться оптимизировать никуда негодные.

ГЛАВА 1. Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

39

Не оптимизируйте ваш код в процессе программирования. Обычно это просто пустая трата времени. Перед тем как приступать к оптимизации, завершите написание
если не всей игры, то по крайней мере, основной ее части. Тем самым вы сохраните
силы и сэкономите время. Только когда игра готова, наступает время для ее профилирования и поиска проблемных участков кода, которые следует оптимизировать.
Не используйте слишком сложные структуры данных для простых объектов. Не стоит
использовать связанные списки для представления массива, количество элементов
которого точно известно заранее, только в силу того, что такие списки — это очень
круто. Программирование видеоигр на 90% состоит из работы с данными. Храните
их в как можно более простом виде с тем, чтобы обеспечить как можно более быстрый и простой доступ к ним. Убедитесь, что используемые вами структуры данных
наиболее подходят для решения ваших задач.
Разумно используйте C++. Не пытайтесь применять множественное наследование только
потому, что вы знаете, как это делается. Используйте только те возможности, которые реально необходимы и результаты применения которых вы хорошо себе представляете.
Если вы видите, что зашли в тупик, остановитесь и без сожалений вернитесь назад. Лучше потерять 500 строк кода, чем получить совершенно неработоспособный проект.
Регулярно делайте резервные копии вашей работы. При работе с игровой программой достаточно легко восстановить какую-нибудь простую сортировку, но восстановить систему искусственного интеллекта — дело совсем другое.
Перед тем как приступить к созданию игры, четко организуйте свою работу. Используйте понятные и имеющие смысл имена файлов и каталогов, выберите и придерживайтесь последовательной системы именования переменных, постарайтесь разделить графические и звуковые данные по разным каталогам.

Использование инструментов
Раньше для написания видеоигр обычно не требовалось ничего, кроме текстового редактора и, возможно, кустарных графических и звуковых программ. Сегодня ситуация намного усложнилась. Для написания трехмерной игры необходим, как минимум, компилятор C/C++, программа создания двумерных изображений, программа обработки звука,
а также какой-нибудь минимальный разработчик трехмерных объектов (если только вы не
собираетесь вводить все ЗО-модели как ASCII-данные). Кроме того, понадобится музыкальный секвенсор, если вы собираетесь использовать какие-либо MIDI-данные.
Давайте рассмотрим некоторые наиболее популярные программы и их возможности.
• Компиляторы C/C++. Для работы под Windows 9x/Me/XP/2000/NT нет лучшего
компилятора, чем MS VC++ (рис. 1.3). Он делает все, что может вам потребоваться, и даже больше того. Генерируемые им .ЕХЕ-файлы обладают максимально быстрым кодом. Компилятор фирмы Borland также хорошо работает, однако имеет
меньше возможностей. В любом случае вам не нужна полная версия — достаточно
студенческой версии, способной генерировать Win32 .ЕХЕ-файлы.
• Программы для двумерной растровой графики. Сюда относятся программы для
рисования, черчения и обработки изображений. Программы для рисования
позволяют рисовать изображения попиксельно с помощью примитивов и редактировать их. Насколько мне известно, Paint Shop Pro фирмы JASC

40

ЧАСТЫ. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИБТРЕХМЕРНЫХ ИГР

(рис. 1.4) является лидером по показателю соотношения цены и производительности. Но все же несомненным фаворитом большинства компьютерных
художников является Adobe Photoshop ~ он обладает гораздо большей мощностью, чем та, которая когда-либо может вам понадобиться.

Рис, 1.3. Среда Microsoft VC++

Программы для двумерной векторной графики. Эти программы позволяют создавать изображения, которые состоят в основном из кривых, прямых и двумерных геометрических примитивов. Этот тип программ не столь полезен, однако вам потребуется одна из них. Adobe Illustrator — это то, что вам нужна,
Программы для завершающей обработки изображений. Редакторы изображений — это финальный класс программ для двумерной графики. Эти программы предназначены больше для завершающей обработки, чем для создания
изображений. В этой области фаворитом многих пользователей является Adobe
Photoshop, однако мне больше по душе Corel Photo-Paint. Решать вам.
Программы для обработки звука. 90% всех звуковых эффектов, используемых
в играх, — это оцифрованный звук. Для работы со звуковыми данными этого
типа вам понадобится программа обработки звука. Одна из лучших программ
этого рода— Sound Forge (рис. 1.5). Несомненно, это одна из самых сложных
программ обработки звука. Я все еше открываю в ней новые и новые замечательные возможности! Еще одна довольно мощная программа — Cool Edit Pro,
но с ней я работал меньше.
Программы разработки трехмерных объектов. Здесь все зависит от ваших финансовых возможностей. Такие программы могут стоить десятки тысяч долларов. Тем не
менее, опыт показывает, что последние версии не очень дорогих редакторов трехмерных объектов могут в буквальном смысле создавать кино. Для разработки простых и среднего уровня сложности трехмерных моделей я использую, главным образом, Caligari trueSpace (рис. 1.6). Это самый лучший ЗО-редактор по такой цене— он стоит всего несколько сот долларов. Кроме того, я считаю, что у него
самый удобный интерфейс.
ГЛАВА 1. Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

Рис. 14. Paint Shop Pro фирмы JASC Software

Рис. 1.5. Sound Forge в работе

Если же вам недостаточно этих возможностей и требуется полный фотореализм — используйте 3D Studio Max (рис, 1.7). Однако он стоит 2500долл., так что тут
есть о чем задуматься. В то же время, поскольку мы собираемся использовать эти
программы в большинстве случаев лишь для создания трехмерных сеток, а не для визуализации, все эти "рюшечки и финтифлюшечки1' нам не нужны, и наилучшим выбором будет trueSpace или, возможно, какая-то бесплатная или условно-бесплатная
программа.

ЧАСТЬ). ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Рис. 1.6. Редактор Caligari trueSpace

Рис. 1.7. Тяжеловес 3D Studio Max
В Internet можно найти множество бесплатных или условно-бесплатных 30-редакторов,
которые иногда имеют такие же возможности, как и коммерческий продукт. Поэтому,
если денежные средства ограничены — ищите, и, возможно, вы найдете то, что нужно.

Редакторы трехмерных уровней
В этой книге мы собираемся создать программный трехмерный игровой процессор.
Однако мы не намерены создавать сложные инструменты для моделирования трехмер-

ГЛАЕА 1. ОСНОВЫ ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

43

ного интерьера. Трехмерный мир можно создать с помошью редактора ЗО-объектов, однако есть программы, гораздо лучше приспособленные для этого, такие как WorldCraft
(рис. 1.8). Следовательно, самый правильный подход при написании трехмерной игры и
игрового процессора — это использовать файловый формат и структуру данных, совместимые с наиболее доступным из редакторов, таким как WorldCraft (или подобным).
В этом случае для создания миров вы сможете воспользоваться инструментами сторонних разработчиков. Конечно, файловый формат большинства таких редакторов основан
преимушественно на разработках фирмы id Software и игрового процессора Quake. Тем
не менее, будет ли это файловый формат Quake или какой-либо иной, в любом случае
очевидно, что этот формат проверен и работает прекрасно.

Рис. 1.8. Редактор уровней WorldCraft
Музыкальные программы и MIDI-секвенсоры. В современных играх есть два типа
музыки: цифровая (как на компакт-дисках) и MIDI-музыка (Musical Instruments
Digital Interface — цифровой интерфейс музыкальных инструментов), представляющая собой синтетический объект, основанный на нотных данных. Для управления MIDI-данными необходим секвенсор. Один из лучших— CakeWalk
(рис. 1.9). Одним из достоинств CakeWalk является приемлемая цена. Поэтому, если вы намерены работать с MIDI-музыкой, я весьма рекомендую познакомиться с
этой программой. Мы поговорим о MIDI-данных, когда речь пойдет о DirectMusic.
А сейчас кое-то на десерт... Некоторые производители программ, упоминавшихся
выше, разрешили мне записать на компакт-диск свои условно-бесплатные или ознакомительные версии. Ознакомьтесь с ними!

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

ЧАСТЬ I. ВВВДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Рис. 1.9. Программа CakeWalk
Излагаемые здесь сведений относятся к Visual C++, однако на принципиальном
уровне они касаются всех компиляторов.

1. Прочтите руководство пользователя
я умоляю вас!

компилятора полностью —

пожалуйста,

2. Вы должны установить в системе DirectX 9.0 SDK. Чтобы сделать это, зайдите
в каталог DirectX SDK на компакт-диске, прочтите README.TXT и выполните все,
что там написано, т.е. щелкните на файле запуска установки DirectX SDK. И помните, в этой книге мы используем DirectX 9.0 (однако пользуемся интерфейсами
версий 7 и 8), так что вам нужно использовать DirectX SDK с компакт-диска. Хотя — если вы действительно этого хотите — можете компилировать все с помощью
DirectX 8.O.
3. Мы собираемся создавать Win32 .ЕХЕ-модули приложений, а не DLL, не компоненты ActiveX, не консольные приложения и не что-либо еще (если только я не скажу
вам об этом отдельно). Поэтому первое, что нужно сделать, это создать новый проект и установить в качестве выходного файла Win32 .ЕХЕ. На рис. 1.10 показано, как
сделать этот шаг в компиляторе Visual C++ 6.0.
Есть два типа .ЕХЕ-модулей, которые можно компилировать: Release и Debug. Окончательный (Release) обладает более быстрым и оптимизированным кодом, тогда как отладочный (Debug) медленнее и содержит контрольные точки отладчика. Советую во
время разработки программы использовать Debug-версию, а затем, когда программа
заработает, переключить компилятор в режим Release.

Для добавления исходных файлов в проект используйте команду Add Files и:) главного меню или из самого узла проекта. Этот шаг для Visual C++ 6.0 показан на
рис. 1.11.

ГЛАВА 1. Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

Рис, 1.10. Создание Win32 .ЕХЕ-модуля с помощью Visual
C++6.0

Рис. J. 11. Добавление файлов в проект в среде Visual C++ 6.0

Когда вы компилируете что-то, для чего нужен DirectX, необходимо указать в пути
поиска файлов компилятором путь к месту нахождения заголовочных файлов DirectX, а также .LIB-файлов DirectX. Это можно сделать с помощью пункта меню
Options, Directories в главном меню интегрированной среды разработки. Затем
сделайте соответствующие добавления в переменные Include Files и Library Files.
Этот шаг показан на рис. 1.12 (конечно, следует убедиться, что в них указан путь
к конкретному месту инсталляции DirectX, а также новейших версий библиотек
и заголовочных файлов).
Проверьте, чтобы узлы поиска DirectX были первыми в списке(ах). Вы же не хотите,
чтобы компилятор нашел старые версии файлов DirectX, которые могли быть установлены вместе с компилятором?

46

ЧАСТЬ!. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Рис. 1.12. Настройка пути поиска
файловв VisualC++6.0
6. Кроме того, вам обязательно нужно включить в проект библиотеки импорта СОМинтерфейса DirectX, перечисленные ниже и показанные на рис. 1.13.


DDRAW.UB



DSOUNO.LIB



DINPUT.LIB



DINPUT8.UB

• А также другие, которые я упоминаю в конкретных примерах.
Вход
Вывод
Game.EXE

nyrbOXSDK\Lib
П р име ча н и е: пользоватеп и
компилятора Borland могут
на йт и Bor land - ае реи и
библиотек импорта DirectX
в каталоге DXSDK\Lib\8otland

Заголовочные
файлы DirectX:
DDraw.H
DSound.H
Dlnput.H

Путь DXSOK\Include

Рис. 1.13. Ресурсы, необходимые для создания приложения Win32
DirectX
Эти .LIB-файлы DirectX расположены в каталоге LIB\, находящемся там, куда был установлен DirectX SDK. Вам необходимо добавить эти .LIB-файлы в свой проект. Однако
нельзя просто добавлять каталог LIB\ в путь поиска — тем самым компилятору/компоновщику указывается место поиска, но не говорится о том, что нужно использовать именно DirectX .LIB-файлы. Я получил тысячи (действительно тысячи!) электронных писем от людей, которые не сделали этого и получили проблемы. Повторю еще раз:
вам необходимо вручную включить .LIB-файлы DirectX в список связывания компилятора вместе с другими библиотеками или в проект вместе с вашими .СРР-файлами. Это
можно сделать в подменю Project, Settings диалогового окна Link, General в списке Object/Library-Modules (рис. 1.14).
ГЛАВА 1, Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

47

Рис. 1.14. Добавление .LIB-файлов DirectX в список
связывания Visual C++ 6.0
Если вы используете Visual C++, можете добавить в проект библиотеку расширений
Multimedia Windows (WINMM.LIB). Этот файл находится в каталоге LIB\ в месте установки компилятора Visual C++. Если его там нет, воспользуйтесь командой Find меню
Start. Найдя файл, добавьте его н список связывания.

7. Теперь вы готовы к компиляции программ.
Для пользователей Borland в DirectX SDK есть каталог BORLAND\. Убедитесь, что добавлены
именно эти .LIB-файлы, а не файлы DirectX, установленные вместе с Visual C++.

Если у вас есть еще вопросы по этой теме, не волнуйтесь — на протяжении книги при
компилировании программ я буду возвращаться к этим операциям много раз. Однако если я получу письмо с вопросами по компилированию и меня будут спрашивать о том,
о чем только что говорилось, я за себя не ручаюсь!
Я уверен, что вы слышали о новой системе Visual .NET. Технически это компилятор
и технологияпрограммирования Microsoft под новой маркой. Система нормально
работает только на Windows XP/2000 и избыточна для наших целей, поэтому мы остановимся на Visual C++ 6.0. Тем не менее, все работает и при использовании .NET:
Win32 есть Win32.

Пример трехмерной игры: Raiders 3D
Прежде чем у нас начнут плавиться мозги от разговоров о математике, программировании трехмерных игр и графике, я бы хотел сделать паузу и показать уже готовую трехмерную космическую игру — простую, но все же игру. При этом вы увидите, что такое
настоящий игровой цикл, некоторые вызовы графических функций и компиляция. Звучит неплохо, не правда ли?
Проблема состоит в том, что это только первая глава, поэтому я не могу использовать
материал из последующих глав ~ это было бы нечестно, согласны? И я еще не показал
вам игровой процессор из предыдущей книги. Поэтому я решил приучить вас пользоваться для программирования игр API как "черным ящиком". И эта простая игра позволит вам привыкнуть к идее использования такого API, как DirectX и игрового процессора
из первой книги.

48

ЧАСТЫ. ВВЕДЕНИЕ в ПРОГРАММИРОВАМИЕТРЕХМЕРНЫХ ИГР

Хотя в 70-х, 80-х годах и в начале 90-х годов прошлого столетия можно было все делать самому, в 21 веке в компьютере функционирует слишком много подсистем
оборудования и программного обеспечения, что делает практически невозможным
написание всего кода одним человеком. Увы, мы всего лишь разработчики игр. и
наш удел — использовать чужие API, такие как Win32, DirectX и т.п.
Основываясь на принципе "черного ящика", я поставлю вопрос так: каков абсолютный
минимум средств для создания 16-разрядной трехмерной каркасной космической игры?
Все, что нам необходимо от API — это следующая функциональность:
• переключение в любой графический режим с помощью DirectX;
• рисование цветных линий и пикселей на экране;
• получение ввода с клавиатуры;
• проигрывание звуковой записи из .WAV-файла, находящегося на диске;
• проигрывание МШЬмузыки из .MID-файла, находящегося на диске;
• синхронизация игрового цикла с помощью функций работы со временем;
• вывод на экран строк цветного текста;


копирование на экран двойного буфера (внеэкранной страницы визуализации),

Вся эта функциональность, конечно, содержится в библиотеке игровых модулей
T3DLIB*, созданной в предыдущей книге и состоящей из шести файлов: T3DLIBl.CPP|H,
T3DLIB2.CPPIH, и ТЗОИВЗ.СРР'Н. Модуль 1 предназначен для базовой работы с DirectX, модуль 2 — с Directlnput и DirectSound, а модуль 3 — в основном с DirectMusic.
Основываясь на использовании минимального набора функций из игровой библиотеки
T3DUB*, я написал игру под названием Raiders 3D, которая демонстрирует ряд концепций, уже
обсуждавшихся в данной главе. Кроме того, поскольку это каркасно-трехмерная игра, массу
кода вообще не имеет смысла писать — и это хорошо! Хотя я буду кратко объяснять используемые математику и алгоритмы, не тратьте много времени, пытаясь понять, что к чему: эта
информация дается только для того, чтобы позволить шире взглянуть на вещи.
Raiders 3D иллюстрирует все основные компоненты реальной трехмерной игры,
включая игровой цикл, вычисления, искусственный интеллект, определение столкновений, звук и музыку. На рис. 1.15 показана копия экрана работающей игры. Конечно, это
не Star Wars, однако совсем неплохо для нескольких часов работы!
Прежде чем показать исходный код игры, я хочу, чтобы вы получили представление о
том, из каких компонентов состоит проект (рис. 1.16). Как видно из рисунка, игра состоит из следующих файлов, являющихся частью проекта:


RAIDERS3D.CPP — основной блок логики игры, который использует функциональность T3DLIB и создает минимальное >№п32-приложение;

• T3DLIB1.CPP — исходные файлы библиотеки T3DLIB;


T3DLIB2.CPP;



T3DLIB3.CPP;

• T3DLIB1.H — заголовочные файлы библиотеки;
• T3DLIB2.H;
• T3DUB3.H;
• DDRAW.LIB — DirectDraw — компонент двумерной графики в составе DirectX. Для
создания приложения требуется библиотека импорта DDRAW.LIB. Она не содержит
ГЛАВА 1. Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

49

код DirectX; это промежуточная библиотека, обеспечивающая обращение к динамической библиотеке DDRAW.DLL, которая выполняет реальную работу. Этот файл
можно найти в DirectX SDK в каталоге LIB\;

Рис. J, 15. Копия экрана Raiders 3D
Файлы C++
Raiders3D.cpp
DOLibl.cpp
BDLib2.cpp
T3DLib3.cpp
(should all be in root)

файлы DirectX
D Draw. Lib
DSound.Lib
Dlnput.Lib
+ Headers
Windows мультимедиа
Winmm.Lib| •« • — Эти фар
быть BKS
поумол

Заголовочные файлы
T3DUM.H
тзоиьг.н
T3DLib3.H
(should all be in root)
еОайлы игры

i

> vc

'

'



~}— *• Raiders3D.EXE

Компилятор

Рис. 1.16. Структура кода Raiders 3D
DINPUT.LIB/DINPUT8.UB— Directlnput — компонент пользовательского ввода в составе DirectX. Для создания приложения требуются библиотеки импорта DINPUT. LIB и DINPUT8.LIB;
DSOUND.LIB— DirectSound— компонент цифрового звука в составе DirectX, Для
создания приложения требуется библиотека импорта DSOUND.LIB.
Отметим, что не существует файла DMUSIC.LI8, хотя DirectMusic используется
в T3DLIB. Это так, поскольку DirectMusic — это чистый СОМ-объект. Иными словами,
нет библиотеки импорта, содержащей интерфейсные функции для обращения к DirectMusic — вам нужно все делать самому, К счастью, я уже сделал этот для вас!

ЧАСТЬ I. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Следующие файлы не нужны компилятору или компоновщику, но они являются выполняемыми DLL-файлами DirectX, которые загружаются при запуске игрового приложения:
• DINPUT.DLL/DINPUT8.DLL — это динамически компонуемые библиотеки DirectDraw,
которые содержат СОМ-реализацию функций интерфейса Directlnput, вызываемых через библиотеку импорта OINPUT.LIB. Здесь вам не нужно ни о чем беспокоиться, проследите лишь, чтобы были установлены исполняемые файлы DirectX;


DDRAW.DLL— это динамически компонуемая библиотека DirectDraw, которая содержит СОМ-реализацию интерфейсных функций DirectDraw, вызываемых через
библиотеку импорта DDRAW.LIB;

• DSOUND.DLL — это динамически компонуемая библиотека DirectSound, которая содержит СОМ-реализацию интерфейсных функций DirectSound, вызываемых через
библиотеку импорта DSOUND.LIB;


DMUSIC.DLL— это динамически компонуемая библиотека DirectMusic, которая содержит СОМ-реализацию интерфейсных функций DirectMusic, вызываемых непосредственно через СОМ-обращения.

Основываясь на нескольких вызовах библиотеки, я создал игру RAIDERS3D.CPP, показанную в нижеприведенном листинге. Игра запускается в оконном режиме с 16разрядной графикой, поэтому проследите, чтобы экран находился в 16-битовом цветовом режиме.
Хорошо изучите представленный код: главный цикл игры, трехмерную математику, а
также вызовы других функций.
// Raiders3D - RAIDERS3D.CPP - наша первая трехмерная игра
// ПРОЧТИТЕ ЭТО!
// Перед компиляцией убедитесь, что включили в список
// связывания проекта файлы DDRAW.LIB, DSOUND.LIB,
// DINPUT.LIB, WINMM.LIB, а также исходные модули
// T3DLIB1.CPP,T3DLIB2.CPP и T3DLIB3.CPP в проект!!!
// Включите заголовочные файлы T3DLIB1.HJ3DLIB2.H и
//T3DLIB3.H в рабочий каталог, с тем чтобы компилятор
// мог найти их.
// Для запуска игры убедитесь, что экран установлен в
// 16-битовый цветовой режим с разрешением 640x480 или выше

// INCLUDES /////////////////////////У/////////////////////
fldefine INITGUID // Гарантируем доступность СОМ-интерфейсов
// Вместо этого можно подключить .UB-файл
// DXGUID.LIB
«define WIN32_LEAN_AND_MEAN
tfincLude // Подключаем функциональность Windows
tfinclude

JrmcLude
tfinclude // Подключаем функциональность С/С-н-

((include
ftincLude

ГЛАВА 1. Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ игр

51

#include
^include
#indude
#include
#inclLide
#include
#incliide
#include
^include // Подключаем DirectX
#include
^include
#include
fifnclude
Sindude
tfincLude
^include "TSDLIBl.rT // Подключаем библиотеку T3D
tfindude "T3DLIB2.h"
Include "T3DLIB3.h"
// DEFINES ////////////У///////////////////////////////////
// Определяем интерфейс windows
tfdefine WINDOW_CLASS_NAME "WIN3DOASS"// Имя класса
^define WINDOW_TITLE "T3D Graphics Console Ver 2.0"
^define WINDOW_WIDTH 640
// Размер окна
#defineWINDOW_HEIGHT 480
«define WINDOW_BPP 16 // Битовая глубина цвета окна
// (8Д6,24 и т.д.)
// Примечание: если работа идет в окне и не используется
//полноэкранный режим работы,то битовая глубина цвета
// должна быть такой же, как в системе. То же самое в
// случае создания и подключения 8-раэрядной палитры
fldefine WINDOWED_APP I // 0 - не оконный режим, 1 - оконный

iliiiiliiiiliiilinilillllillliililflllliilliiiiliiillliilll
^define NUM_STARS 250 // Число звезд в модели
^define NUM_TIES 10 // Число боевых кораблей в модели
// Константы 30-игрового процессора
#define NEAR_2 10 // Ближняя плоскость отсечения
ffdefine FAR_Z
2000 // Дальняя плоскость отсечения
tfdefine VIEW_DISTANCE 320 // Расстояние видимости для данной
// точки обзора. Оно дает размер
// поля зрения при угле зрения 9ff
// в случае проекции на окно
// шириной 640 пикселей
// Константы игрока
tfdefine CROSS_VEL 8 // Скорость, с которой движется
// перекрестие прицела
52

ЧАСТЫ. ВВВДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ игр

«define PLAYER_Z_VEL 8 // виртуальная z-скорость игрока
// для имитации перемещения
// Константы модели боевого корабля
#defineNUM_TIE_VERTS ю
«define NUM_TIE_EDGES 8
//Данные о взрыве
fldefine NUM_EXPLOSIONS (NUMJTIES)// Общее число взрывов
//Состояния игры
tfdefineGAME_RUNNING 1
tfdefine GAME_OVER 0

// типы uiiiiiiijiiniiiiiiiiiiiijiiiiiiiiiiiiuiiiiiiiiii
II Трехмерная точка
typedef struct PQINT3D_TYP
(
USHORT color; // 16-битовый цвет точки
float x,y,z; // Координаты точки
} POINTS D,*POINT3D_PTR;
// 30-линия, два индекса в списке вершин
typedef struct LINE3D_TYP
{
USHORT color; // 16-битовый цвет линии
int vl,v2; // Индексы конечных точек в списке вершин
}LINE3D,*l_INE3D_PTR;
// Боевой корабль
typedef struct TIEJTYP
I
int state; // Состояние боевого корабля:
// 0=мертв, 1-жив
float х, у, г; // Координаты корабля
float xv,yv,zv; // Скорость корабля
} TIE, *TIE_PTR;
// Базовый 30-вектор, используемый для скорости
typedef struct VEC3D_TYP

[

float x,y,z; // Координаты вектора
}VEC3D,
// Каркасный взрыв
typedef struct EXPLJTYP

{

int state; // Состояние взрыва
int counter; // Счетчик взрывов
USHORT color; // Цвет взрыва

ГЛАВА 1. Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

53

// Взрыв - это совокупность ребер/линий,
// основанная на модели взрывающегося корабля
POINT3D pl[NUMJ"IE^EDGES]; // Начальная точка ребра п
POINT3D p2[NUM_TIE_EDGE$]; // Конечная точка ребра п
VEC3D veL[NUM_TIE_EDGESJ; // Скорость осколков
} ЕХРЦ *EXPL_PTR;
// ПРОТОТИПЫ /////////////////////////////////////////////
// Консоль игры
int Game_Init(void *parms=NULL);
int Game_Shutdown(vofd *parms=NULL);
int Game_Main(void *parms=NULL);
// Функции игры
void Initjie(int index);
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ/////////////////////////////////
HWND main_jvindow_handte = NULL;//Дескриптор окна
HINSTANCE main_ins1:ance = NULL; // Сохраняем экземпляр
charbuffer[256];
//Используется для
// вывода текста
// Корабль - это совокупность вершин, соединенных
//линиями, которые образуют форму
POINT3D tie_vUst[NUM_TIE_VERTS]; // Список вершин модели
// боевого корабля
LINE3D tie_shape[NUM_TIE_EDGES]; // Список ребер корабля
TIE ties[NUM_TIES];
// Боевые корабли
POINT3Dstars[NUM_STARS];

//Звездное поле

// Некоторые цвета мы не можем создать, пока не знаем формат
// цвета — 5.5.5 или 5.6.5, поэтому мы подождем немного
// и сделаем это в функции Game_Init()
USHORT rgb_green,
rgb_wnite,
rgb_red,
rgbjjlue;
// Переменные игрока
float cross_x - 0, // Перекрестие прицела
cross__y = 0;
int cross_x_screen - WINDOW_WIDTH/2, // Перекрестие прицела
cross_y_screen -WINDOW_HEIGHT/2,
target_x_screen - WINDOW_WIDTH/2r
target_y_screen - WINDOW_HEIGHT/2;
intptayer_z_veL = 4;// Виртуальная скорость
54

ЧАСТЬ I. ВВВДЕНИЕ в ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХ ИГР

// наблюдателя/корабля
int cannon_state = 0; // Состояние лазерной пушки
int cannon_count- 0; // Счетчик выстрелов из пушки
EXPL explosions[NUM_EXPLOSIONS]; // Взрывы

int misses « 0; // Число кораблей, которые
// не удалось поразить
int hits -0; //Число попаданий
int score - 0; // Счет

// Музыкальные и звуковые данные
int main_track_id ** -1, // Идентификатор музыкальной дорожки
laseMd
- -1, // Звук лазерной вспышки
explosionjd --!,// Звук взрыва
flybyj'd - -1; // Звук пролетающего корабля
int game_state-GAME_RUNNING;// Состояние игры

LRESULT CALLBACK WindowProcfHWND hwnd,
UINT msg,
WPARAM wparam,
LPARAM Iparam)

(

// Обработчик сообщений системы
PAINTSTRUCT ps; //Используется в WM^PAINT
HOC
hdc; // Контекст устройства
// Какое сообщение получено?
switch (msg)
{
case WM_CREATE:

{

// Инициализация данных
return (0);
} break;
case WM_PAINT:
{
//Начало рисования
hdc - BeginPaint(hwnd,&ps);
// Окончание рисования
EndPaint(hwnd,&ps);
return (0);
} break;
caseWM.DESTROY:

{

// Закрываем приложение

ГЛАВА 1. Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

55

PostQuitMessage(O);
return (0);
} break;
defaultrbreak;
}// switch
// Обработка необработанных сообщений
return (DefWindowProc:(hwnd, msg, wparam, Iparam)};
}//WinProc

iiuiiHiiiniiiiiuuimiiniiiiiiuniuiiiii
intWINAPIWinMain( HINSTANCE hinstance,
HINSTANCE hprevinstance,
LPSTRlpcmdLine,
int ncmdshow)

WNDCLASS winclass; //Создаваемый класс
HWND hwnd; // Обобщенный дескриптор окна
MSG
msg;
// Обобщенное сообщение
HOC
hdc;
//Дескриптор контекста устройства
PAINTSTRUCT ps; // Структура вывода
//Заполняем структуру класса
winclass.style
- CS „DBLCLKS | CSJDWNDC |
CS^HREDRAW | CSJ/REDRAW;
winclass. LpfnWndProc -WindowProc;
winclass.cbClsExtra -0;
winclass. cbWnd Extra »0;
windass. hinstance - hinstance;
wincLass.hkon
- LoadIcon(NULU IDIJVPPLICATION);
winclass.hCursor •= LoadCursorfNULL IOC_ARROW);
winclass. hbrBackground»
(HBRUSH)GetStockObj'ect(BLACICBRUSH);
winclass.lpszMenuName - NULL;
winclass.LpszClassName-WINDOW_CLASS_NAME;
// Регистрируем класс окна
if {!RegisterClass(&windass})
return (0);
// Создаем окно. Обратите внимание на проверку
//для выбора надлежащего флага окна
if (!(hwnd = CreateWinclow(WINDOW_CLASS^NAME,// Класс
WINDOW_TITLE, //Заголовок
(WINDOWED_APP?
(WS_OVERLAPPED | WS_SYSMENU) :
(WS_POPUP|WS_VISIBLE)},
OA
//x,y
56

ЧАСТЫ. ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

WINDOWJVIDTH, // Ширина
WINDOW_HEIGHT, // Высота
NULL // Дескриптор родителя
NULL //Дескриптор меню
hinstance,
// экземпляр
NULL))}
//Параметры
return (0);
// Сохраняем дескриптор и экземпляр
// окна в глобальной переменной
main_window_handle = hwnd;
main_instance = hinstance;
// Изменим окно, чтобы клиентская область имела
// размер width x height
if(WINDOWED_APP)
{
// Изменим размер окна так, чтобы клиентская область
// имела именно тот размер, который указан в запросе,
// поскольку, если приложение работает в окне, там
// могут быть рамки и панели органов управления.
// Если приложение работает не в окне, это не имеет
// значения
RECTwindow_rect=-{0,0,WINDOW_WIDTH,WINDOW_HEIGHT};
// Вызов для настройки window_reet
AdjustWindowRectEx(&window_rect,
GetWindowStyle{main_window_handle),
GetMenu(main_window__handle)!- NULL
GetWindowExStyLe(main_window_handle));
// Сохраним глобальные переменные смещения клиента,
// необходимые для DDraw_FLip{)
window_client__xQ = -window_rect.left;
window_client_yO = -window_rect.top;
// Изменим размер окна с помощью вызова MoveWindowQ
MoveWindow(main_window_handle,
CWJJSEDEFAULT,// Координата x
CW.USEDEFAULT,//Координата у
// Ширина и высота
window_rect.right - window_rect.left,
window_rect.bottom - window_rect.top,
FALSE);
// Выводим окно
ShowWindow(main_window_handle/SW_SHQW);
}// if windowed
// Выполняем инициализацию параметров игровой консоли
Game_Init();
// Отключаем CTRL-ALT-DEL, ALT-TAB. Закомментируйте
ГЛАВА 1. Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

57

// эту строку, если она вызывает зависание системы
SystemParametersInfo(SPI_SCREENSAVERRUNNING,TRUE,NlJLL,0);
// Входим в главный цикл событий
whi'Le(l)
{
if(PeekMessage(&msg,NULUOAPM_REMOVE))
(
// Проверяем, не следует ли выйти
if (msg.message — WM_QUIT)
break;
//Транслируем горячие клавиши
TranslateMessage{8.msg);
// Посылаем сообщение обработчику окна
DispatchMessagef&msg);
// Основная работа игры выполняется здесь
Game_Main();
}// while
// Выходим из игры и освобождаем все ресурсы
Game_Shutdown();
// Включаем CTRL-ALT-DEL, ALT-TAB, закомментируйте
// эту строку, если она приводит к зависанию системы
SystemParametersInfQ(SPI_SCREENSAVERRUNNING,
FALSE,NULU>);
// Возвращаемся в Windows
return(msg.wParam);
}//WinMain
// ФУНКЦИИ КОНСОЛИ ИГРЫ T3D II ////////////////////////////
int Game_Init(void *parms)
I
//Инициализация параметров игры
int index;
Open_Error_File(" error, tort");
// Запускаем DirectDraw (заменяем параметры по желанию)
DDraw_Init(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOWLBPP,
WINDOWED_APP);
//Инициализируем Directlnput
DInput_Init();

58

ЧАСТЬ I. ВВВДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

// Запрашиваем клавиатуру
DInput_Init_Keyboard();
// Инициализируем DirectSound
DSound_Imt();
//Загружаем звуковые файлы
exptosionjd - DSound_Load_WAV("expl.wav"};
Laserjd * DSound_Load_WAV("shocker.wav"};
// Инициализируем DirectMusk
DMusic_Init();
//Загружаем и запускаем музыкальный трек
main_track_id-DMusic_Load_MIDI("midifile2.mid");
DMusic_PLay(main_track_id);
// Добавляем вызовы для ввода данных иэ других
// устройств Directlnput
// Прячем мышь
ShowCursorCFALSE);
// Инициализируем генератор случайных чисел
srand(Start_aockO);
// Создаем системные цвета
rgb_green= RGB16Bit(0,31,0);
rgb_white = RGB16Bit(31,31,31);
rgb_bLue -RGB16Bit(0,0,31);
rgb_red -RGB16Bit(31,0,0);
// Создаем звездное поле
for (index-0; index < NUM_STARS; index++)
{
// В беспорядке располагаем звезды в цилиндре,
// вытянутом отточки наблюдателя (ОД-d) в
// направлении отсекающей плоскости (0,0,far_z)
stars[index].x- -WINDOW_WIDTH/2 +
rand{)%WINDOW_WIOTH;
stars[index].y = -WINDOW_HEIGHT/2 +
rand{)%WINDOW_HEIGHT;
stars[index].z - NEAR_2 +
rand()%(FAR_Z-NEAR_Z);
//устанавливаем цвета звезд
stars[index].color* rgb_white;
}//for index
// Создаем модель боевого корабля
// Список вершин боевого корабля
POINT3D temp_tie_vlist[NUNLTIE_VERTS] ГЛАВА 1. Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

59

//Цвет,х,у,2
{
{rgb_white,-40,40,0}, // рО
{rgb_white,-40, 0,0}, //pi
{rgb_white,-40,-40,0}, // p2
{rgb_white,-10, 0,0}, //p3
{rgb^white, 0, 20,0}, // p4
{rgb_whiter 10, 0,0}, // p5
(rgb_white, 0,-20,1)}, // рб
{rgb_white, 40, 40,0}, // p7
{rgb^white, 40, 0,0}, // p8
{rgb_white, 40,-40,0}}; // p9
// Копируем модель в глобальные массивы
for (index=0; index 15)
cannon_state - 2;
-// фаза охлаждения
if (cannon_state -- г)
if (-H-cannon_count > 20)
cannon_state - 0;
// Перемещаем звездное поле
Move_StarfieLd();
// Перемещаем и выполняем блок искусственного
// интеллекта для кораблей
Process_Ties();
// Обработка взрывов
Process_Explosions();
//Закрываем задний буфер
DDraw_Lock_Back_Surface();

70

ЧАСТЫ. ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХ игр

// Рисуем звездное поле
Draw_Starfield();
// Рисуем боевые корабли
DrawJTiesQ;
// Рисуем взрывы
Draw_ExpLosions();
// Рисуем перекрестие прицела
// Вначале вычисляем экранные координаты перекрестия
// прицела. Обратите внимание на знак по оси у
cross_x_screen = WINDOWJVIDTH/2 +• cross_x;
- WINDOW_HEIGHT/2 - cross_y;
// Рисуем перекрестие прицела в экранных координатах
Draw_Clip_Linel6(cross_x_screen-l6,cross_y_screen,
cross _x_screen+16,cross_y_screen,
rgb_redfback_bufferfback_lpttch);
Draw_Clip_Linel6(cross_x_screen,cros$_y_screen-16,
cross_x_screen,aos5_y_screen+16,
rgb_red,back_buffer,back_Lpitch);
Draw_Qip_Linel6(cross_x_screen-16,cro$s_y_screen-4,
cross_x_screen-16,cross_y_screen+4,
rgb_red,back_bufferfback_lpitch);
DrawJClip_Linel6(cross_x_screen+16,cross_y>jcreen-4,
cross_x_screen+16,cross_y_screen+4,
rgb_red,back_buffer,back_lpitch);
// Рисуем лазерные лучи
if (cannon_state — 1)
{
if ((rand(J%2 — 1))
\
// Правый луч
Draw_CLip_Linel6{WINDOW_WIDTH-l,WINDOW_HEIGHT-l,
-4+rand(j%8+target_x_screen,
-4+rand()%8+target_y_screen,
RGB16Bit(0,0,rand()),
back_buffer,back_lpitch);
else
I
//Левый луч
Draw_Clip_Linel6(0,WINDOW_HEIGHT-l,
-4-*-rand()%8+target_x_streen,
-4+rand{}%8+target_y_screen,
RGB16Bit(0,0,rand()>,
back_buffer,back_Lpitch);
ГЛАВА 1. Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

71

}//ff
// Визуализация завершена, открываем
// поверхность заднего буфера
DDraw_Untock_Back_$urface{);
// Выводим текстовую информацию
sprintf (buffer, "Score %d Kills %d Escaped %d",
score, hits, misses};
Draw_Text_GDI(bLiffer, OARGB(0,255,0), Ipddsback);
if {game_state— GAME_OVER)
DrawJexLGDIf'G A M E 0 V E R", 320-8*10,240,
RGB(255,;255,255), Ipddsback);
// Проверяем, закончилась ли музыка,
// если да - перезапускаем
if(DMusic_Status_MIDI(main_trackJd)-=MIDI_STOPPED)
DMusic__Play(main_track_id);
// Меняем поверхности
DDraw_Flip();
// Синхронизация 30 кадров в секунду
Wait_Clock(30);
// Проверяем переключатель состояния игры
if (misses > 100)
game_state - GAM E_OVER;
// Проверяем, выходит ли пользователь из игры
if (KEY_DOWN(VK_ESCAPE) || keyboard_state[DIKJSCAPE])

I

PostMessage(main_window_nandle,WM_DESTROY,0,0);

// Возвращаем код завершения
return (1);
JV/GameJ-lain

iilHlilllllllilllillllitlliillllltlililllllllllUtlllllll
Очень немного для трехмерной игры, правда? Это реальная трехмерная игра
Win32/DirectX.
Прежде чем мы займемся анализом кода, я хочу, чтобы вы сами скомпилировали его.
Я запрещаю вам двигаться дальше, пока компиляция не завершится успехом! Надеюсь,
я понятно выразился. Итак, займитесь настройкой компилятора в соответствии с описанными выше инструкциями для создания Win32 .ЕХЕ-приложениЙ, указывая пути поиска и создавая списки связывания, настроенные для DirectX. Затем, когда проект будет
готов, подключите исходные файлы T3DUB1.CPP, T3DLIB2.CPP, T3DLIB3.CPP, RAIDERS3D.CPP.

72

ЧАСТЫ. ВВЕДЕНИЕВПРОГРАММИРОВАНИЁТРЕХМЕРНЫХИГ

Конечно же, заголовочные файлы T3DUB1.H, T3DLIB2.H, T3DLIB3.H должны быть в рабочем
каталоге компилятора. И наконец, необходимо быть абсолютно уверенным, что вы
включили в проект .LIB-файлы DirectX вместе с .СРР-файлами или включили их в список
связывания. Вам необходимы лишь следующие .LIB-файлы DirectX: DDRAW..LIB,
DSOUND.LIB, DINPUT.LIB, DINPUT8.LIB.
Вы можете назвать .ЕХЕ-файл как угодно ~ возможно, TEST.EXE или RAIDERS3D_TEST.D£ —
однако не идите дальше, пока вы не сможете его скомпилировать.

Цикл событий
Главная точка входа для всех программ Windows — функция WinMainQ, точно так же,
как main()— главная точка входа для программ DOS/UNIX. В любом случае WinMainQ
создает окно для Raiders3D и затем входит прямо в цикл событий. WinMain() начинает
с создания и регистрации класса Windows. Затем создается окно игры, после чего производится вызов функции Game_Init(), которая выполняет инициализацию игры. После завершения инициализации выполняется вход в стандартный цикл событий Windows, который считывает сообщения. Если сообщение найдено, вызывается процедура Windows
WinProc, которая обрабатывает его. В противном случае вызывается функция игры
Game_Main(). Именно здесь происходит реальное действие игры.
Читатели предыдущей книги могут заметить, что в разделе инициализации функции
WinMainQ появился дополнительный код для обработки оконной графики и изменения размера окна. Эта возможность, а также поддержка 16-битового цвета являются
частью новой версии игрового процессора T3DLIB. Тем не менее, большая часть кода
в этой книге по-прежнему поддерживает 8-битовую графику, поскольку в общем
случае скорость 16-битовых программ все еще слишком мала.
При желании вы можете войти в бесконечный цикл Game_Main() и никогда больше не
возвращаться в основной цикл событий WinMainQ, но это было бы плохо, поскольку
в этом случае Windows не будет получать сообщения. Нам нужно рассчитать и вывести
один кадр анимации, а затем вернуться в WinMainQ. При этом Windows продолжит работу
и будет обрабатывать сообщения. Этот процесс показан на рис. 1.17.

Внутренняя логика игры
После выполнения блока игровой логики в функции Game_Main() производится визуализация изображения во внеэкранную рабочую область (двойной буфер, или на жаргоне
DirectX "задний буфер" (back buffer)). Завершающим этапом является вывод изображения на экран в конце цикла с помощью вызова DDraw^FlipQ, что создает иллюзию анимации. Игровой цикл состоит из стандартных разделов, определенных ранее в элементах
двумерных или трехмерных игр. Теперь я хочу сосредоточиться на ЗО-графике.
Логика искусственного интеллекта врага очень проста. Вражеский корабль создается
в случайной точке трехмерного пространства на расстоянии, превышающем видимость. Рисунок 1.18 показывает пространство вселенной Raiders3D. Как видно из рисунка, камера или
наблюдатель расположены в точке на отрицательной оси г с координатами (0,0,-zd)( где -zd —
расстояние от наблюдателя до виртуального окна, куда проецируется изображение. Здесь используется левая система координат (положительная ось z направлена в экран).
После того как вражеский корабль создан, он следует по заданному вектору траектории, пересекающей поле зрения игрока, т.е. он идет более или менее встречным курсом.
Вектор и начальное положение корабля генерируются функцией Init_Tie(). Вашей целью
как игрока является прицелиться во врага и выстрелить.
ГЛАВА 1. основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

73

Вызывается один
раз в начале

Вызывается для
каждого кадра
Ввод
Физика

J

Искусственный интеллект!
Визуализация

J

Вызывается в конце
Рис. 1.17. Схема системы обработки сообщений
Итак, как же генерируется трехмерная картинка? Вражеские корабли — это не что
иное, как многоуоэльные объекты (совокупность линий, которые образуют контур 3Dобъекта) — т.е. они скорее двумерные, а не полностью трехмерные. Ключевым моментом
трехмерности является перспектива. На рис. 1.19 показана разница между ортогональной
и аксонометрической проекцией. Это два основных типа проекций, используемых в системах трехмерной [рафики.


Дальняя
плоскость
отсечения

• Плоскость обзора
640 х 480

(0,0, -zd)
Камера

- V

Рис. 1.18. Вселенная RaidersSD
74

ЧАСТЬ I. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

а. Аксонометрическая проекция

б. Ортогональная проекция

Р,
Проецирующие
лучи

Точка проекции
Проецирующие
лучи

Рис. L19. Аксонометрическая и ортогональная аксонометрическая проекции

Трехмерные проекции
Ортогональная проекция хорошо подходит для технических чертежей и изображений,
где аксонометрическое искажение нежелательно. Математика ортогональной проекции
очень проста — по сути, происходит простое изменение координаты z каждой точки в соответствии со следующим уравнением.
Уравнение 1.1. Ортогональная проекция
Для точки с координатами (x,y,z)
УоПЬо ~ У'

Аксонометрическая проекция немного сложнее, и на данном этапе я не хочу слишком
вдаваться в ее объяснение. В целом, для получения двумерной проекции на экране с координатами (x^.y^) нам необходимо учитывать координату z, а также расстояние от
наблюдателя. Математика аксонометрической проекции показана на рис. 1.20. Она основана на вычислении координат подобных треугольников.
Уравнение 1.2. Аксонометрическая проекция
Для точки с координатами (x,y,z) с расстоянием до наблюдателя zd
х

_ zd-x

-~~7"'
_ zd-y
z

_

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

ГЛАВА 1. ОСНОВЫ ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

75

мерные объекты проецируются на двумерное поле зрения (экран) по законам аксонометрической проекции.

Проецируемая
точка

+2

+х (левая система координат)

Используя подобие треугольников,
можно записать:
Y_per
zd ,
__
_
Y_per = у, •

Рис. 1.20. Аксонометрическое преобразование

(-4,4)

PI

(-4,0)

.
р

э./

\

v

PS

\P5

7

РЭ

PZ

Список вершин Р0... Рэ

Список ребер

PO

M-4)

Ребро 0

Ро-Рг

PI

(-4,0)

Ребро 1

Рг

(-4-4)

Ребро 2

Р.-Р3
Р
з-Р*

P3

(-1.0)

Ребро 3

P*

(0, 2)

Ребро 4

(i.o)

Ребро В

Ps

PI

(a -2)

P?

(4.4)

f'8
Рэ

(4,0)
(4, -4)

Ребро 6
Ребро 7

Р

«-Р5

Р5-Р6

Р«-Р.

PS-PS
Р7-Р,

/*ис. 7.2Л Каркасная модель боевого корабля
Итак, мы преобразуем каждый вражеский корабль с помощью аксонометрической
проекции и выполняем его визуализацию на экран. Рис. 1.21 показывает каркасную мо76

ЧАСТЫ. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ игр

дель вражеского корабля, которая используется в качестве основы для визуализации.
Таблица положений каждого корабля хранится в следующем массиве.
// Боевой корабль
typedef struct TIE_TYP
I

int state; // Состояние корабля: 0=мертв, 1=жив
floatx,у,z; //Координаты корабля
float xv,yv,zv; // Скорость корабля
} TIE, *TIE_PTR;
Когда приходит время рисовать вражеские корабли, используются данные в виде
структуры TIE и мы получаем более-менее реалистичное трехмерное изображение движущегося объекта.
Вы также заметите, что корабли по мере приближения становятся ярче. Этот эффект
легко реализовать. Главный принцип состоит в том, что ось z используется в качестве
масштабирующего коэффициента при изменении яркости корабля.

Звездное поле
Звездное поле — это не что иное, как совокупность одиночных точек, генерируемых
некоторым источником в пространстве. Кроме того, после ухода из поля зрения игрока
они появляются заново. Точки подвергаются визуализации как полноценные 3Dобъекты в соответствии с перспективным преобразованием. Однако их размер— 1x1x1
пиксель, поэтому это "настоящие" точки и всегда выглядят как одиночные пиксели.

Лазерные пушки и обнаружение попаданий
Лазерные пушки, из которых стреляет игрок, — это не что иное, как двумерные линии, выходящие из углов экрана и сходящиеся на перекрестии прицела. Обнаружение
попаданий выполняется путем наблюдения за двумерными проекциями кораблей на поле зрения и проверкой того, попадают ли лазерные лучи в ограничивающий прямоугольник каждой проекции. Это процесс схематически показан на рисунке 1.22.
Этот алгоритм работает, поскольку лазерные лучи распространяются со скоростью
света. Не имеет значения, находится ли цель на расстоянии 10 метров или 10000 километров, — если вы прицеливаетесь и лазерный луч пересекает проекцию трехмерного
объекта, произойдет попадание.

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

Как играть в RaidersSD
Чтобы запустить игру— просто сделайте щелчок мышью на файле RAIDERS3D..EXE на
компакт-диске, и программа тотчас запустится. Клавиши управления:

ГЛАВА 1. Основы ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

77

• клавиши управления курсором — перемещение перекрестия прицела;
• пробел — огонь из лазерных пушек;
• Esc — выход из игры.
Игра использует DirectDraw, Directlnput, DirectSound и DirectMusic, поэтому убедитесь, что в системе установлен DirectX.
Объекты после визуализации

М
Объекты в системе обнаружения попаданий

-оВиртуальные
ограничивающие
прямоугольники

Рис, 1.22. Использование двумерных ограничивающих
прямоугольников для обнаружения попаданий

Резюме
Эта глава представляет собой лишь начало введения в программирование трехмерных игр. Самым важным здесь является компилирование DirectX-программ. Кроме
того, мы рассматриваем основы программирования игр, игровой цикл, основы аксонометрического преобразования SD-объектов, а также использование компилятора.
Здесь вкратце рассмотрена библиотека T3DLIB, разработанная в предыдущей книге
Программирование игр для Windows. Советы профессионала. Она позволит нам сосредоточиться на программировании трехмерных игр, а не на создании поверхностей, изучении интерфейса и загрузке звука. Теперь, когда вы уже умеете компилировать, поэкспериментируйте с игрой — добавьте противников, астероиды, что-нибудь еще — и
приступайте к чтению новой главы.

78

ЧАСТЫ. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХ ИГР

ГЛАВА 2

Краткий курс Windows
и DirectX
В этой главе...






Модель программирования Win32
Необходимый минимум знаний по программированию
для Windows
Базовое приложение для Windows
Краткий курс DirectX и СОМ
Краткое введение в СОМ

80
81
87
104
108

Основная цель этой главы — изложение краткого курса программирования для Windows и DirectX. Предполагается, что вы уже знакомы с основами программирования для Win32 и DirectX, однако я
попытаюсь так построить изложение, что даже если вы и незнакомы
с этим материалом, то все равно сможете просто использовать созданный мной API и сфокусироваться на аспектах трехмерного программирования. Для тех кто не имел удовольствия познакомиться с
программированием для Win32/DirectX, я позволю себе дать основные сведения по этой теме. Вот что мы рассмотрим в этой главе:
• программирование для Windows;
• циклы событий;
• написание простой Windows-оболочки;
• основы DirectX;
• модель составных объектов.

Модель программирования Win32
Windows — это многозадачная/многопоточная операционная система. Вместе с тем,
она управляется событиями. В отличие от большинства DOS-программ (собственно, всех
DOS-программ), большинство Windows-программ находится в состоянии ожидания действий пользователя (являющихся событиями), после чего Windows реагирует на событие
и выполняет определенные действия. На рис. 2.1 этот процесс показан графически. Как
видно из рисунка, оконные приложения посылают Windows свои события или сообщения для обработки. Определенная обработка выполняется самой Windows, однако большинство сообщений или событий передаются для обработки приложениям.
Системная очередь событий

Сообщения
направляются
каждому
приложению!

Рис. 2.1. Обработка событий в Window
Могу вас обрадовать: в большинстве случаев вам не придется заботиться о других работающих приложениях — Windows все сделает за вас. Все, что от вас потребуется, •— это побеспокоиться о собственном приложении и обработке поступающих для него сообщений. Это
было не совсем так в Windows 3.0/3.1. Эти версии Windows не были полностью многозадачными, и каждое приложение должно было само давать сигнал операционной системе о возможности переключения на другое приложение. Это создавало ощущение замедления работы
приложений в этих версиях Windows. Если какое-то приложение тормозило систему, другие
программы ничего не могли с этим поделать. В Windows 9x/Me/2000/XP/NT ситуация кардинально изменилась. Система при необходимости сама переключает приложения, причем так
быстро, что это невозможно заметить!
Теперь вы знаете все, что нужно, о принципах работы операционной системы. К счастью, на сегодняшний день Windows настолько хорошо приспособлена для написания игр, что вам не нужно беспокоиться о распределении ресурсов, планировании
процессов и тому подобных вещах. Все, о чем вы должны беспокоиться, — это игровой код и чувство предельных возможностей компьютера.

ЧАСТЬ!. ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЁХМЕРНЫХ ИГР

Минимальный курс программирования
для Windows
Теперь, когда вы уже имеете общее представление об операционной системе Windows,
некоторых ее свойствах и базовых принципах, самое время приступить к изложению
краткого курса программирования для Windows на примере нашей первой программы
для Windows.
Изучение любого нового языка программирования принято начинать с написания
программы "Hello World". Мы поступим так же. Ниже приведен листинг стандартной
DOS-версии программы "Hello World".
// DEMOII2_1.CPP — Стандартная версия программы hello world
Jfinclude
// Основная точка входа для всех стандартных DOS/консольных
//программ
void main(void)
{
printf("\nTHERECANBEONLYONE!!!\n");
} // main

Теперь посмотрим, как это делается в Windows.
Кстати, если вы компилируете ОЕМОП2_1.СРР в компиляторе Visual C++ или Borland, вы можете создать так называемое консольное приложение. Оно напоминает DOSприложение, однако это 32-битовое приложение. Это приложение работает только в текстовом режиме, но прекрасно подходит для тестирования идей и алгоритмов.

Все начинается с WinMain()
Выполнение всех программ для Windows начинается с функции WinMain(). Это эквивалент
функции main() в случае платформ DOS и UNIX. Что будет делать функция WinMain(J — целиком зависит от вас. При желании можно создать окно, начать обработку событий, вывод изображений на экран и т.д. С другой стороны, можно просто вызвать одну из сотен (или тысяч)
Win32 API-функций. Именно это мы и будем делать дальше.
В качестве примера я хочу вывести на экран некий текст в маленьком окне сообщений. Для этого существует специальная функция Win32 API — MessageBox(). Ниже приводится листинг нормальной, корректно компилируемой программы для Windows, которая создает и выводит на экран окно сообщения, которое можно двигать и при желании закрыть.
// DEMOII2_2.CPP - Простое окно сообщений
ffdefine WIN32_LEAN_AND_MEAN // MFC не используются
^include
^include

// Основные заголовочные файлы

// Главная точка входа для всех приложений Windows
int WINAPI WinMain(HINSTANCE hinstance,
HINSTANCEhprevinstance,
LPSTR Lpcmdline,
int ncmdshow)

ГЛАВА2. КРАТКИЙ КУРС WINDOWS и DIRECTX

81

// Вызов функции API с нулевым дескриптором родителя
MessageBox(NULU "THERE CAN BE ONLY ONE!']",
"MY FIRST WINDOWS PROGRAM",
MB_OK I MB_ICONEXCLAMATION);

// Выход из программы
return(O);
} //WinMain

Для компиляции программы выполните следующие шаги.
1. Создайте новый проект .ЕХЕ-приложения Win32 и включите в него файл
DEMOII2_2.CPP из каталога ТЗОПСНАР02\на компакт-диске.
2. Выполните компиляцию и компоновку программы.
3. Запустите ее (или готовую версию DEMOII2_2.EXE с компакт-диска).
Вы думали, что в Windows-программе сотни строк кода? Как бы то ни было, при компиляции и запуске программы вы должны увидеть нечто наподобие рис. 2.2.

Рис. 2.2. Копия экрана DEMOS2_2.EXE

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

Она заслуживает некоторого пояснения. Есть два пути написания программ для Windows: с помощью MFC (Microsoft Foundation Classes) или SDK (Software Development
Kit — набор средств для разработки программного обеспечения). MFC — значительно
более сложный инструмент, основанный полностью на C++ и классах. Возможности
MFC в десятки раз превышают ваши потребности в области программирования игр. С
другой стороны, SDK — это гибкий инструмент, который можно изучить в течение недели или двух (по крайней мере, его основы). Он использует обычный С. Поэтому SDK —
это именно то, чем мы будем пользоваться в этой книге.
Возвращаемся к объяснению. Макроопределение WIN32_LEAN_AND_MEAN дает компилятору команду не подключать внешние MFC классы.
Затем включаются следующие заголовочные файлы.
^include
^include

Первое включение (windows.h) на самом деле включает все заголовочные файлы Windows,
избавляя вас от необхо,димости вручную включать десятки заголовочных файлов.
Второе включение (windowsx.h)— это включение заголовочных файлов с наборами
макросов и констант, которые значительно облегчают программирование для Windows.
А вот и самая важная часть — главная входная точка всех приложений Windows, WinMainQ.
82

ЧАСТЬ!. ВВВДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

int WINAPI WinMain(HINSTANCE hinstance,
HINSTANCE hprevinstance,
LPSTR IpcmdLine,
int
ncmdshow);

Прежде всего, вы должны обратить внимание на этот странный декларатор объявления —
WINAPI. Этот эквивалент декларатора PASCAL, который заставляет компилятор передавать параметры слева направо, в отличие от обычного порядка передачи справа налево в принятом по
умолчанию соглашении о вызовах CDECL. Однако декларатор PASCAL устарел и его место занял
декларатор WINAPI. Вы обязаны использовать WINAPI для функции Win Main (), в противном случае загрузочный код будет неправильно передавать параметры в функцию!
Теперь давайте подробно рассмотрим каждый параметр функции.
• hinstance. Этот параметр представляет собой дескриптор экземпляра, который
Windows генерирует для приложения. Экземпляры используются для отслеживания
используемых ресурсов. В нашем случае hinstance используется для отслеживания
параметров приложения, таких как имя или адрес. При запуске приложения Windows передает ему данный параметр.
• hprevinstance. Этот параметр больше не используется, однако в предыдущих версиях Windows он использовался для отслеживания предыдущего экземпляра приложения, иными словами, экземпляра приложения, которое запустило данное.
• LpcmdLine. Строка с завершающим нулевым символом; аналог командной строки
стандартной C/C++ функции main()— с тем отличием, что здесь нет отдельного
параметра, аналогичного argc, показывающего количество параметров командной
строки. Например, если вы создаете Windows-приложение под названием TEST.EXE
и запускаете его со следующими параметрами;
TEST.EXE one two three

то IpcmdUne будет содержать следующие данные:
Ipcmdline - "one two three"
Заметим, что само имя. ЕХЕ-файла не является частью командной строки.
• ncmdshow. Этот последний параметр — просто целое число, передаваемое приложению во время запуска и определяющее, как именно должно открываться основное приложение. Таким образом, у пользователя есть небольшая возможность
управления тем, как будет запускаться приложение. Конечно, вы как программист
можете при желании отказаться от этой возможности. Однако этот параметр создан для случая, когда вы захотите воспользоваться им (вы передаете его ShowWindow(), однако здесь мы уже забегаем вперед). Таблица 2.1 содержит основные значения, которые может принимать параметр ncmdshow.
Таблица 2.1. Windows-коды для параметра ncmdshow
Код
SWJSHOWNORMAL

Значение кода
Активизирует и отображает окно. Если окно минимизировано или
максимизировано, Windows восстанавливает его до нормального
размера и положения. Приложение должно устанавливать этот
флаг при первом отображении окна

ГЛАВА 2. КРАТКИЙ КУРС WINDOWS и DIRECTX

83

Окончание табл. 2.1
Код

Значение кода

SW_SHOW

Активизирует окно и отображает его для данных размеров и положения

SW_HIDE

Прячет окно и активизирует другое окно

SW_MAXIMIZE

Максимизирует указанное окно

SW_MINIMIZE

Минимизирует указанное окно и активизирует следующее окно
в Z-порядке

SW_RESTORE

SW_SHOWMAXIMIZED

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

SW_SHOWMINIMIZED

Активизирует окно и минимизирует его

SW_SHOWMIN NOACTIVE

Минимизирует окно. При этом активное окно остается активным

SW_SHOWNA

Отображает окно в его текущем состоянии. Активное окно остается активным

SW_SHOWNOACTIVATE

Отображает окно с его последними размерами и в последнем положении. Активное окно остается активным

Как видно из табл. 2.1, есть множество кодов для ncmdshow (многие из которых в данный
момент не имеют смысла для нас). На практике большинство из них никогда не передаются
в cmdshow; вы будете использовать их с другой функцией — ShowWindowQ, которая отображает
окно после его создания. Однако мы рассмотрим этот вопрос несколько позднее. Сейчас я хочу обратить ваше внимание на то, что в Windows есть множество опций, dinaro» и т.п., которые
вы никогда не будете использовать, но все же они есть. Это аналогично возможностям видеомагнитофона: больше — это всегда лучше, но при этом вам необязательно использовать их
все, если вы этого не хотите. Именно так построена Windows. Чтобы сделать каждого пользователя счастливым, в ней предусмотрено множество возможностей. Но фактически 99% времени мы будем использовать только SW_SHOW, SW_SHOWNORMAL и SW_HIDE.
Теперь давайте поговорим о вызове функции MessageBox() внутри WinMain{). Этот вызов делает за нас всю работу. MessageBox() — это функция Win32 API, которая делает для
нас некоторые полезные вещи и тем самым избавляет нас от необходимости делать их
самим. MessageBoxQ используется для отображения сообщений с различными пиктограммами и одной или двумя кнопками. Как видите, отображение простых сообщений
в приложениях Windows — это тривиальная задача благодаря функции, написанной специально для того, чтобы программист каждый раз мог экономить полчаса времени на написание подобной программы.
MessageBoxQ делает не очень много, но этого достаточно для вывода на экран окна, задания вопроса и ожидания ввода пользователя. Ниже дается прототип MessageBoxQ.
int Message Box(
HWND hwnd,
//Дескриптор окна пользователя
LPCTSTR Iptext // Текст в окне сообщения
LPCTSTR Ipcaption,// Заголовок окна сообщения
UINT utype); //Стиль окна сообщения

84

ЧАСТЬ I. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХ ИГР



hwnd — дескриптор окна, к которому вы хотите присоединить окно сообщения.
Пока что мы не рассматривали дескриптор окна, поэтому считайте этот параметр
просто родителем окна сообщений. В случае DEMOII2_2.CPP мы устанавливаем его
равным нулю. Это означает, что мы используем в качестве родительского окна рабочий стол Windows.

• Iptext — строка с завершающим нулем, содержащая отображаемый текст.


Ipcaption — строка с завершающим нулем, содержащая заголовок диалогового окна
сообщений.

• utype — пожалуй, единственный интересный параметр из всего множества параметров функции. Он определяет тип отображаемого окна сообщений. В табл. 2.2
перечислены возможные значения этого параметра.
Таблица 2.2. Типы окон MessageBoxQ
Флаг

Значение флага

Общий вид окна сообщений
МВ_ОК

Окно сообщений содержит одну кнопку ОК. Это режим по
умолчанию

MELOKCANCEL

Окно сообщений содержит две кнопки: ОК и Cancel

MB_RETRYCANCEL

Окно сообщений содержит две кнопки: Cancel и Retry

MB_YESNO

Окно сообщений содержит две кнопки: Yes и No

MB_YESNOCANCEL

Окно сообщений содержит три кнопки: Yes, No и Cancel

MB_ABORTRETRYIGNORE

Окно сообщений содержит три кнопки: Abort, Retry и Ignore

Пиктограмма в окне сообщения
MB_ICONEXCLAMATION

В окне сообщений появляется пиктограмма с восклицательным знаком

MB^ICONINFORMATION

В окне сообщений появляется буква i в окружности.

MB_ICONQUESTIQN

В окне сообщений появляется пиктограмма с вопросительным знаком

MB_ICONSTOP

В окне сообщений появляется пиктограмма со знаком "стоп"

Кнопка, используемая по умолчанию
MB_DEFBUTTONn

n — это число (1...4), обозначающее номер кнопки (слева направо), используемой по умолчанию

Примечание. Имеются и другие флаги уровня операционной системы, однако для нас они не представляют интереса. При необходимости вы всегда сможете узнать о них в справочной системе
Win32 SDK.
Вы можете применять побитовое ИЛИ к значениям из таблицы 2.2 для создания требуемого окна сообщений. Обычно выбирают только один флаг из каждой группы.
И конечно, как и подавляющее большинство функций Win32 API, MessageBox() возвращает значение, которое позволяет узнать, что произошло. В нашем случае это никому
не нужно, но в общем случае может возникнуть необходимость узнать возвращаемое значение, например, если окно сообщения содержит вопрос Yes/No и т.п. Возможные возвращаемые значения перечислены в табл. 2.3.
ГЛАВА 2. КРАТКИЙ КУРС WINDOWS и DIRECTX

85

Таблица 2.3. Возвращаемые значения функции MessageBoxf)
Значение

Его смысл

IDABORT

Была выбрана кнопка Abort

IDCANCEL

Была выбрана кнопка Cancel

IDIGNORE

Была выбрана кнопка Ignore

IDNO

Была выбрана кнопка No

ШОК

Была выбрана кнопка ОК

IDRETRY

Была выбрана кнопка Retry

IDYES

Была выбрана кнопка Yes

Теперь я хочу, чтобы вы набрались терпения, поскольку вам придется вносить изменения в программу и компилировать ее различными способами. Попробуйте менять различные опции компилятора— такие как оптимизация, генерация кода и т.д. Затем попробуйте пропустить программу через отладчик и во всем этом разобраться.
Если вы хотите услышать какой-нибудь звук— поэкспериментируйте с функцией
MessageBeep(). О ней можно узнать в справочной системе Win32 SDK. По простоте использования она напоминает функцию MessageBox().
BOOL MessageBeep(UINT utype); // Звук, выводимый компьютером

Здесь различным звукам соответствуют константы, показанные в табл. 2.4.
Таблица 2.4. Тип звука функции MessageBeepQ
Параметр

Значение параметра

MBJCONASTERISK

Системный звук "звездочка"

MBJCONEXCLAMATION

Системный звук "восклицание"

MBJCONHAND

Системный звук "рука"

MB_ICONQUESTION

Системный звук "вопрос"

МВ_ОК

Системный звук по умолчанию

OxFFFFFFFF

Стандартный тоновый сигнал, воспроизводимый встроенным динамиком компьютера

Примечание. Если у вас установлены темы MS-Plus, то вы получите очень интересные результаты.
Ну что, теперь вы видите, насколько силен Win32 API? В нем буквально сотни интересных функций. Конечно, это не самые лучшие функции в мире, но для общего пользования, для системы ввода-вывода и графического интерфейса пользователя Win32 API
весьма удобен.
Теперь давайте резюмируем все, что мы знаем на данный момент о программировании для Windows. Во-первых, Windows — это многозадачная/многопоточная система,
а поэтому в ней можно одновременно запускать несколько приложений. Однако что нас
действительно интересует — это то, что Windows управляется событиями. Это означает,
что мы должны обрабатывать события (о чем мы на данный момент не имеем представления) и реагировать на них. Наконец, все программы Windows начинаются с функции
W i n M a i n Q , которая отличается от обычной DOS-функции mainf) наличием нескольких
дополнительных параметров, на что есть веские причины.

86

ЧАСТЬ!. ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХИГР

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

Базовое приложение для Windows
Поскольку цель этой книги — написание трехмерных игр, которые будут работать
в операционной системе Windows, нет необходимости много знать о программировании для
Windows. Фактически все, что нужно знать, — это базовую структуру программы для Windows, которая открывает окно, обрабатывает сообщения и вызывает основной игровой
цикл, — и это все. В данном разделе книги моя цель — это прежде всего научить вас создавать простые программы для Windows. Кроме того, мы выполним подготовительную работу
для создания игровой "оболочки", которая будет весьма похожа на 32-битовое DOS/UN IXприложение. Итак, давайте начнем.
Основной момент в любой Windows-программе— это открытие окна. Окно— это
всего лишь рабочая область, в которую выводится информация, такая как текст и графика, и с которой может взаимодействовать пользователь. Для создания полностью функционапьной программы для Windows необходимо выполнить следующие шаги.


Создать класс Windows.

• Создать обработчик событий (WinProc).


Зарегистрировать класс окна в операционной системе Windows.



Создать окно с созданным классом Windows.



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

Рассмотрим каждый шаг подробно.

Класс Windows
Windows—- это действительно объектно-ориентированная операционная система.
В Windows есть множество концепций и функций, которые берут свое начало из С и C++.
К ним относятся и классы окон. Каждое окно, элемент управления, список, диалоговое окно и другие объекты в Windows — это на самом деле окна. Друг от друга они отличаются определяющим их классом. Классы описывают типы окон, которые могут обрабатываться
системой Windows.
Есть множество предопределенных классов окон, таких как кнопки, окна списков,
окна выбора файлов и т.д. Вы можете создавать и свои собственные классы окон.
Вы должны создать, по крайней мере, один класс окна зля каждого приложения, которое
вы пишете, — иначе ваша программа будет, скажем так, скучноватой. Класс окна можно
рассматривать как шаблон, которому должна следовать Windows при выводе вашего окна
и при обработке сообщений для него.
Для хранения информации о классе окна есть две структуры данных WNDCLASS
и WNDCLASSEX. WNDCLASS старше и, вероятно, вскоре устареет. Поэтому мы будем использовать новую, расширенную версию WNDCLASSEX. Структуры очень похожи, и если вам интересно, то можете посмотреть на определение WNDCLASS в справочной системе Win32. Давайте рассмотрим, как определяется WNDCLASSEX в заголовочных файлах Windows.

ГЛАВА 2. КРАТКИЙ КУРС WINDOWS и DIRECTX

87

typedef struct _WNDCLASS EX
UINT cbSize;
// Размер данной структуры
UINT style;
// Флаги стиля
WNDPROC IpfnWndProc; //Указатель на обработчик
int cbClsExtra; // Доп. информация о классе
int cbWndExtra; //Доп. информация об окне
HANDLE hlnstance; // Экземпляр приложения
HICON hlcon;
// Основная пиктограмма
HCURSOR hCursor;
// Курсор окна
HBRUSH hbrBackground; // Кисть для рисования фона окна
LPCTSTR LpszMenuName; // Имя прикрепляемого меню
LPCTSTR IpszClassName;// Имя класса
HICON hlconSrn; //Дескриптор пиктограммы
}WNDCLASSEX;
Итак, все, что мы делаем, — это создаем одну из этих структур, а затем заполняем ее поля.
WNDCLASSEXwincLass;// Пустой класс окна
Теперь давайте рассмотрим, как заполнять каждое поле.
Первое поле cbSize очень важно. Это размер самой структуры WNDCLASSEX. Можно
спросить, а зачем структуре нужно знать, каков ее размер? Это хороший вопрос. Ответ состоит в том,, что если эта структура передается как указатель, получатель всегда
может проверить первое поле (4 байта), чтобы определить предельную длину пакета
данных и перейти сразу в его конец. Это своего рода предосторожность и небольшая
вспомогательная информация, благодаря чему другим функциям не нужно вычислять размер класса во время исполнения программы. Все, что нужно сделать, это записать
winclass.cbSize = sizeof(WNDCLASSEX);
Следующее поле имеет дело с флагами информации о стилях, которые описывают
обшие свойства окна. Этих флагов много, и я не собираюсь показывать их все. Достаточно сказать, что с их помощью можно создать любой тип окна. Хороший практичный набор флагов приведен в табл. 2.5. Для создания нужного типа окна можно использовать
эти параметры с оператором побитового ИЛИ.
Таблица 2.5. Флаги стилей классов окон
Флаг

Описание

CS_H REDRAW

Если при перемещении или коррекции размера изменяется ширина окна, требуется перерисовка всего окна

CS_VREDRAW

Если при перемещении или коррекции размера изменяется высота окна, требуется перерисовка всего окна

CS_OWNDC

Каждому окну данного класса выделяется свой контекст устройства
(более подробно об этом будет сказано позже)

CS_DBLCLKS

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

CS_PARENTOC

Устанавливает область обрезки дочернего окна равной области обрезки
родительского окна, так что дочернее окно может изображаться в родительском

88

ЧАСТЫ. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Окончание табл. 2.5
Флаг

Описание

C5_SAVEBITS

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

CS_NOCLOSE

Отключает команду Close в системном меню

Примечание. Наиболее часто употребляемые флаги выделены полужирным шрифтом.
Я привел только те флаги, которые используются в случае перерисовки окна при его перемещении или изменении размеров, а также в случае, когда требуется использовать контекст
устройства вместе с возможностью обработки событий при двойном щелчке мыши.
Контекст устройства используется при визуализации GDI-графики в окне. Если вы
хотите создавать графику, необходимо запрашивать контекст устройства для конкретного
интересующего вас окна. Если мы определяем класс окна, в котором есть собственный
контекст устройства, с помощью флага CS_OWNDC мы можем сэкономить время за счет
отказа от постоянного запрашивания контекста. Вот пример того, как с помощью выбора
стиля можно приспособить окно под наши потребности.
wincLass.styLe =CS_VREDRAW ] CS_HREDRAW |
CSJJWNDC | CS_DBLCLICKS;

Очередное поле структуры WNDCLASSEX— IpfnWndProc— представляет собой указатель на
функцию-обработчик событий. Это— функция обратного вызова (callback). Такие функции
достаточно широко распространены в Windows и работают следующим образом. Когда происходит некоторое событие, Windows уведомляет вас о нем вызовом предоставляемой вами
функции обратного вызова, в которой и выполняется обработка этого события.
Вы предоставляете функцию обратного вызова классу окна (конечно, функция должна иметь определенный прототип) и, когда происходит событие, Windows вызывает ее
для вас. Это схематически показано на рис. 2.3. Эта тема будет подробнее рассмотрена
в следующих разделах. А пока просто укажем используемую классом функцию событий.
wintlass.LpfnWndProc - WinProc; // Функция событий
Работа

Цикл

Вызов обработчика
Обработчик событий
в вашем приложении

Рис. 2.3. Функция обратного вызова обработчика событий Windows в действии
Если вы незнакомы с указателями на функции, то можно считать, что они в чем-то
похожи на виртуальные функции C++. Если вы незнакомы с виртуальными функциями, я попытаюсь объяснить, что это такое :-). Пусть у нас есть две функции, работающие с двумя числами.
ГЛАВА 2. КРАТКИЙ КУРС WINDOWS и DIRECTX

int Addfint opl,intopZ} {return (opl + op2); }
int Sub(int opl, int op2} {return (opl - op2); }
Вы хотите иметь возможность вызывать любую из них посредством одного и того же
вызова., Это можно сделать с помощью указателя на функцию следующим образом.
// Определим указатель на функцию, получающую в качестве
// параметров два целых числа и возвращающую целое число.
int(*Math)(intint);
Теперь вы можете присвоить значение указателю и использовать его для вызова соответствующей функции.
Math-Add;
int result -Math(l,2);// Вызывается Add(l,2)
//result = 3
Math = Sub;
int result = Math(l,2); // Вызывается Sub(l,2)
//result = -1
Красиво, не правда ли?
Следующие два поля, cbClsExtra и .ebWndExtra, первоначально были созданы, чтобы позволить Windows сэкономить немного места в классе окна для хранения дополнительной
информации об исполняемой программе. Большинство программистов не используют
эти поля и просто присваивают им значение 0.
winclass.cbClsExtra = 0; // Поля дополнительной информации
winclass.cbWndExtra == 0; // класса и окна
Следующее поле— hlnstance. Это просто дескриптор hinstance, передаваемый
в функцию WinMainQ при запуске.
winclass.hlnstance - hinstance; // Экземпляр приложения
Оставшиеся поля относятся к графическим аспектам класса окна, однако прежде чем
обсудить их, я хочу сделать небольшой обзор дескрипторов.
Мы постоянно встречаемся с дескрипторами в Windows-программах: дескрипторы
изображений, дескрипторы курсоров, дескрипторы чего угодно! Дескрипторы— это
всего лишь идентификаторы некоторых внутренних типов Windows. В действительности
это просто целые числа, но, так как теоретически Microsoft может изменить это представление, лучше все же использовать внутренние типы. В любом случае вы будете очень
часто встречаться с теми или иными дескрипторами. Запомните, что имена дескрипторов
начинаются с буквы h (handle).
Очередное поле структуры определяет пиктограмму, представляющую ваше приложение. Вы можете загрузить собственную пиктограмму или использовать одну из системных. Для получения дескриптора системной пиктограммы можно воспользоваться функцией Loadlcon().
winclass.hlcon = LoadIeon(NULUIDI_APPLICATION};
Этот код загружает стандартную пиктограмму приложения — невыразительно, зато очень
просто. Если вас интересует функция LoadlconQ, обратитесь к справке по Win32 API — там
приведен ряд готовых пиктограмм, которые можно использовать в своих приложениях.
Итак, половину полей мы уже прошли. Приступим ко второй половине, которая начинается с поля h Cursor. Оно похоже на поле hlcon в том плане, что также представляет
собой дескриптор графического объекта. Однако hCursor отличается тем, что это дескрипЧАСТЬ!. ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

тор курсора, который изображается, когда указатель находится в клиентской области окна. Для получения дескриптора курсора используется функция LoadCursorQ, которая так
же, как и LoadlconQ, загружает изображение курсора из ресурсов приложения (о ресурсах
речь пойдет несколько позже).
winclass.hCursor - LoadCursor(NULU IDC_ARROW);

Если вас интересуют другие возможные стили курсора, обратитесь к справочной системе по Win32 API.
Очередное поле структуры — hbrBackground. Когда окно выводится на экран или перерисовывается, Windows, как минимум, перерисовывает фон клиентской области окна с
использованием предопределенного цвета или, в терминах Windows, кисти (brush). Следовательно, hbrBackground — это дескриптор кисти, используемой для обновления окна.
Кисти, перья, цвета — все эти части GDI (Graphics Device Interface — интерфейс графического устройства) более подробно рассматриваются в следующей главе. Сейчас же я
просто покажу, каким образом можно запросить базовую системную кисть для закраски
окна. Это выполняется посредством функции GetStockObjectQ, как показано в следующей
строке типа (обратите внимание на приведение возвращаемого типа к HBRUSH).
winclass.hbrBackground - (HBRUSH}GetStockObject(WHITE_BRU5H);

Функция GetStockObjectQ выдает дескриптор объекта из семейства кистей, перьев,
палитр или шрифтов Windows. Она получает один параметр, указывающий, какой
именно ресурс следует загрузить. В табл. 2.6 приведен список возможных объектов
(только кисти и перья).
Таблица 2.6. Идентификаторы объектов GetStockObjectQ
Значение

Описание

BLACK.BRUSH

Черная кисть

WHITE^BRUSH

Белая кисть

GRAY^BRUSH

Серая кисть

LTGRAY_BRUSH

Светло-серая кисть

DKGRAY_BRUSH

Темно-серая кисть

HOLLOW_BRUSH

Пустая кисть

NULL_BRUSH

Нулевая кисть

BLACK_PEN

Черное перо

WHITEJ>EN

Белое перо

NULL_PEN

Нулевое перо

В большинстве случаев фоновая кисть окна не имеет значения, поскольку ответственность за вывод на экран берет на себя DirectX.
Следующее поле в структуре WNDCLASS — это поле меню IpszMenuName. Это ASCII-строка
имени ресурса меню, заканчивающаяся нулем. Меню загружается и прикрепляется к окну.
Поскольку мы не собираемся работать с меню, просто присвоим параметру нулевое значение.
winclass.IpszMenuName = NULL; // Имя присваиваемого меню

Как уже упоминалось, каждый класс Windows представляет отдельный тип окна, которое
может создать ваше приложение. Windows требуется каким-то образом различать разные классы окон, для чего и служит поле IpszClassName. Это завершающаяся нулевым символом строка
ГЛАВА 2. КРАТКИЙ КУРС WINDOWS и DIRECTX

91

с текстовым идентификатором данного класса. Лично я предпочитаю использовать в качестве
имен строки типа "WINCLASS1", "WINCLASS2" и т.д. Впрочем, это дело вкуса,
windass.lpszClassNarne = "WINCLASS1";

После данного присвоения вы сможете обращаться к новому классу Windows по его имени.
Последним по порядку, но не по значимости идет поле hlconSm — дескриптор малой
пиктограммы. Это поле добавлено в структуру WNDCLASSEX и в старой структуре WNDCLASS
отсутствовало. Оно представляет собой дескриптор пиктограммы, которая выводится в
полосе заголовка вашего окна и на панели задач Windows. Обычно здесь загружается
пользовательская пиктограмма, но сейчас мы просто воспользуемся одной из стандартных пиктограмм Windows с помощью функции LoadlconQ.
wincUss.hlconSm - LoadIcon(NULL, IDI_APPLICATION);
Вот и все. Итак, вот как выглядит определение класса полностью.
WNDCLASSEX wincLass;
winclass.cbSize = sizeof(WNDCLASSEX);
windass.style = CS_VREDRAW | CS.HREDRAW |
CS_OWNDC | CS_DBLCKUCKS;
winclass.LpfnWndProc: = WinProc;
windass.cbClsExtra = 0;
win class. cbWnd Extra •=•• 0;
winclass.hlnstance = Hnstanse;
windass.hlcon
= LoadIcon(NULU IDI_APPLICATION);
windass.hCursor
- LoadCursor(NULL, IDC_ARROW);
windass.hbrBackground - (HBRUSH)GetStockObject(WHITE_BRUSH);
winclass.LpszMenuName = NULL;
windass.lpszCtassName = "WINCLASSl";
windass.hlconSm
= LoadlconfNULL, IDI_APPLICATION);

Конечно, для экономии ввода можно поступить и иначе, используя инициализацию
структуры при ее объявлении.
WNDCLASSEX windass-{
sizeof(WNDCLASSEX),
CS_VREDRAW | CSJJREDRAW | CS_OWNDC | CS^DBLCKLICKS,
WinProc,
0,
0,

hinstanse,
LoadIcon{NULU IDI_APPLICATION),
LoadCursor(NULU IDC. ARROW),
(HBRUSH)GetStockObject(WHITE_BRUSH),
NULL,
"WINCLASSl",
Loadkon(NULL IDI_APPLICATION}};

Регистрация класса Windows
Теперь, когда класс Windows определен и сохранен в переменной wincLass, его следует
зарегистрировать. Для этого вызывается функция RegisterClassEx(), которой передается
указатель на определение нового класса.
RegisterClassEx(&winclass);

92

ЧАСТЬ!. ВВВДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Я думаю, очевидно, что здесь мы используем не имя класса, а структуру, описывающую его свойства, — ведь до этого вызова Windows не подозревает о существовании нового класса вообще.
Для полноты изложения упомяну также устаревшую функцию RegistertlassQ, которой
передается указатель на структуру WNDCLAS5.
После того как класс зарегистрирован, можно создавать соответствующие окна. Выясним, как это делается, а затем детально рассмотрим обработчик событий, главный
цикл событий и узнаем, какого рода обработку событий следует обеспечить для работоспособности приложения Windows.

Создание окна
Для создания окна (или любого другого "окнообразного" объекта) используется функция
CreateWindowQ или CreateWindowExQ. Последняя функция более новая и поддерживает дополнительные параметры стиля, так что будем использовать именно ее. При создании окна требуется указать текстовое имя класса данного окна (в нашем случае это "WNDCLASS1").
Вот как выглядит прототип функции Create Win do wEx().
HWNDCreateWindowEx(
DWORD dwExStyle, // Дополнительный стиль окна
LPCTSTR LpCtassName, // Указатель на имя
//зарегистрированного класса
LPCTSTR IpWindowName,//Указатель на имя окна
DWORD dwStyLe,
// Стиль окна
int х,
// Горизонтальная позиция окна
inty,
// Вертикальная позиция окна
int nWidth,
// Ширина окна
int nHeight
// Высота окна
HWND hWndParent // Дескриптор родительского окна
HWND hMenu,
// Дескриптор меню или
// идентификатор дочернего окна
HINSTANCE hlnstance, // Дескриптор экземпляра приложения
LPVOID IpParam); // Указатель на данные создания окна
Если функция выполнена успешно, она возвращает дескриптор вновь созданного окна; в противном случае возвращается значение NULL.
Большинство параметров очевидны; тем не менее вкратце рассмотрим все параметры
функции.


dwExStyle. Флаг дополнительных стилей окна; в большинстве случаев просто равен
NULL. Если вас интересует, какие именно значения может принимать данный параметр, обратитесь к справочной системе Win32 SDK. Единственный из этих параметров, время от времени использующийся мною, — WS_EX_JOPMOST, который
"заставляет" окно находиться поверх других.
• IpClassName. Имя класса, на основе которого создается окно.
• IpWindowName. Завершающаяся нулевым символом строка с заголовком окна, например "Мое первое окно".
• dwStyLe. Флаги, описывающие, как должно выглядеть и вести себя создаваемое окно. Этот параметр очень важен! В табл. 2.7 приведены некоторые наиболее часто
используемые флаги (которые, как обычно, можно объединять с помощью операции побитового ИЛИ).

ГЛАВА 2. КРАТКИЙ КУРС WINDOWS и DIRECTX

93



х, у. Позиция верхнего левого угла окна в пикселях. Если положение окна при создании не принципиально, воспользуйтесь значением CW_USEDEFAULT, и Windows
разместит окно самостоятельно.



nWidth, nHeight. Ширина и высота окна в пикселях. Если размер окна при создании
не принципиален, воспользуйтесь значением CW_USEDEFAULT, и Windows выберет
размеры окна самостоятельно.



hWndParent, Дескриптор родительского окна, если таковое имеется. Значение NULL
указывает, что у создаваемого окна нет родительского окна и в качестве такового
должна считаться поверхность рабочего стола.

• НМегш. Дескриптор меню, присоединенного к окну. О нем речь идет в следующей
главе, а пока будем использовать значение NULL


hlnstance. Экземпляр приложения. Здесь используется значение параметра hinstance функции WinMainQ.

• LpParam. Пока что мы просто устанавливаем это значение равным NULL.
Таблица 2.7. Значения стилей, использующиеся в параметре dwStyle
Значение

Описание

WS_POPUP

Всплываюшее окно

WS_OVERLAPPED

Перекрывающееся окно с полосой заголовка и рамкой. То же, что и
стиль WS.TILED

WSJ)VERLAPPEDWMDOW Перекрывающееся окно со стилями WS_OVERLAPPED, WS_CAPTION,
WS_SYSMENU, WS.THICKFRAME, WS_MINIMIZEDBOX и W$_MAXIMIZEDBOX
WS_VISIBLE
WS_SVSMENU
WS_BORDER
WS_CAPTION

Изначально видимое окно
Окно с меню в полосе заголовка. Требует также установки стиля
WSJAPTION
Окно в тонкой рамке
Окно с полосой заголовка (включает стиль WS_BORDER)

WS_ICONIC

Изначально минимизированное окно. То же, что и стиль WS_MINIMIZE

WS„MAXIMIZE
WS_MAXIMIZEBOX

Изначально максимизированное окно
Окно с кнопкой Maximize. He может быть скомбинирован со стилем WS_EX_CONTEXTHELP; кроме того, требуется указание стиля
WS_SYSMENU

WS_MINIMIZE

Изначально минимизированное окно. То же, что и стиль WS_ICONIC

WS_MINIMIZEBOX

Окно с кнопкой Minimize. He может быть скомбинирован со стилем
W5_EX_CONTEXTHELP; кроме того, требуется указание стиля WS_SYSMENU

WS_POPUPWINDOW

Всплываюшее окно со стилями WS_BORDER, WS_POPUP и WS_SYSMENU.
Дтя того чтобы меню окна было видимо, требуется комбинация стилей
WS_CAPTION MWS_POPUPWINDOW

WS_$rZEBOX

Окно, размер которого можно изменять перетягиванием рамки. То
же, что и WSJTHICKFRAME

WS_HSCROLL

Окно имеет горизонтальную полосу прокрутки

WS_VSCROLL

Окно имеет вертикальную полосу прокрутки

Примечание. Наиболее часто употребляющиеся значения выделены полужирным шрифтом.

94

ЧАСТЬ1. ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Вот как создается обычное перекрывающееся окно со стандартными управляющими
элементами в позиции (0,0) размером 400x400 пикселей.
HWND hwnd; // Дескриптор окна
if (!(hwnd - CreateWindowEx( NULL,
"WINCLASS1",
"Your Basic Window",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
0,0,400,400,
NULL NULL,
hinstance,NULL)))
return (0);

После того как окно создано, оно может быть как видимым, так и невидимым. В нашем случае мы добавили флаг WS_VISIBLE, который делает окно видимым автоматически.
Если этот флаг не добавлен, требуется явным образом вывести окно на экран.
// Вывод окна на экран
ShowWindow(riwnd, ncmdshow);

Помните параметр ncmdshow функции WinMain()? Вот где он пригодился. Хотя в нашем случае просто использовался флаг WS_VISIBLE, обычно этот параметр передается
функции ShowWindowQ. Следующее, что вы можете захотеть сделать, — это заставить
Windows обновить содержимое окна и сгенерировать сообщение WM^PAINT. Все это делается вызовом функции UpdateWindowQ, которая не получает никаких параметров.
// Этот код посылает сообщение WM_PAINT окну
//и обеспечивает обновление его содержимого
UpdateWindowQ;

Обработчик событий
Для того чтобы освежить память и вспомнить о том, для чего нужен обработчик событий и что такое функция обратного вызова, вернитесь к рис. 2.3.
Обработчик событий создается вами и обрабатывает столько событий, сколько вы сочтете необходимым. Всеми остальными событиями, остающимися необработанными,
будет заниматься Windows. Разумеется, чем больше событий и сообщений обрабатывает
ваша программа, тем выше ее функциональность.
Прежде чем приступить к написанию кода, обсудим детали обработчика событий
и выясним, что он собой представляет и как работает. Для каждого создаваемого класса
Windows вы можете определить свой собственный обработчик событий, на который
в дальнейшем я буду ссылаться как на процедуру Windows или просто Win Proc. В процессе
работы пользователя (и самой операционной системы Windows) для вашего окна, как и
для окон других приложений, генерируется масса событий и сообщений. Все эти сообщения попадают в очередь, причем сообщения для вашего окна попадают в очередь сообщений вашего окна. Главный цикл обработки сообщений изымает их из очереди и печедает WinProc вашего окна для обработки.
Существуют сотни возможных сообщений, так что обработать их все мы просто не
'остоянии. К счастью, для того, чтобы получить работоспособное приложение, мы мом обойтись только небольшой их частью.
Итак, главный цикл обработки событий передает сообщения и события WinProc, коая выполняете ними некоторые действия. Следовательно, мы должны побеспокоитье только о WinProc, но и о главном цикле обработки событий.
2. КРАТКИЙ КУРС WINDOWS и DIRECTX

95

Рассмотрим теперь прототип WinProc.
LRESULT CALLBACK WindowProcf
HWND hwnd, //Дескриптор окна или отправитель
UINT msg,
// Идентификатор сообщения
WPARAM wparam, //Дополнительная информация о сообщении
LPARAM Iparam);//Дополнительная информация о сообщении

Разумеется, это просто прототип функции обратного вызова, которую вы можете назвать как угодно, лишь бы ее адрес был присвоен полю win class. LpfnWndProc,
winclass.lpfnWndProc = WindowProc;

А теперь познакомимся с параметрами этой функции.
• hwnd. Дескриптор окна. Этот параметр важен в том случае, когда открыто несколько окон одного и того же класса. Тогда hwnd позволяет определить, от какого
именно окна поступило сообщение (рис. 2.4).


msg. Идентификатор сообщения, которое должно быть обработано WinProc.

• wparam и Lparam. Эти величины являются параметрами обрабатываемого сообщения, несущими дополнительную информацию о нем.
И никогда не забывайте о спецификации CALLBACK при объявлении данной функции!
Единое приложение
hwnd г

hwnd 1

hwnd 3
Сообщения

Сообщения

Сообщения

Окно 1

Окно 2

Окно 3

"Winclass!"

"Winclassl"

"Winclass 1"

j

WinProeO

11
класса "Winclass 1
В случае одного и того же класса
все сообщения обрабатываются
одним и тем же обработчиком событий

Рис. 2,4. Несколько оком одного и того же класса
Большинство программистов при написании обработчика событий используют конструкцию switch(msg), в которой на основании значения msg принимается решение об
использовании дополнительных параметров wparam и/или Iparam. В табл. 2,8 приведены
некоторые из возможных идентификаторов сообщений.
Таблица 2.8. Краткий список идентификаторов сообщений
Значение

Описание

WM_ACTIVATE

Посылается, когда окно активизируется или получает фокус ввода

WM_CLOSE

Посылается, когда окно закрывается

WM CREATE

Посылается при создании окна

96

ЧАСТЬ I. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ TPEXMEPI-

Окончание табл. 2.8
Значение
WM__DESTROY

Описание
Посылается, когда окно должно быть уничтожено

WM_MOVE

Посылается при перемещении окна

WM_,MOUSEMOVE

Посылается при перемещении мыши

WM_KEYUP

Посылается при отпускании клавиши

WM_KEYDOWN

Посылается при нажатии клавиши

WMJFIMER

Посылается при наступлении события таймера

WM_USER

Позволяет вам посылать собственные сообщения

WM_PAINT

Посылается при необходимости перерисовки окна

WM_QUIT

Посылается при завершении работы приложения

WM_SIZE

Посылается при изменении размеров окна

Внимательно посмотрите на приведенные в таблице идентификаторы сообщений. При
работе вам придется иметь дело в основном с этими сообщениями. Идентификатор ссюбщения передается в WinProc в виде параметра msg, а вся сопутствующая информация — как
параметры wparam и Iparam. Какая именно информация передается в этих параметрах для
каждого конкретного сообщения, можно узнать из справочной системы Win32 SDK.
При разработке игр нас в первую очередь интересуют три типа сообщений.
• WM_CREATE. Это сообщение посылается при создании окна и позволяет нам выполнить все необходимые действия по инициализации, захвату ресурсов и т.п.
• WM_PAINT. Это сообщение посылается, когда требуется перерисовка окна. Это может произойти по целому ряду причин: окно было перемещено, его размер был изменен, окно другого приложения перекрыло наше окно и т.п.
• WM_DESTROY. Это сообщение посылается нашему окну перед тем, как оно должно
быть уничтожено. Обычно это сообщение — результат щелчка на пиктограмме закрытия окна или выбор соответствующего пункта системного меню. В любом случае по получении этого сообщения следует освободить все захваченные ресурсы и
сообщить Windows о необходимости закрыть приложение, послав сообщение
WM_QUIT (мы встретимся с этим немного позже в данной главе).
Вот простейшая функция WinProc, обрабатывающая описанные сообщения.
LRESULT CALLBACK WindowProc(HWND hwnd,
UINT msg,
WPARAM wparam,
LPARAM Lparam)
PAINTSTRUCT ps;
// Используется в WM_PAINT
HOC
hdc; // Дескриптор контекста устройства
// Какое сообщение получено?
switch (msg)
{
case WM_CREAYE:
{
// Выполнение инициализации

ГЛАВА?. КРАТКИЙ KypcWiNDOWSH DIRECTX

97

return(O); // Успешное выполнение
} break;
case WM_ PAINT:
i
// Обновляем окно
hdc •= BeginPaint(hwnd,&ps);
// Здесь выполняется перерисовка окна
EndPaint(hwnd,&ps);
return(O); // Успешное выполнение
} break;
caseWM_DESTROY:

i

// Завершение приложения
PostQuitMessage(O);
return(O); //Успешное выполнение
} break;
i
default: break;
} // switch
// Обработка прочих сообщений
return(DefWindowProc(nwnd, msgr wparam, tparam)};
} // WindowProc

Как видите, обработка сообщений в основном сводится к отсутствию таковой :-).
Начнем с сообщения WM_CREATE. Здесь наша функция просто возвращает нулевое значение, говорящее Windows, что сообщение успешно обработано и никаких других действий
предпринимать не надо. Конечно, здесь можно выполнить все действия по инициализации, однако в настоящее время у нас нет такой необходимости.
Сообщение WM_PAINT очень важное. Оно посылается при необходимости перерисовки окна. В случае игр DirectX это не так важно, поскольку мы перерисовываем весь экран
со скоростью от 30 до 60 раз в секунду, но для обычных приложений Windows это может
иметь большое значение. Более детально WM_PAINT рассматривается в следующей главе, а
пока просто сообщим Windows, что мы уже перерисовали окно, так что посылать сообщение WM_PA1NT больше не требуется.
Для этого необходимо объявить действительной клиентскую область окна. Возможно
несколько путей решения этой задачи, и простейший из них — использовать пару вызовов BeginPaint() ~ EndPaint(). Эта пара вызовов делает окно действительным и заливает
фон с помощью выбранной в определении класса кисти.
hdc = BeginPaint(hwnd,&ps);
//Здесь выполняется перерисовка окна
EndPaint{hwnd,&ps);

Здесь я хотел бы кое-что подчеркнуть. Заметьте, что первым параметром в каждом
вызове является дескриптор окна h w n d . Это необходимо, поскольку функции Begin PaintQ
и EndPaintQ потенциально способны выводить изображение в любом окне вашего приложения и дескриптор указывает, в какое именно окно будет перерисовываться. Второй
параметр представляет собой указатель на структуру PAINTSTRUCT, содержащую прямоугольник, который необходимо перерисовать. Эта структура представлена ниже.
98

ЧАСТЬ I. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

typedef struct tagPAINTSTRUCT
{
HOC hdc;

BOOL f Erase;
RECT rcPaint;
BOOLfRestore;
BOOLflncUpdate;
BYTErgbReserved[32];
} PAINTSTRUCT;
Полоса заголовка
Окно

fr-j

PAINTSTRUCT

Эта область стала
недействительной,
возможно, из-за
другого окна

Л е ре рисовываатся
только эта область

{

Клиентская
область

RECT rcPaint

BeginPaint(hwnd, &ps);

Клиентская область
• Обрабатывается WM_PAINT

Рис. 2.5. Перерисовывается только недействительная область
Сейчас, пока не рассматривалась работа с GDI, вам незачем беспокоиться о всех полях этой структуры — вы познакомитесь с ними позже. Здесь же стоит познакомиться
поближе только с полем rcPaint, которое представляет собой структуру RECT, указывающую минимальный прямоугольник, требующий перерисовки. Взгляните на рис. 2.5, поясняющий это. Windows пытается обойтись минимальным количеством действий, поэтому при необходимости перерисовки окна старается вычислить минимальный прямоугольник, перерисовать который было бы достаточно для восстановления содержимого
всего окна. Если вас интересует, что конкретно представляет собой структура RECT, то это
не более чем четыре угла прямоугольника.
typedef struct tagRECT
•i
LONG Left; //Левая сторона прямоугольника
LONG top; // Верхняя сторона прямоугольника
LONG right; // Правая сторона прямоугольника
LONG bottom; // Нижняя сторона прямоугольника
} RECT;
Последнее, что следует сказать о вызове BeginPaintQ: эта функция возвращает дескриптор графического контекста hdc.
HDC hdc;
hdc - BeginPaint(hwnd,&ps);

ГЛАВА 2. КРАТКИЙ КУРС WINDOWS и DIRECTX

99

Графический контекст представляет собой структуру данных, которая описывает видеосистему и изображаемую поверхность. Для непосвященного — это сплошное шаманство,
так что запомните главное: вы должны получить этот контекст, если хотите работать с какой-либо графикой. На этом мы пока завершаем рассмотрение сообщения WM_PAINT.
Сообщение WM_DESTROY достаточно интересно. Оно посылается, когда пользователь
закрывает окно. Однако это действие закрывает только окно, но не завершает само приложение, которое продолжает работать, но уже без окна. В большинстве случаев, когда
пользователь закрывает окно, он намерен завершить работу приложения, так что вам надо заняться этим и послать сообщение о завершении приложения самому себе. Это сообщение WM_QUIT, и в связи с распространенностью его использования имеется даже
специальная функция для отправки его самому себе — PostQuitMessageQ.
При обработке сообщения WM_DESTROY необходимо выполнить все действия по освобождению ресурсов и сообщить Windows, что она может завершать работу вашего приложения, вызвав PostQuitMessage(O). Этот вызов поместит в очередь сообщение WM_QUIT, которое приведет к завершению работы главного цикла событий.
При анализе WinProc вы должны знать некоторые детали. Во-первых, я уверен, что вы
уже обратили внимание на операторы return (0) после обработки каждого сообщения.
Этот оператор служит для двух целей: выйти из WinProc и сообщить Windows, что вы обработали сообщение. Вторая важная деталь состоит в использовании обработчика сообщений по умолчанию DefaultWindowProcQ. Эта функция передает необработанные вами сообщения Windows для обработки по умолчанию. Таким образом, если вы не обрабатываете какие-то сообщения, то всегда завершайте ваш обработчик событий вызовом
return(DefWindowProc(hwnd, msg, wparam, Iparam));

Я знаю, что все; это может показаться сложным. Тем не менее, все очень просто: достаточно иметь основу кода приложения Windows и просто добавлять к ней собственный
код. Моя главная цель, как я уже говорил, состоит в том, чтобы помочь вам в создании
"ОО832-подобных" игр, где вы можете практически забыть о существовании Windows.
В любом случае, нам необходимо рассмотреть еще одну важную часть Windowsприложения — главный цикл событий.

Главный цикл событий
Все, сложности закончились, так как главный цикл событий очень прост.
while(GetMessage(&msg,NULLAO))
I
// Преобразование клавиатурного ввода
TranslateMessage(&msg);

г

II Пересылка сообщения WinProc
DispatchMessage(&msg);

}

Вот и все. Цикл wnHe() выполняется до тех пор, пока GetMessageQ возвращает ненулевое значение. Эта функция и есть главная рабочая лошадь цикла, единственная цель которого состоит в извлечении очередного сообщения из очереди событий и его обработке.
Обратите внимание на то, что функция GetMessageQ имеет четыре параметра. Для нас важен только первый параметр; остальные имеют нулевые значения. Ниже представлен
прототип функции GetMessageQ.
BOOL GetMessagef
LPMSG IpMsg,
100

// Адрес структуры с сообщением
ЧАСТЫ. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

HWND hWnd,
//Дескрипторокна
DINT wMsgFUterMin, // Первое сообщение
DINT wMsgFHterMax); // Последнее сообщение
Параметр msg представляет собой переменную для хранения полученного сообщения.
Однако^в отличие от параметра msg в WinProcQ, этот параметр представляет собой сложную структуру данных.
typedef struct tagMSG
I

HWND hwnd; // Окно сообщения
UINT message; // Идентификатор сообщения
WPARAM wParam; // Дополнительный параметр сообщения
LPARAM LParam; //Дополнительный параметр сообщения
DWORD time; // Время события сообщения
POINT pt;
// Положение указателя мыши

} MSG;

Итак, как видите, все параметры функции WinProcQ содержатся в этой структуре данных наряду с другой информацией, такой как время или положение указателя мыши.
Итак, функция GetMessage() получает очередное сообщение из очереди событий. А что затем? Следующей вызывается функция TranslateMessage(), которая представляет собой транслятор виртуальных "быстрых клавиш". Ваше дело — вызвать эту функцию, остальное — не ваша
забота. Последняя вызываемая функция — DispatchMessage(), которая, собственно, и выполняет всю работу по обработке сообщений. После того как сообщение получено с помощью
функции GetMessage() и преобразовано функцией TranslateMessageQ, функция DispatchMessiageQ
вызывает для обработки функцию WinProcQ, передавая ей всю необходимую информацию, которая находится в структуре MSG. На рис. 2.6 показан весь описанный процесс.

Рис. 2.6. Механизм обработки сообщений
Вот и все; если вы разобрались в этом, можете считать себя программистом в
Windows. Остальное — не более чем небольшие детали. Посмотрите с этой точки зрения
ГЛАВА 2. КРАТКИЙ КУРС WINDOWS и DIRECTX

101

на приведенный ниже листинг, где представлена завершенная Windows-программа, которая просто создает одно окно и ждет, пока вы его не закроете.
// DEM02_3.CPP - Базовая Windows-программа
// Включаемые файлы //////////////////////////////У////////
^define WIN32_LEAN_AND_MEAN // He MFC :-)
tfincLude
^include
#include
#ind.ude
//

Определения

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

«define WINDOW_CLASS_NAME "WINCLASSl"
// Глобальные переменные ///////У//////////////////////////

// Функции//////////////////У/////////////////////////////
LRESULT CALLBACK W/indowProcfHWND hwnd,
UINT msg,
WPAR.AM wparam,
LPARAM tparam)
I
// Главный обработчик сообщений в системе
PAINTSTRUCT ps; // Используется в WMJ>AINT
HOC
hdc; // Дескриптор контекста устройства
// Какое сообщение получено
switch(msg)
i
case WM_CREATE:
{
//Действия по инициализации
return(O); //Успешное завершение
} break;
case WM_PAINT:
{
// Объявляем окно действительным
hdc= BeginPaint(hwnd,&ps);
//Здесь выполняются все действия
// по выводу изображения
EndPaint(hwnd,&ps);
return(O); // Успешное завершение
} break;
case WM_DESTROV:
{
// Завершение работы приложения
PostQuitMessage(O);
// Успешное .завершение
return(O);

102

ЧАСТЬ!. ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХИГР

} break;
default:break;
} // switch
// Обработка остальных сообщений
return (DefWindowProc(hwnd, msg, wparam, Lparam));
}//WinProc
// WinMain ////////////////////////////У///////////////////
intWINAPIWinMain( HINSTANC£ hinstance,
HINSTANCEhprevinstance,
LPSTR Ipcmdline,
int
ncmdshow)

i

WNDCLASSEX winclass; // Класс создаваемого сообщения
HWND
hwnd; // Дескриптор окна
MSG
msg; // Структура сообщения

// Заполнение структуры класса
winclass.cbSize
= sizeof(WNDCLASSEX);
winclass.style
- CS_DBLCLKS | CS_OWNDC |
CS_HREDRAW j CS_VREDRAW;
winclass.LpfnWndProc = WindowProc; •
winclass.cbCLsExtra = 0;
winclass.cbWndExtra = 0;
winclass. hlnstance = hinstance;
winclass.hlcon
LoadIcon(NULU MISAPPLICATION);
winclass.hCursor
- LoadCursor(NULU IDC_ARROW);
winclass.hbrBackground (HBRUSH)GetStockObject(BLACK_BRUSH);
winclass.IpszMenuName -NULL;
winclass.IpszClassName - WINDOW_CLASS_NAME;
winclass.hlconSm
LoadlconfNULL, MISAPPLICATION);
// Регистрация класса
if (!RegisterClassEx(&winclass))
return (0);
// Создание окна
if (!(hwndCreateWindowEx(NULU
WINDOW_CLASS_NAME,
"Your Basic Window",
WS_OVERLAPP EDWIN DOW | WS^VISIBLE,
0,0,
400,400,
NULL,
NULL,
hinstance,
NULL}))
return(0);

ГЛАВА2. КРАТКИЙ КУРС WINDOWS и DIRECTX

103

// Главный цикл событий
while(GetMessage(&msg,NULLAO))
{
Tra n s late Message (&msg);
DispatchMessage(&msg);
}//while
// Возврат в Windows
return(msg.wParam);
}//WinMain
illiilllillllHIillilillilllllllllillillilliiiilUlllilHIl
Для компиляции программы создайте проект Win32 .EXE и добавьте к нему
DEM02_3.CPP (вы можете также просто запустить скомпилированную программу
DEM02_3.EXE, находящуюся на прилагаемом компакт-диске). На рис. 2.7 показана работа данной программы.

Рис. 2.7. Копия экрана приложения DEMQH2__3.EXE
Прежде чем идти дальше, хотелось бы осветить ряд вопросов. Начнем с того, что если
вы внимательнее посмотрите на цикл событий, то увидите, что он не предназначен для
работы в реальном времени; ведь пока программа ожидает получения сообщения посредством функции GetMessage(}, ее выполнение заблокировано.

Создание цикла событий реального времени
Для решения этой проблемы нам нужен способ узнать, имеется ли какое-либо сообщение в очереди или она пуста. Если сообщение есть, мы обрабатываем его; если нет —
продолжаем выполнение игры. Windows предоставляет необходимую нам функцию PeekMessage(). Ее прототип практически идентичен прототипу функции GetMessageQ.
BOOLPeekMessage(

LPMSG IpMsg,
// Адрес структуры ссообщением
HWND hWnd,
//Дескриптор окна
UINT wMsgFHterMin, // Первое сообщение
104

ЧАСТЬ!. ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХИГР

UINT wMsgFitterMax, // Последнее сообщение
DINT wRemovwMsg); //Флаг удаления сообщения

Данная функция возвращает ненулевое значение, если в очереди имеется сообщение.
Отличие прототипов функций заключается в последнем параметре, который определяет, как именно должно быть выбрано сообщение из очереди. Корректными значениями параметра wRemoveMsg являются следующие:


PM_NOREMOVE. Вызов PeekMessageQ не удаляет сообщение из очереди,

• PM_REMOVE. Вызов PeekMessageQ удаляет сообщение из очереди.
Рассматривая варианты вызова функции PeekMessageQ, мы приходим к двум возможным способам работы: либо вызываем функцию PeekMessage() с параметром PM_NOREMOVE
и, если в очереди имеется сообщение, вызываем функцию GetMessageQ; либо используем
функцию PeekMessageQ с параметром PM_REMOVE, чтобы сразу выбрать сообщение из очереди, если оно там есть. Воспользуемся именно этим способом. Вот измененный основной цикл, отражающий принятую нами методику работы.
whue(TRUE)
[

if (PeekMessage(&msg,NULU)APN_REMOVE))
{

// Проверка сообщения о выходе
if (msg. message == WM_QUTT) break;
TransLateMessage(Smsg);
DispatchMessage(&msg);

// Выполнение игры
Game_Main();

} //while
В приведенном коде наиболее важные моменты выделены полужирным шрифтом.
Рассмотрим первую выделенную часть.
if (msg. message == WM_QUIT) break;

Этот код позволяет определить, когда следует выйти из бесконечного цикла
while(TRUE). Вспомните: когда в WinProc обрабатывается сообщение WM_DESTROY, мы посылаем сами себе сообщение WMJ3UIT посредством вызова функции PostQuitMesbageQ.
Это сообщение знаменует завершение работы программы, и, получив его, мы выходим из
основного цикла.
Вторая выделенная полужирным шрифтом часть кода указывает, где следует разместить вызов главного цикла игры. Но не забывайте о том, что возврат из вызова
Game_Main() — или как вы его там назовете — должен осуществляться немедленно по выводу очередного кадра анимации или одного просчета логики игры. В противном случае
обработка сообщений в главном цикле прекратится.
Примером использования нового подхода к решению проблемы цикла реального
времени может служить программа DEM02_4.CPP, которую вы можете найти на прилагаемом компакт-диске. Именно эта структура программы и будет использоваться в оставшейся части книги.

ГЛАВА 2. КРАТКИЙ КУРС WINDOWS и DIRECTX

105

Краткий курс DirectX и СОМ
DirectX может потребовать большего количества работы и вмешательства со стороны программиста, но дело стоит того. DirectX представляет собой программное
обеспечение, которое позволяет абстрагировать видеоинформацию, звук, входящую
информацию, работу в сети и многое другое таким образом, что аппаратная конфигурация компьютера перестает иметь значение и для любого аппаратного обеспечения используется один и тот же программный код. Кроме того, технология DirectX
более высокоскоростная и надежная, чем GDI или MCI (Media Control Interface —
интерфейс управления средой), являющиеся "родными" технологиями Windows,
На рис. 2.8 показаны схемы разработки игры для Windows с использованием DirectX и без нее. Обратите внимание, насколько ясным и элегантным решением является DirectX.

быстро/просто

Обычная игра GDI/MCI
медленно/мало
быстро
возможностей

\Л/т32-игра с DirectX
быстро

Рис. 2.8. DirectX против GDI/MCI

Так как же работает DirectX? Эта технология предоставляет возможность управления всеми устройствами практически на аппаратном уровне. Это возможно благодаря технологии COM (Component Object Model — модель составных объектов) и
множеству драйверов и библиотек, написанных как Microsoft, так и производителями оборудования. Microsoft продумала и сформулировала ряд соглашений: функции,
переменные, структуры данных и т.д. — словом, все то, чем должны пользоваться
производители оборудования при разработке драйвера для управления производимым ими устройством.
Если эти соглашения соблюдаются, вам не следует беспокоиться об особенностях
того или иного устройства. Достаточно просто обратиться к DirectX, а уж она сама
обработает и учтет эти особенности за вас. Если у вас есть поддержка DirectX, то совершенно не важно, какая у вас звуковая или видеокарта либо другие устройства.
Любая программа может обращаться к тому или иному устройству, даже не имея никакого представления о нем.
В настоящий момент DirectX состоит из ряда компонентов. Их список приводится
ниже и показан на рис. 2.9.

106

ЧАСТЫ. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Приложение Win32
Подсистема DirectX 8.0

Объединены
в DirectX Graphics

Объединены
в DirectX Audio

HEL: уровень эмуляции аппаратного обеспечения

Программная
эмуляция

HAL: уровень абстракции аппаратного обеспечения

Аппаратное обеспечение: звуковое, видео, ввод данных, память...

Рис. 2.9. Архитектура DirectX и отношения с Win32


DirectDraw

• DirectSound
• DirectSoundSD
• DirectMusic
• Directlnput


Direct Play

• DirectSetup
• Direct3DRM
• Direct3DIM
В DirectX 8.0 DirectDraw был слит с DirectSD и переименован в DirectGraphics,
a DirectSound был слит с DirectMusic и переименован в DtrectAudio. Таким образом,
в DirectX 8.0+ больше нет DirectDraw. Однако одно из правил DirectX и СОМ состоит
втом, что вы всегда можете запросить предыдущий интерфейс. Таким образом, в этой
книге мы будем использовать для графики DirectDraw под DirectX 7.Оа, а для звука — новейшие интерфейсы 8.0+. В любом случае мы собираемся собрать графику, звук и средства ввода в нашем виртуальном компьютере, так что будет работать даже DirectX 3.0.

HEL и HAL
На рис. 2.9 можно заметить, что DirectX основывается на двух уровнях, которые называются HEL (Hardware Emulation Layer— уровень эмуляции аппаратного обеспечения)
и HAL (Hardware Abstraction Layer— уровень абстракции аппаратного обеспечения).
ПЛАВАЙ. КРАТКИЙ КУРС WINDOWS и DIRECTX

107

Так как DirectX создан "с дальним прицелом", предполагается, что в будущем аппаратное
обеспечение будет поддерживать дополнительные возможностями, которыми можно будет
пользоваться при работе с DirectX. Ну, а что делать, пока аппаратное обеспечение не поддерживает эти ВОЗМО.ЖНОСТИ? Проблемы этого рода и призваны решать HEL и HAL.
* HAL находится ближе к "железу" и общается с устройствами напрямую. Обычно
'• это драйвер устройства, написанный производителем, и с помощью обычных запросов DirectX вы обращаетесь непосредственно к нему. Отсюда вывод: HAL используется тогда, когда вам надо обратиться к функции, поддерживаемой самим
устройством (что существенно ускоряет работу). Например, если вы хотите загрузить растровый рисунок на экран, то аппаратно это будет сделано гораздо быстрее,
чем при использовании программного цикла.


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

Вы можете решить, что в HEL очень много уровней программного обеспечения. Это
так, но это не должно вас волновать: DirectX — настолько ясная технология, что единственное неудобство ддя программиста заключается лишь в вызове одной-двух лишних
функций. Это не слишком большая плата за ускорение 2D/3D графики, работы в сети и
обработки звука. DirectX — это попытка Microsoft и производителей оборудования помочь вам полностью использовать все аппаратные возможности.

Подробнее о базовых классах DirectX
Теперь вкратце познакомимся с компонентами DirectX и узнаем, чем занимается каждый из них.
DirectDraw. Основной компонент, отвечающий за вывод двумерных изображений
и управляющий дисплеем. Это тот канал, по которому проходит вся графика и который,
пожалуй, является самым важным среди всех компонентов DirectX. Объект DirectDraw
в большей или меньшей степени представляет видеокарту вашей системы. Однако в DirectX 8.0 он более не доступен, так что для этой цели следует пользоваться интерфейсами
DirectX 7.O.
DirectSound. Компонент DirectX, отвечающий за звук. Он поддерживает только цифровой звук, но не MIDI. Однако использование этого компонента значительно упрощает
жизнь, так как теперь вам не нужна лицензия на использование звуковых систем сторонних производителей. Программирование звука — это настоящая черная магия, и на рынке звуковых библиотек ряд производителей загнали в угол всех остальных, выпустив
Miles Sound System и DiamondWare Sound Toolkit. Это были очень удачные системы, позволявшие легко загружать и проигрывать и цифровой звук, и MIDI как под DOS, так и
из Win32-про грамм. Однако благодаря DirectSound, DirectSound3D и новейшему компоненту DirectMusic библиотеки сторонних разработчиков используются все реже.
DirectSound3D. Компонент DirectSound, отвечающий за ЗО-звук. Он позволяет позиционировать звук в пространстве таким образом, чтобы создавалось впечатление движения объектов. Это достаточно новая, но быстро совершенствующаяся технология.

108

ЧАСТЫ. ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

На сегодня звуковые платы поддерживают ЗО-эффекты на аппаратном уровне, включая
такие эффекты, как эффект Доплера, преломление, отражение и др. Однако при использовании программной эмулядии от всех этих возможностей остается едва ли половина.
DirectMusic. Самое последнее новшество в DirectX. Поддерживает технологию MIDI,
незаслуженно забытую ранее. Кроме того, в DirectX есть новая система под названием
DLS (Downloadable Sounds System— система подгружаемых звуков), которая позволяет
создавать цифровое представление музыкальных инструментов, а затем использовать его
в MIDI-контроллере. Это во многом подобно синтезатору Wave Table, но только работающему на программном уровне. DirectMusic позволяет также в реальном времени изменять параметры звука, пользуясь вашими шаблонами. По существу, эта система в состоянии создавать новую музыку "на лету".
Directlnput. Система, которая обрабатывает информацию, поступающую со всех устройств ввода, включая мышь, клавиатуру, джойстик, пульт ручного управления, манипулятор-шар и т.д. Кроме того, в настоящее время Directlnput поддерживает электромеханические приводы и датчики, определяющие силу давления, что дает возможность задавать механическую силу, которую пользователь ощущает физически. Эти возможности
могут буквально взорвать индустрию киберсекса!:-)
DirectPlay. Часть DirectX, работающая с сетью. Использование DirectPlay позволяет
абстрагировать сетевые подключения, использующие Internet, модемы, непосредственное соединение компьютер или любые другие типы соединений, которые могут когдалибо появиться. DirectPlay позволяет работать с подключениями любого типа, даже не
имея ни малейшего представления о работе в сети. Вам больше не придется писать драйверы, использовать сокеты или что-либо в этом роде. Кроме того, DirectPlay поддерживает концепции сессии, т.е. самого процесса игры, и того места в сети, где игроки собираются для игры. DirectPlay не требует от вас знания многопользовательской архитектуры.
Отправка и получение пакетов для вас — вот все, что он делает. Содержимое и достоверность этих пакетов — ваше дело.
Direct3DRM (DirectX3D Retained Mode — режим поддержки DirectSD). Высокоуровневая ЗО-система, основанная на объектах и фреймах, которую можно использовать для создания базовых ЗО-программ. DirectSDRM использует все достоинства ЗО-ускорителей, хотя и не является самой быстрой системой трехмерной графики в мире.
Direct3DIM (Direct3D Immediate Mode — режим непосредственной поддержки DirectSD).
Представляет собой низкоуровневую поддержку трехмерной графики DirectX. Первонач;ально
с ним было невероятно трудно работать и это было слабым местом в войне DirectX с OpenGL.
Старая версия Immediate Mode использовала так называемые execute buffers (бу^кры выполнения). Эти созданные вами массивы данных и команд, описывающие сцену, которая должна
быть нарисована, — не самая красивая идея. Однако, начиная с DirectX 5.0, интерфейс Immediate Mode больше напоминает интерфейс OpenGL благодаря функции DrawPrimitiveQ. Теперь
вы можете передавать обработчику отдельные детали изображения и изменять состояние при
помощи вызовов функций, а не посредством буферов выполнения. Честно говоря, я полюбил
DirectSD Immediate Mode только после появления в нем указанных возможностей. Однако в
данной книге мы не будем углубляться в детали использования DirectSDIM.
DirectSetup/AutoPlay. Квазикомпоненты DirectX, обеспечивающие инсталляцию DirectX
на машину пользователя непосредственно из вашего приложения и автоматический запуск игры при вставке компакт-диска в дисковод. DirectSetup представляет собой небольшой набор функций, которые загружают файлы DirectX на компьютер пользователя
во время запуска вашей программы и заносят все необходимые записи в системный реестр. AutoPlay — это обычная подсистема для работы с компакт-дисками, которая ищет
в корневом каталоге компакт-диска файл Autoplay.inf. Если этот файл обнаружен, то
AutoPlay запускает команды из этого файла.
ГЛА8А2. KPATKHPlKVPCWlNDOWSHDlRECTX

109

DirectX Graphics,. Компонент, в котором Microsoft решила объединить возможности
DirectDraw и Direct3D, чтобы увеличить производительность и сделать доступными
трехмерные эффекты в двумерной среде. Однако, на мой взгляд, не стоило отказываться
от DirectDraw, и не только потому, что многие программы используют его. Использование DirectSD для получения двумерной графики в большинстве случаев неудобно и громоздко. Во многих программах, которые по своей природе являются двумерными приложениями (такие, как GUI-приложен и я или простейшие игры), использование DirectSD
явно избыточно. Однако не стоит беспокоиться об этом, так как мы будем использовать
интерфейс DirectX 7.0 для работы с DirectDraw.
DirectX Audio. Результат слияния DirectSound и DirectMusic, далеко не такой фатальный, как DirectX Graphics. Хотя данное объединение и более тесное, чем в случае с DirectX Graphics, но при этом из DirectX ничего не было удалено. В DirectX 7.0 DirectMusic
был полностью основан на СОМ и мало что делал самостоятельно, а кроме того, он не
был доступен из DirectSound. Благодаря DirectX Audio ситуация изменилась, и у вас теперь есть возможность работать одновременно и с DirectSound и с DirectMusic.
DirectShow. Компонент для работы с медиапотоками в среде Windows. DirectShow обеспечивает захват и воспроизведение мультимедийных потоков. Он поддерживает широкий
спектр форматов— Advanced Streaming Format (ASF), Motion Picture Experts Group
(MPEG), Audio-Video Interleaved (AVI), MPEG Audio Layer-3 (МРЗ), а также WAV-файлы.
DirectShow поддерживает захват с использованием устройств Windows Driver Model (WDM),
а также более старых устройств Video for Windows. Этот компонент тесно интегрирован
с другими технологиями DirectX. Он автоматически определяет наличие аппаратного ускорения и использует его, но в состоянии работать и с системами, в устройствах которых аппаратное ускорение отсутствует. Это во многом облегчает вашу задачу, так как раньше, чтобы использовать в игре видео, вам приходилось либо использовать библиотеки сторонних
разработчиков, либо самому писать эти библиотеки. Теперь же все это уже реализовано
в DirectShow. Сложность лишь в том, что это довольно сложная система, требующая достаточно много времени, чтобы разобраться в ней и научиться ею пользоваться.
У вас, наверное, возник вопрос: как же разобраться в этих компонентах и версиях DirectX? Ведь все может стать совсем другим за какие-то полгода. Отчасти это так. Бизнес,
которым мы занимаемся, очень рисковый — технологии, связанные с графикой и играми, меняются очень быстро. Однако, так как DirectX основан на технологии СОМ, программы, написанные, скажем, для DirectX 3.0, обязательно будут работать и с DirectX 8.O.
Давайте посмотрим, как это получается.

Краткое введение в СОМ
СОМ была задумана много лет назад как белый лист бумаги на базе новой парадигмы
программирования, основной принцип которой можно сравнить с принципом конструктора:
вы просто соединяете отдельные части и в результате получаете работающее единое целое.
Каждая плата в компьютере (или каждый блок конструктора Lego) точно знает, как она должна функционировать, благодаря чему функционирует и собранный из них компьютер (или собранная из деталей игрушечная машина). Чтобы реализовать такую же технологию в программировании, необходим интерфейс общего назначения, который может представлять собой множество разнообразных функций любого типа. Именно в этом и состоит суть СОМ.
При работе с платами компьютера наиболее приятно то, что, добавляя новую штату,
вы не должны отчитываться об этом перед другими платами. Как вы понимаете, в случае
программного обеспечения ситуация несколько сложнее. Программу придется, по крайней мере, перекомпилировать. И это вторая проблема, решить которую призвана СОМ.
110

ЧАСТЬ!. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХ игр

Нам требуется возможность расширять возможности СОМ-объектов так, чтобы программы, работавшие со старой версией объекта, продолжали успешно работать и с новой
версией. Кроме того, можно изменить СОМ-объекты, не перекомпилируя при этом саму
программу, — а это очень большое преимущество.
Поскольку вы можете заменять старые СОМ-объекты новыми, не перекомпилируя
при этом профамму, вы можете обновлять ваше программное обеспечение на машине
пользователя, не создавая никаких очередных заплат или новых версий профамм. Например, у вас есть программа, которая использует три СОМ-объекта: один отвечает за
графику, один за звук и один за работу в сети (рис. 2.10), А теперь представьте, что вы уже
продали 100000 копий своей программы и хотите избежать отсылки 100000 новых версий. При использовании СОМ для обновления работы с графикой вам достаточно дать
своим пользователям новый СОМ-объект, и старая профамма будет использовать его
вместо старого; при этом не нужно делать решительно ничего: ни перекомпилировать, ни
компоновать приложение. Конечно, реализация такой технологии на уровне профаммирования — задача очень сложная. Чтобы создать собственный СОМ-объект, требуется
приложить массу усилий. Но зато как легко будет потом им пользоваться!
Версия 1.0
Основная
программа
СОМ-объект!
Graphics

Версия 2.0

Чтобы обновить программу,
пользователь может
загрузить новый
СОМ-объект в систему
и использовать его

СОМ-объект|
Sound
COW-объект|
Network

Основная
программа
Старый
СОМ-объект |
Graphics
Новый
СОМ-объект!
Sound
Новый
СОМ-объект!
Network

Загрузка из Internet.
Перекомпиляция не требуется

Рис. 2.10. Представление о СОМ
Следующий вопрос заключается в том, как и в каком виде распространять СОМобъекты с учетом их природы Plug and Play. Четких правил на этот счет не существует,
В большинстве случаев СОМ-объекты представляют собой динамически компонуемые
библиотеки (DLL), которые могут поставляться вместе с профаммой или зафужаться
отдельно. Таким образом, СОМ-объекты могут быть легко обновлены и изменены. Проблема лишь в том, что профамма, использующая СОМ-объект, должна уметь загрузить
его из DLL. Позже мы вернемся к этому вопросу.

Что такое СОМ-объект?
В действительности СОМ-объект представляет собой класс (или набор классов)
на языке C++, который реализует ряд интерфейсов (которые, в свою очередь, являются
наборами функций). Эти интерфейсы используются для связи с СО М-объектами. ВзгляГЛАВА 2. КРАТКИЙ КУРС WINDOWS и DIRECTX

111

ните на рис. 2.11. На нем вы видите простой СОМ-объект с тремя интерфейсами: IGRAPHICS,ISOUNDHlINPUT.
Ввод

Интерфейс 1

Вывод

funcl()
func2()

lUnknown
AddrefQ
Release()
Querylnterface{)

Приложения Win32,
использующие

IGRAPHICS

СОМ-объект
Интерфейс 2
fund ()
func2()

ISO UN О

Все интерфейсы
проиэводны от
Illnknown
Интерфейс З
fund ()
func2{)

IINPUT

Рис. 2.П. Интерфейсы СОМ-обьекта
У каждого из этих трех интерфейсов есть свой набор функций, который вы можете
(если знаете, как) использовать в своей работе. Итак, каждый СОМ-объект может иметь
один или несколько интерфейсов и у вас может быть один или несколько СОМобъектов. В соответствии со спецификацией СОМ, все интерфейсы, созданные вами,
должны быть производными от специального базового класса lUnknown. Для вас, как для
программиста на C++, это означает, что lUnknown — это некая отправная точка, с которой нужно начинать создание интерфейсов.
Давайте взглянем на определение класса l U n k n o w n .
struct lUnknown
|
// Эта функция используется для получения
//указателей на другие интерфейсы
virtual HRESULT_stdcalL Querylnterface(
const IID &iid, (void **)ip) « 0;
// Это функция увеличения счетчика ссылок
virtual ULONG __stdcall AddRefQ - 0;

i;

// Это функция уменьшения счетчика ссылок
virtual ULONG __stdcall ReleaseQ - 0;

112

ЧАСТЬ I. ВВЦДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Обратите внимание на то, что все методы виртуальны и не имеют тела (абстрактны).
К тому же все методы используют соглашение _stdcalL, в отличие от привычных вызовов в C/C++. При использовании этого соглашения аргументы функции вносятся
в стек справа налево.

Определение этого класса выглядит немного причудливо, особенно если вы не привыкли к использованию виртуальных функций. Давайте внимательно проанализируем
ILJnknown. Итак, все интерфейсы, наследуемые от IDnknown, должны иметь, по крайней
мере, следующие методы: Querylnterface{), AddRef(), Release().
Метод Query Interface () — это ключевой метод СОМ. С его помощью вы можете получить указатель на требующийся вам интерфейс. Для этого нужно знать идентификатор
интерфейса, т.е. некоторый уникальный код этого интерфейса. Это число длиной
128 бит, которое вы назначаете вашему интерфейсу. Существует 2™ возможных значений
идентификатора интерфейса, и я гарантирую, что нам не хватит и миллиарда лет, чтобы
использовать их все, даже если на Земле все только и будут делать, что днем и ночью создавать СОМ-объекты! Несколько позже в этой главе, когда будут рассматриваться реальные примеры, мы еще затронем тему идентификаторов интерфейсов.
Кроме того, одно из правил СОМ гласит: если у вас есть интерфейс, то вы можете получить доступ к любому другому интерфейсу, так как они все происходят из одного
СОМ-объекта. В общих чертах это означает: где бы вы ни находились, вы можете попасть
куда захотите (рис. 2.12).
СОМ-объект

Интерфейс А
Query Interface!)

-"--~-1
!'

^ Interface,ptr**
Querylnte

Интерфейс В
Query InterfaceQ
Из любого данного интерфейса
вы можете запросить другой
интерфейс того же СОМ-объекта

Интерфейс С
QuerylnterfaceQ

Интерфейс Z
Querylnterface()

1— >

Рис. 2.12. Работа с интерфейсами СОМ-объекта

Довольно любопытна функция AddRefQ. В СОМ-объектах используется технология, называемая счетчиком ссылок (reference counting) и отслеживающая все ссылки на объекты.
Необходимость в этом объясняется одной из особенностей СОМ-технологии: она не ориентируется на конкретный язык программирования. Следовательно, когда создается СОМобъект или интерфейс, для того чтобы отследить, сколько имеется ссылок на этот объект,
вызывается AddRefQ. Если бы СОМ-объект использовал вызовы mallocQ или new[] — это
было бы явным требованием использовать для разработки конкретный язык программирования С или C++. Когда счетчик ссылок обнуляется, объект автоматически уничтожается.
Тут же возникает второй вопрос: если СОМ-объекты — это классы, написанные на
C++, то каким образом их можно создавать или использовать в Visual Basic, Java, ActiveX
и т.д.? Просто так уж сложилось, что создатели для реализации СОМ использовали вир-

ГЛАВА2. КРАТКИЙ КУРС WINDOWS и DIRECTX

113

туальные классы C++. Вас никто не заставляет использовать именно этот язык программирования. Главное, чтобы созданный вами машинный код был аналогичен создаваемому компилятором Microsoft C++ при создании виртуальных классов. Тогда созданный
СОМ-объект будет вполне корректен и работоспособен.
Функция AddRefQ интерфейсов или СОМ-объектов вызывается автоматически функцией Qu«rylnterface(), т.е. вызывать ее самостоятельно не нужно. Но вьГможете захотеть вызывать ее явно: например, если по какой-либо причине пожелаете увеличить
счетчик ссылок на объект, чтобы этот объект считал, что число указателей, которые
указывают на него, больше, чем на самом деле.

У большинства компиляторов есть специальные дополнительные инструменты и возможности для создания СОМ-объектов, так что особой проблемы эта задача не представляет. Самое замечательное во всем этом то, что вы можете создавать СОМ-объекты
в C++, Visual Basic или Delphi и затем эти объекты могут быть использованы любым из
перечисленных языков. Бинарный код есть бинарный код!
Функция ReleaseQ служит для уменьшения счетчика ссылок СОМ-объекта или интерфейса на единицу,, В большинстве случаев эту функцию вам придется вызывать самостоятельно по окончании работы с интерфейсом. Однако если вы создаете один объект
из другого, то вызов функции Release() родительского объекта автоматически вызовет ReLeaseQ дочернего объекта. Тем не менее, лучше все же самостоятельно вызывать ReleaseQ
в порядке, обратном порядку создания объектов.

Создание и использование СОМ-интерфейсов DirectX
Я думаю, теперь вам понятно, что СОМ-объекты — это набор интерфейсов, которые
представляют собой не что иное, как указатели на функции (или, точнее, таблицу виртуальных функций). Следовательно, все, что вам нужно для работы с СОМ-объектами DirectX, — это создать объект, получить указатель на интерфейс, а затем обращаться к интерфейсу с использованием соответствующего синтаксиса. В качестве примера я буду использовать основной интерфейс DirectDraw.
Прежде всего, чтобы экспериментировать с DirectDraw, нам нужно следующее.
• Должны быть загружены и зарегистрированы СОМ-объекты времени исполнения
DirectDraw и соответствующие динамические библиотеки. Все это делает инсталлятор DirectX.
• Необходимо включить в Win32-nporpaMMy библиотеку импорта DDRAW.LIB, в которой находятся функции для работы с СОМ-объектами.
• Необходимо подключить к программе заголовочный файл DDRAW.H, чтобы компилятор мог получить информацию о типах данных и прототипах функций, используемых при работе с DirectDraw.
С учетом всего сказанного, вот как будет выглядеть тип данных для указателя на
интерфейс для разных версий DirectDraw. В случае DirectDraw 1.0 это LPDIRECTDRAW
Lpdd =NULL;, для DirectDraw 4.0 - LPDIRECTDRAW4 Ipdd =NULL;, для DirectDraw 7.0LPDIRECTDRAW7 Ipdd =NULL;. Версии DirectDraw 8.0, как уже отмечалось, не существует.
Теперь, чтобы создать СОМ-объект DirectDraw и получить указатель на интерфейс
объекта DirectDraw (который представляет видеокарту), достаточно использовать функцию DirectDrawCreatef).
DirectDrawCreatefNULL, &lpdd, NULL);

114

ЧАСТЫ. ВВВДЕНИЕ В ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Не беспокойтесь о параметрах. Если вам это интересно, обратитесь к справочной системе DirectX SDK, однако в 99 случаях из 100 функция будет выглядеть так же. Т.е. используется только один параметр, и это — адрес указателя интерфейса, заполняемый адресом DirectDraw COM-интерфейса. Сейчас просто поверьте, что этот вызов создает
объект DirectDraw и устанавливает указатель интерфейса на Lpdd.
Конечно, в этой функции выполняется масса вещей. Она открывает .DLL, загружает
ее, делает вызовы, а также много чего еще, но вам не нужно беспокоиться об этом.

После этого можно обращаться к DirectDraw. Хотя нет — ведь вы же не знаете, какие
методы и функции вам доступны, именно поэтому вы и читаете эту книгу! В качестве
примера установим видеорежим 640x480 с 256 цветами.
Lpdd->SetVideoMode(640,480,256);

Просто, не правда ли? Единственная дополнительная работа, которая при этом должна
быть выполнена, — это разыменование указателя на интерфейс Ipdd. Конечно, на самом деле происходит поиск в виртуальной таблице интерфейса, но не будем вдаваться в детали.
По существу, любой вызов DirectX выполняется следующим образом.
interface_pointer->method_name(parameter list);

Непосредственно из интерфейса DirectDraw вы можете получить и другие интерфейсы, с которыми предстоит работать (например Direct3D); для этого следует воспользоваться функцией QuerylnterfaceQ.

Запрос интерфейсов
Как ни странно, но в DirectX номера версий не синхронизированы, и это создает определенные проблемы. Когда вышла первая версия DirectX, интерфейс DirectDraw назывался IDIRECTDRAW. Когда вышла вторая версия DirectX, DirectDraw был обновлен до версии 2.0 и мы получили интерфейсы IDIRECTDRAW и IDIRECTDRAW2. В версии 6.0 набор интерфейсов расширился до IDIRECTDRAW, IDIRECTDRAW2 и IDIRECTDRAW4. После выхода
версии 7.0 к ним добавился интерфейс IDIRECTDRAW7, который остается последним, так
как в версии DirectX 8.0 DirectDraw не поддерживается.
Вы спрашиваете, что случилось с третьей и пятой версией? Понятия не имею! Но в результате, даже если вы используете DirectX 8.0, это еще не означает, что все интерфейсы
обновлены до этого же номера версии. Более того, разные интерфейсы могут иметь разные версии- Например, DirectX 6.0 может иметь интерфейс DirectDraw версии 4.0 —
IDIRECTDRAW4, но DirectSound при этом имеет версию 1.0 и его интерфейс имеет имя
IDIRECTSOUND. Кошмар! Мораль сей басни такова: когда вы используете интерфейс DirectX, вы должны убедиться в том, используется ли самая последняя его версия. Если вы
в этом не уверены, воспользуйтесь указателем на интерфейс версии 1.0, получаемый при
создании объекта, чтобы получить указатель на интерфейс последней версии.
Непонятно? Вот конкретный пример. DirectDrawCreateQ возвращает указатель на базовый интерфейс первой версии, но нас интересует интерфейс DirectDraw под именем IDIRECTDRAW?. Как же нам получить все возможности интерфейса последней версии?
Существует два пути: использовать низкоуровневые функции СОМ или получить указатель на интересующий нас интерфейс при помощи вызова Querylnterface(), что мы сейчас и сделаем. Порядок действий следующий: сначала создается СОМ-интерфейс DirectDraw с помощью вызова DirectDrawCreateQ. При этом мы получаем указатель на интерфейс IDIRECTDRAW. Затем, используя этот указатель, мы вызываем Querylnterface(),

ГЛАВА 2. КРАТКИЙ КУРС WINDOWS и DIRECTX

115

передавая ему идентификатор интересующего нас интерфейса, и получаем указатель на
него. Вот как выглядит соответствующий код.
LPDIRECTDRAW Ipdd; // версия 1.0
LPDIRECTDRAW7lpdd7; //версия 7.0
// Создаем интерфейс объекта DirectDrawl.O
DirectDrawCreate (NULL, &pldd, NULL);
// В DDRAW.H находим идентификатор интерфейса IDIRECTDRAW7
// и используем его для получения указателя на интерфейс
lpdd->QueryInterface(II[)JDirectDraw7f&lpdd7);
// Теперь у вас имеется два указателя. Так как указатель
//HalDIRECTDRAWHaM не нужен, удалим его.
lpdd->Release();
// Присвоение значения NULL для безопасности
lpdd=NULL;
// Работа с интерфейсом IDIRECTDRAW7

К По окончании работы с интерфейсом мы должны удалить его
ipdd7->Release();
//Присвоение значения NULL для безопасности
lpdd7=NULL;
Теперь вы знаете, как получить указатель на один интерфейс с помощью другого.
В DirectX 7.0 Microsoft добавила новую функцию DirectDrawCreateEx(), которая сразу возвращает интерфейс IDIRECTDRAW7 (и после этого тут же, в DirectX 8.0, Microsoft полностью отказывается от DirectDraw...).
HRESULT WINAPI DirectDrawCreateEx(
GUID FAR *LpGUID, // GUID для драйвера,
// NULL для активного дисплея
LPVOID
*iplpdd, // Указатель на интерфейс
REFIID
iid, // ID запрашиваемого интерфейса
lUnknown FAR *pUnkOLtter// Расширенный COM. NULL

);
При вызове этой функции вы можете сразу указать в iid требующуюся версию DirectDraw. Таким образом, вместо описанных выше вызовов QueryTnterfaceQ мы просто
вызываем функцию DirectDrawCreateExQ.
LPDIRECTDRAW7 Lpdd; // Версия 7.0
// Создание интерфейса объекта DirectDraw 7.0
DirectDrawCreateEx(NULU{void**)&lpdd,IID_IDirectDraw7,NULL);
Вызов DirectOrawCreateEx() создает запрошенный интерфейс, и вам не придется использовать для этого DirectDraw 1.0. Вот и все, что касается принципов использования
DirectX и СОМ. Теперь дело за изучением сотен функций и интерфейсов DirectX.

Резюме
В данной главе даются сведения по основам программирования для Windows, a
также о том, что представляет собой DirectX и как он связан с Windows. Замечательной
116

ЧдстЫ. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХ ИГР

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

ГЛАВА 2. КРАТКИЙ КУРС WINDOWS и DIRECTX

117

ГЛАВА 3

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


Введение в интерфейс виртуального компьютера

i 2U

• Построение интерфейса виртуального компьютера

122

• Консоль HipbiT3DLIВ

132

• Библиотека T3DUB1

138



Система ввода DirectX

182



Звуковая и музыкальная библиотека T3DLIB3

!88



Окончательная версия консоли игры

197



Образцы приложений T3DLIB

211

В этой главе мы в основном займемся созданием виртуальной
компьютерной системы, основанной на программном интерфейсе.
Она будет поддерживать линейные 8- и 16-битовые буферы кадров
(двойная буферизация) и входные устройства, а также возможность
работы с музыкой и звуком. Имея такой интерфейс, мы сможем: в оставшейся части книги сосредоточиться на проблемах трехмерной математики, графики и программирования игр, не отвлекаясь на нсякие
мелочи типа получения ввода от клавиатуры. Вот о чем мы поговорим в данной главе:
• разработка графического интерфейса виртуального компьютера;


построение консоли игры для Windows;

• список функций API библиотеки T3DLIB из книги Программирование игр для Windows. Советы профессионала',
• реализация виртуального компьютера при помощи библиотеки T3DLIB;


окончательная версия консоли игры;

• использование игровой библиотеки T3DUB.

Введение в интерфейс виртуального
компьютера
Цель данной книги — научить читателя работать с трехмерной графикой и программировать игры. Однако передо мной как автором стоит дилемма — на что в первую очередь
должна быть ориентирована данная книга? Только ли на графику и игры — без упоминания
таких вещей, как детали программирования для Win32, DirectX и т.п.? Несмотря на то, что
после прочтения предыдущей главы у вас должны появиться определенные знания о программировании Win32 и DirectX, этого все же слишком мало. Поэтому мое решение вопроса состоит в создании "виртуального компьютера" на основе разработанного в предыдущей
книге (Программирование игр для Windows. Советы профессионала) игрового процессора, который вы будете использовать как "черный ящик", так что мы сможем уделить больше
внимания вопросам трехмерной графики и программирования игр.
Это подход,, используемый OpenGL Вы используете библиотеки OpenGL, которые отвечают за низкоуровневую работу с устройствами и выделение и освобождение ресурсов,
включая такие вещи, как открытие окон и получение пользовательского ввода.

Такой подход вполне обоснован по целому ряду причин. Ведь нас в первую очередь интересует программирование игры, а не частности, связанные с получением ввода, выводом музыкального сопровождения или музыкальных эффектов. 99% нашей работы связано с растеризацией, отображением текстур, освещением, удалением скрытых поверхностей и т.п. Что
нам реально надо — это пустой экран, возможность получить информацию от джойстика,
мыши и клавиатуры и, возможно, вывод музыки и звуковых эффектов.
Вместо того, чтобы копаться в деталях Win32, разбираться с DirectX и т.п., мы можем
просто воспользоваться готовым API, который был разработан в предыдущей книге, в
качестве инструмента для создания обобщенного виртуального компьютера, с помощью
которого будут проведены наши эксперименты в области трехмерной графики и написаны наши игры.
Использование такого уровня косвенности приводит к тому, что разрабатываемый
код оказывается достаточно обобщенным для того, чтобы быть легко перенесенным на
другую платформу, например, для работы под управлением Мае или Linux. Все, что потребуется для этого — это эмулировать низкоуровневые интерфейсы, такие как графическая система с двойной буферизацией, вывод музыки, звука, и работа с устройствами
ввода. Весь прочий код будет одним и тем же на всех платформах.
Единственный недостаток игрового процессора из первой книги состоит в том, что
множество структур данных, да и сам дизайн оказались определяемыми DirectX. Это связано с тем, что в целях повышения производительности процессора я стремился сделать
рассматриваемый уровень между процессором и DirectX как можно более тонким. Однако вполне можно разработать дополнительный уровень над разработанным мною, который будет полностью машинно-независимым (но я не думаю, что эта задача стоит необходимого для ее решения времени). Главное в том, что если вы хотите перенести игру на

120

ЧАСТЬ!. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХ ИГР

другую платформу, то если вы сможете создать экран с двойной буферизацией, то все остальное будет нормально работать. Мои функиии звукового сопровождения и пользовательского ввода не делают ничего, кроме вызова соответствующих функций DirectX.
Как только вы разберетесь с API, предоставляемым моим игровым процессором, вы
можете забыть о Win32, DirectX и прочем — после этого вам нужно будет только корректно заполнить буфер кадра.
Итак, мы собираемся создать спецификацию виртуального, или абстрактного графического компьютера, а затем реализовать его при помощи API из предыдущей книги. Однако главное назначение виртуального компьютера — позволить нам вплотную заняться
программированием игр. С учетом этого вот как выглядит список возможностей, которые нужны нам для написания трехмерной игры для произвольного компьютера.
1 Возможность создания окна или экрана с определенной глубиной цвета и линейной адресацией двумерной матрицы пикселей. Кроме того, нужна поддержка внеэкранной страницы или двойной буферизации, чтобы изображение могло быть визуализировано без вывода на экран а затем скопировано или переключено на основной экран для создания эффекта анимации.

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

Аппаратное
обеспечение
ввода

Аппаратное
обеспечение
видеосистемы
Вторичный
буфер

Первичный
буфер

Экран

Рис. 3.1. Диаграмма системных уровней виртуального компьютера

На рис. 3.1 приведена схема создаваемого нами виртуального компьютера. Главный
принцип, положенный в основу этого компьютера,— "если я могу вывести пиксель
и прочесть нажатую клавишу, я в состоянии написать Doom". Это примерно так и есть —
все, что вам надо, это доступ к буферу кадра, а все остальное решается просто. Кроме
того, поскольку наша книга посвящена программированию трехмерных игр, ни один из
рассматриваемых алгоритмов не будет использовать возможности аппаратного ускореГЛА8А 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР ДЛЯ ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

171

ния (это даже как-то не спортивно...). Мы сами будем чертить многоугольники, определять освещенность и выполнять все вычисления, .необходимые для создания изображения в буфере кадра.
Вы можете спросить— зачем использовать программные решения, если доступны
аппаратные? Во-первых, чтобы быть настоящим профессионалом, надо уметь решать такие задачи без "аппаратных костылей". Во-вторых, знание того, как это все
работает, поможет вам при изучении аппаратных ускорителей. И в-третьих, кто, повашему, пишет код для аппаратного обеспечения?!

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

Буфер кадра и видеосистема
Перед тем как начать, будем считать, что у нас есть способ инициализации видеосистемы и открытия окна с определенным разрешением и глубиной цвета. Назовем соответствующую функция Create_Window(). Вот как она может выглядеть.
Create_Window(int width, int height int bit_depth);
Для создания экрана размером 640x480 с глубиной цвета 8 битов используется следующий вызов.
Create_Window(640,480,8);

Соответственно, для создания экрана 800x600 с глубиной цвета 16 битов вызов будет таким.
Create_Window(800, 600,16);
Не забывайте, что :за таким простым вызовом в действительности может скрываться целое
множество вызовов других функций — просто сейчас это нас не интересует. Да и само "окно"
может быть как обычным окном Windows, так и всем дисплеем в полноэкранном режиме.
Как я говорил, мы заинтересованы в разработке системы, которая опирается на первичный, видимый буфер дисплея, и вторичный, внеэкранный буфер, который является
невидимым. Оба буфера должны быть линейно адресуемы, причем одно слово (которое,
в зависимости от глубины цвета, может быть BYTE, WORD или QUAD) представляет отдельный пиксель. Наша система буферов кадров представлена на рис. 3.2.
В буфере кадра (первичном или вторичном) имеется область заполнения памяти, т.е. в ряде
видеосистем используется линейная адресация к пределах строки, но между строками имеется
определенный скачок адресов. Пример такой видеосистемы приведен на рис. 3.3. Ее характеристики — 640x480x8, т.е. на один пиксель приходится один байт памяти, соответственно, на
строку должно приходиться 640 байтов. Но, как видно из рисунка, на строку приходится
большее количество байтов, что связано с адресацией памяти в видеокарте. Нам нужна соответствующая модель, которая учитывает данный шаг памяти (memory pitch).
122

ЧАСТЬ!. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

(Внеэкранное изображение
в системной или видеопамяти)

(Экранная видеопамять)
или

memcpyO
переключение буферов!
Копирование данных

Очередной кадр копируется на дисплей
Дисплей

Рис. 3.2. Система буферов виртуального компьютера

р

Адреса памяти
(0,0) LP_Surfaee (первыйбайт)

О
IPitch
2 х IPitch

Кэш, виртуальная память
(639,0)

Строка 0
Строка 1
Строка 2

640 байт.
[Pitch
.'хДополж- -'•
~ тельная ~
область

640 х 480
Рабочая область

479 х IPitch

Строка 479

(О, 479)

(639, 479)

Рис. 3.3. Аппаратный буфер кадра

В такой модели учитывается, что видеокарта может иметь дополнительную область памяти за пределами строки, связанную с особенностями кэширования или аппаратной адресации. Такая организация видеопамяти не представляет никакой проблемы — мы просто
должны учесть, что шаг памяти не совпадает с шагом видеопамяти. Так, чтобы обратиться к
пикселю в системе с глубиной цвета 8 битов, мы используем следующий код.
UCHAR *video_buffer; // Указатель на видеобуфер
intx,y;
// Координаты пикселя
int memory_pitch; // Количество памяти на строку
video_buffer[x +• y'memory_pitch] - pixel;

ГЛАВАЗ. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

123

В системе с глубиной цвета 16 битов код почти ничем не отличается от рассмотренного.
USHORT *v5deo_buffer;//Указатель на видеобуфер

intxfy;
// Координаты пикселя
int memory_pitch; // Количество памяти на строку
video_buffer[x + y*(memory_pitch » 1)] = pixel;
Конечно, если переменная memory_pitch указывает количество слов USHORT на одну строку, то операция сдвига становится излишней.
С учетом сказанного, приступим к разработке модели первичного и вторичного буферов. Учтите, что это всего лишь модель, и ее детали могут измениться, когда мы приступим к ее реализации с использованием игрового процессора T3DLIB. В любом случае нам
нужны указатели на первичный и вторичный буфер и переменная для хранения шага памяти. Таким образом, нам нужны следующие глобальные переменные.
UCHAR *primaiy_buffer; // Первичный буфер
int primary_pitch; // Шаг памяти буфера
UCHAR *secondary_buffer; // Вторичный буфер
int second ay „pitch; // Шаг памяти буфера

Обратите внимание, что оба указателя имеют тип UCHAR*, и что шаг памяти выражен в
байтах. Это удобно при работе с 8-битовой глубиной цвета, но при работе в 16-битовом
режиме на каждый пиксель приходится по 2 бита. В этом случае вы можете при желании
выполнить явное приведение типа к USHORT*, но мне лично представляется более логичным и понятным использование указателя UCHAR* и значения шага памяти в байтах. В
этом случае все функции, которые нам предстоит написать, будут использовать одни и те
же параметры, независимо от режима работы.

Блокировка памяти
Прд работе с буферами мы сталкиваемся с еще одной особенностью— блокировкой
(locking) памяти. Многие видеокарты имеют специальную видеопамять, которая может
быть переносимой, кэшируемой и т.п, Это означает, что при обращении к памяти для
чтения или записи в буфер, вы должны дать системе знать об этом, с тем чтобы не произошло случайного изменения памяти во время вашей работы с ней. По завершении работы вы разблокируете память, и система может продолжать работу с ней.
Если вы — старый программист, то ощутите прилив ностальгических чувств — ведь
в версиях 1.0-3.0 Windows вы были обязаны заниматься блокироакой памяти.

Все это с точки зрения программистов означает, что указатели primary_buffer и secondaryjbuffer корректны только на время блокировки. Причем нельзя гарантировать, что их
значения останутся теми же при очередной блокировке. Например, при первой блокировке указатель на буфер может иметь значение OxOFFFEFFCQOOOOOOO, а при следующей —
OxOFFFFFFDOOOOOOOO. Это связано с работой аппаратного обеспечения, которое может, например, просто перенести буфер в другое место, так что будьте внимательны! В любом
случае процедура использования блокировки выглядит следующим образом.
1. Заблокировать интересующий нас буфер (первичный или вторичный) и получить
стартовый адрес и шаг памяти.
2. Выполнить необходимые действия с видеопамятью.
3. Разблокировать буфер.
124

ЧАСТЫ. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ игр

Само собой разумеется, в 99% случаев вы будете выполнять последовательность блокировка— чтение/запись—разблокирование только для вторичного буфера, поскольку явно не захотите, чтобы пользователь видел, как вы вносите изменения в изображение,
Исходя из описанной особенности обращения к видеопамяти, разработаем новые
функции для выполнения блокировки и разблокирования.
Lock_Primary(UCHAR**primary_buffer,
int*primary_pitch);
Urilock_Primary(UCHAR *primaiy_buffer);
Lock_Secondary(UCHAR**secondary_bufferf
int *secondary_pitch);
Unlock_Secondary(UCHAR Secondary buffer);

Для блокировки буфера вы вызываете функцию, передавая ей в качестве параметров
адреса переменных для хранения адреса буфера и шага памяти. Функция блокирует поверхность и записывает фактические значения в переданные ей переменные, после чего
вы можете использовать их — до тех пор, пока не разблокируете поверхность вызовом соответствующей функции. Просто, правда?
В качестве примера посмотрим, как будет выглядеть запись одного пикселя на экране
размером 800x600 как в 8-битовом, так и в 16-битовом режимах. Вот как это выглядит
при глубине цвета, равном 8 битам.
UCHAR *primary_bijffer; // Первичный буфер
int primary_pitch; // Шаг памяти
UCHAR *secondary_buffer;// Вторичный буфер
int seconday__pitch; //Шаг памяти
UCHAR pixel;
int х,у;

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

// Шаг 1: Создание окна
Create_Window(800,600,8);
// Шаг 1: Блокировка вторичной поверхности
Lockjiecondary(&secondary_buffer,&secondary_pitch);
// Запись пикселя в центре экрана
secondary_buffer[x + y*secondary_pitch] = pixel;
// Разблокирование буфера
Unlock_Secondary(secondary_buffer);

Все очень просто. А вот тот же код, но для 16-битового режима.
UCHAR *primary_buffer; // Первичный буфер
int primary_pitch; // Шаг памяти
UCHAR *secondary_buffer; // Вторичный буфер
int seconday_pitch; //Шаг памяти
USHORT pixel;
// Записываемый пиксель
int x,y;
// и его координаты

ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

125

// Шаг 1: Создание окна
Create_Window(800, 600,16);
// Шаг 1: Блокировка вторичной поверхности
Lock_Secondary{&secondary_buffer, &secondary_pitch);
//Запись пикселя в центре экрана. Здесь надо не забывать о
//том, что указатель, полученный при блокировке, имеет тип
// UCHAR*, а не USHORT*, так что мы должны выполнить явное
// преобразование типа
USHORT *video_buffer = (USHORT *)secondary_buffer;
video_buffer[x-*-y*(secondary_pitch » 1)] hutdown(void *parms)
// Эта функция завершает работу игры и освобождает все
// захваченные ресурсы
// Возврат кода успешного завершения
return(l);
} //Game_Shutdown

ilHiUHHIflfililUllllilHfflllliHUllllllillHIIHIII
int Game__Main(void *parms)
I
// Эта функция вызывается в реальном времени в каждом
// цикле событий. Вся логика игры сосредоточена в этой
//функции
// Проверка, не запрошен ли выход из игры
if(KEY_QOWN(VK_ESCAPE))
PostMessage(main_window_handle,WM_DESTROY,0,0);
)

// Возврат кода успешного завершения
return(1);
} // Game_Main

iiiiiuiHiiHiiiiiiiiUiHiiHiiiiuiuiiiiiiiiiiiiiiiiiu
ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

137

Имя файла с исходным кодом — T3DCONSOLEALPHA1.CPP; само собой разумеется, исполняемый файл имеет имя T3DCONSOLEALPHA1.EXE. Если вы запустите его, то увидите на экране
пустое окно, показанное на рис. 3.8. Однако на самом деле сделано очень много: создано
окно, вызвана функция Game_Init(), постоянно вызывается функция Game_MainQ... Наконец, при закрытии окна вызывается функция GameJ>hutdown().

Рис. 3.8. Первое приближение консоли игры в действии
Все, что требуется от вас, — это добавить необходимую функциональность в три
функции Game_*(),
Как видите, совершенно напрасно большинство книг утверждает, что программирование для Windows — очень сложная работа. Вы уже убедились, что это просто
сплошное удовольствие!

У нас уже имеется все необходимая функциональность Windows, и теперь нам необходима функциональность DirectX. Нам нужен интерфейс DirectX, который бы эмулировал
наш виртуальный компьютер и предоставил в наше распоряжение графическую систему
с двойной буферизацией и возможностями воспроизведения звуковых эффектов и музыки. Сейчас можно познакомиться с окончательной версией консоли игры, которая предоставляет все это в наше распоряжение, но мне представляется, что будет лучше рассмотреть API, который я использовал для создания консоли игры. Не забывайте, что нет
необходимости детально разбираться с API и каждой его функцией. Просто ознакомьтесь
с ними и с прилагаемыми к ним пояснениями и примерами.
После того как вы ознакомитесь с инструментарием, который будет использован для построения окончательной версии консоли игры, мы возьмем файл T3DCONSOLEALPHA1.CPP в качестве основы и добавим к нему различную функциональность, необходимую для получения
окончательной версии интерфейса виртуального компьютера. Затем этот "шаблон" будет использоваться в качестве стартовой точки для всех демонстрационных программ и игр в данной
книге. Кроме того, в конце этой главы я приведу ряд примеров, в которых покажу, как использовать графику, ввод и звуковые возможности библиотеки T3DLJB.

Библиотека T3DLIB1
Теперь мы готовы рассмотреть все макроопределения, структуры данных и функции,
составляющие API графического модуля библиотеки T3DLIB — T3DLIB1.CPP.
Модуль состоит из двух файлов — T3DLI61.CPPJH. При работе вы просто связываете эти
файлы с вашей программой и затем используете их API.

138

ЧАСТЬ!. ВВВДЕНИЁВ ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХИГР

Если вы читали мою предыдущую книгу, то должны заметить, что разрабатываемый
нами игровой процессор практически такой же, как и предыдущий,— с дополнительной поддержкой 16-битового режима и поддержкой окон. Код остается полностью совместимым, гак что вы можете взять любой пример из первой книги и скомпилировать его с новой версией T3DLIB1.CPP без каких бы то ни было проблем. И конечно, весь код компилируется как с DirectX 8.0, так и с DirectX9.0+.

Архитектура графического процессора DirectX
Библиотека T3DUB1 представляет собой двумерный игровой процессор, как видно из схемы, приведенной на рис. 3.9. Это процессор, поддерживающий работу в 8- и 16-битовом цветовых режимах, двойную буферизацию, поддержку различных разрешений экрана и отсекание. Поддерживается как оконный, так и полноэкранный режимы работы. В любом режиме
работы вывод выполняется во внеэкранный буфер, содержимое которого затем копиругтся
в первичный буфер (либо выполняется переключение буферов).
Для построения приложения с использованием библиотеки вы должны включить
в проект файлы T3DLIB1.CPPJH, а также файлы DDRAW.UB (библиотека DirectDraw)
и WINMM.LIB (библиотека мультимедиа Win32).
Библиотека WINMM.LIB нужна только в том случае, когда вы используете Visual C++.

Объекты блиттера

8/16 битовая
графическая
подсистема

Растровые изображения
Многоугольники

Экран

Рис. 3.9. Архитектура графического процессора

Основные определения
Библиотека имеет один заголовочный файл — T3DLJB1.H, внутри которого имеется ряд
макроопределений #define, используемых игровым процессором.
// Проверка множественного включения
ftifndefTSDLIBl

«define T30UB1

ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

139

//

DEFINES

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

// Значения по умолчанию для экрана. Могут быть
// переопределены функцией DDraw_Init()
«define SCREEN.,WIDTH 640 // Размер экрана
«define SCREEN, HEIGHT 480
«define SCREEN^BPP 8
// Битов на пиксель
«define MAX_CGLORS_PALETTE 256
«define DEFAULT_PALETTE_FILE "PALDATA2.PAL"
// Используется для выбора режима экрана (полноэкранный или

//оконный)

«define SCREEN^FULLSCREEN О
«define SCREEN,WINDOWED l
//Определения для работы с растровыми изображениями
«define BITMAPJD 0x4042 //Универсальный идентификатор
// растрового изображения
«define BITMAP_STATE_0EAD О
«define BITMAP.STATE^AUVE 1
«define BITMAP^STATILDYING 2
«define BITMAPJUTR^LOADED 128
«define BITMAP_EXTRACT_MODE_CELL 0
«define BITMAP_EXTRACT_MODE_ABS 1
//Форматы пикселей DirectDraw
«define DD_PrXEL_FORIW8
8
«define DD^PIXELFORMAT555
15
«define DD_PIXEL_FORMAT565
16
«define DD_PIXEL,FORMAT888 24
«define DD_PIXELFORMATALPHA888 32
// Определения для объектов блиттера
«define BOB_STATE_DEAD
0
«define BOB_STATE_AUVE 1
«define BOB_STATE_DYING 2
«define BOB_STATE_ANIM_DONE 1
«define MAX_BOB_FRAMES 64
«define MAX_BOB_ANJMATIONS 16
«define BOB_ATTR_SINGLE_FRAME 1
«define BOB^ATTR^MULTI_FRAME 2
«define BOB_ATTR_MULTI_ANrM 4
«define BOB_ATTR_ANrM_ONE_SHOT 8
«define BOB_ATTR_VISIBL.E
16
«define BOB_ATTR_BOUNCE
32
«define BOB_ATTR__WRAPAROUND 64
«define BOB_ATTR_LOADED
128
«define BOB_ATTR_CLONE
256
// Команды перехода экрана (режим 256 цветов)
«define SCREEN_DARKNESS 0// Переход к черному

140

ЧАСТЬ I. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ w

CREEN

BLUENESS 6 // "

R KpacHOM

//Построение 32-битового цвета в формате А.8.8.8
ttdefine_RGB3?.BH(a,r,g,b)\
(fb) + ((д) « 8) + ((г) « 16) + ((а) « 24))
// Макросы для работы с битами
tfdefine SET_BIT(worUbit_ftag) \
((word)=((wordj | (bitjtag)))
/(define R£SET_SIT(word,bit_flagJ \

//Инициализация структуры DirectDraw
//нулями и указание поля dwSize
^define DDFWW_INIT_STRU(T(ddstrort) \
{memset(&ddstfuctO,sizeof(ddstruct)J;
ddstruct.dwSire-sfzeof"(ddstruct);}
// Вычисление минимума и максимума двух выражений
itdefine MINfa, 6; (((а) < (bj) ? (aj : (bJJ
«define MAXfa, b) (((a) > fb)J ? (bj : (a))
//Обмен значений
tfdefine SWAP(a,6,t) {t-a; a-b; b=t;>
// Некоторые математические макросы
^define DEGJfQ^RAD(ang) f(angJ*PI/180)
Define RAO_TO_DEG(rads) ((radsJfc!80/PIJ
tfdefme RAND_RANGE('x,y) ((x) + (г

Типы и структуры данных
Теперь мы ознакомимся с типами и структурами данных, используемыми игровым
процессором.
//Основные беззнаковые типы
typedef unsigned short USHORT;
typedef unsigned short WORD;
typedef unsigned char UCHAR;
typedef unsigned criar BYTE';
typedef u nsigned int QUAD;
typedef unsigned int UINT;
//Структура для хранения .BMP-файлов с растровыми
//изображениями
typedef struct BITMAP^ FILE^TAG
I
//Заголовок файла с растровым изображением
SITMAPFILEHCADERbitmapfileheader;
//Информация, включающая палитру
SITMAPINFOHEADERbitmapinfoheader;
PALf ТТЕ В NTRV pa(ette[256J; // Палитра
UCHAR *buffer;
// Указатель на данные

142

ЧАСТЬ!. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

// Структура объекта блиттера
typedef struct BQBJTYP

i

int state;
// Состояние объекта
int anim_state; // Состояние анимации
int attr;
// Атрибуты объекта
float x,y;
// Позиция вывода изображения
float xv,yv; // Скорость объекта
int width, height; // Ширина и высота
int width Jill; // Внутренняя переменная
intbpp;
// Битов на пиксель
int counter_l; // Общие счетчики
intcounter_2;
int max_count_l; //Пороговые значения
int max_count_2;
int varsl[16]; // Стек из 16 целых чисел
float varsF[l6]; // Стек из 16 действительных чисел
intcurrjrame; //Текущий кадр анимации
int numjrames; // Общее количество кадров анимации
int curr_animation;// Индекс текущей анимации
int anim__counter; // Используется при анимации
int anim Jndex; // Индекс элемента анимации
int anim_count_max;// Количество циклов до анимации
// Последовательности анимации
int*anirnations[MAXJ}QB_ANIMATIQNS};
// Поверхности
LPDIRECTDRAWSURFACE7images[MAX_JOBJ:RAMES];
}BOB,*BQB_PTR;
// Простое растровое изображение
typedef struct BITMAP JMAGE_TYP

;

int state;
// Состояние изображения
intattv;
// Атрибуты изображения
int x,y;
// Позиция изображения
int width, height; // Размер изображения
int num_bytes;
// Количество байтов изображения
int bpp;
// Битов на пиксель
UCHAR *buffer; // Пиксели изображения
} BITMAP „IMAGE, *BITMAPJMAGEJ>TR;
// Структуры мигающего света
typedef struct BLINKERJTYP

i

int color_index;
// Индекс цвета
PALETTEENTRY on_color; // Цвет включенного состояния
PALETTEENTRY off^color; // Цвет выключенного состояния
int onetime;
// Количество кадров во
// включенном состоянии
int offjime;
// Количество кадров в
// выключенном состоянии
// Внутренние члены
int counter;
// Счетчик переходов состояний

ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ игр

143

fnt state;

// Состояние света:
// -1 - выключен, 1 - еключен,
//О- отключен
> BUNKER, *BLINKER_PTR;
//Двумерная вершина
typedef struct VERTHX2DI_TYP
I
intxy;
// Вершина
} VERTEX2DI, *VERTE:X2DI_PTR;
//Двумерная вершина
typedef struct VERTEX2DF_TYP
I
float x,y;
//Вершина
} VERTEX2DF, *VERTEX2DF_PTR;
//Двумерный многоугольник
typedef struct POLYGON2DJYP
{
fnt state;
// Состояние многоугольника
int num^verts; // Число вершин
intxO,yO;
// Позиция центра
int xv,yv;
// Начальная скорость
DWORD color;
// Индекс или PALETTENTRY
VERTEX2DF Mist; // Указатель на список вершин
} POLYGON2D, *?OLYGON2D^PTR;
// Определения матриц
typedef struct MATRIX3X3_TYP
i
union
I
float M[3][3];// Массив индексированных данных для
//хранения матрицы в формате со
// старшей строкой
struct

{

};

float MOO, M01, M02;
floatM10,M:i,M12;
float MZO, M21, M22;

}; // union
} MATRIX3X3, *MATRIX3X3_PTR;
typedef struct MATRIX1X3_TYP
I
union
(
float M[3]; // Массив индексированных данных для
//хранения матрицы
struct
I
144

ЧАСТЬ). ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХИГР

>;

float MOO, MOl, MOZ;

}; // union
} MATRIX1X3, *MATRIX1X3_PTR;
typedef struct MATRIX3X2 JTYP
I
union
<
float M[3][2]; // Массив индексированных данных для
// хранения матрицы в формате со
// старшей строкой
struct
i
float MOO, MOl;
float Mia Mil;
float M20,M21;

>;

}; // union
} MATRIX3X2,*MATRIX3X2_PTR;
typedef struct MATRDQX2JTYP
(
union
{
float M[21; // Массив индексированных данных для
// хранения матрицы
struct

(

>;

float MOO, MOl;

}; // union
} MATRIX1X2, *MATRIX1X2JTR;
Обратите внимание на некоторую поддержку математических операций. Она связана
с поддержкой двумерных преобразований многоугольников в предыдущей книге.

Прототипы функций
Теперь, перед тем как приступить к рассмотрению отдельных функций, я хочу познакомить вас с их полным списком.
// Функции DirectDraw
int DDraw J.nit(int width, int height, int bpp,
intwindowed-0);
int DDrawJjhutdown(void);
LPDIRECTDRAWCUPPER
DDraw_Attach_CLipper(LPDIRECTDRAWSURFACE7lpdds,
intnumj-ects, LPRECT clipjist):
LPDIRECTDRAWSURFACE7
DDraw_Create_5urface(int width, int height,
int mem_flags=0,
ГЛАВАЗ. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

145

USHORT color_key_value=OJ;
int DDraw_F[ip(void);
int ODraw_Wait_For_Vsync(void);
int DDraw_FttLSurface(LPDIRECTDRAWSURFACE7 [pdds,
USHORT color, RECT *client-NULL);
UCHAR *DDraw_Lock_Surface(LPDIRECTDRAWSURFAC£7 Ipdds,
int *lpitch);
int DDraw^UnLock_.Surface(LPDIRECTDRAWSURFACE7 [pdds);
UCHAR *DDraw^ock__Prirrian/_Suiface(void);
mtDDraw_Unlock__Prfmary_Surface(void);
UCHAR *DDraw_Lock_Bac(t_Surface(void);
int DDraw_Untock_Back_Surface(void);
// Функции для работы с объектами блиттера
int Create_BOB(BOB_PTR bob.int x, int y,int width,
int height, int num_frame$,irttattr,
int mem_flags=0, USHORT color_key_value=0,
int bpp-8);
fntClone_BOB(BOB_PTfi source, BOB^PTR destj;
int Destroy_BOB(BOB_PTR bob);
int Draw_BOB(BOB_PTR bob, LPDIRECTDRAWSURFACE7 dest);
int Draw_Sca(ed_BOB(BOB__PTR bob, int swidth, int sheight
LPDJRECTORAWSURFACE7 dest);
int Draw_BOB16(BOB._PTR bob, LPDIRECTDRAW/SURFACE7 dest);
int Draw_Scaled_BOBl.6(BOB_PTR bob, int swidth, int sheight
LPDIRECTDRAWSURFACE7 dest);
int Load_Frame_BOB(BOB_PTR bob, BITMAP^FILE^PTR bitmap,
int frame, int cx,int cy,int mode);
int Load^Frame_BOB16(BOB_PTR bob, 8ITMAP^nLE_PTR bitmap,
int frame, int cx,int cy,int mode);
int Animate_BOB(BOB_PTR bob);
intMove_BOB(BOB__PTR bob);
int Load_Am"mation_BOB(BOB_PTR bob, int anim^index,
int nom^frames, int "sequence);

.

int Set_Pos_BOB(BOB_PTR bob, int x, inty);
intSet_Vet_BOB(BOB_PTRtiob,intxv,intyv);
int Set_Anim_Speed_BOB(BOB_PTR bob,int speed);
int SelLAnimation_BOB(BOB_PTR bob, int animjndex);
int Hide_BOBfBOB_PTR bob);
ЧАСТЬ I. ВВВДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

intShow_BOB(BOBJ>TR bob);
int Collision_BOBS(BOB_PTR bob!, BQB^PTR bob2);
// Вспомогательные функции общего назначения
DWORD Get_Clock(void);
DWORD Start_Qock(void);
DWORD Waitj;iock(QWORD count);
int Collision__Test(int xl, int yl, int wl, int hi,
int x2, int y2, int w2, int h2);
int Color J>can(int xl, int yl, int x2, int y2,
UCHAR scan^start, UCHAR scan_end, t
UCHAR *scanjjuffer, int scanjpitch);
int Color_Scanl6(int xl, int yl, int x2, int y2,
USHORT scan^start, USHORT scan^end,
UCHAR *scanj}uffer, int scan^Lpitch);
// Графические функции
int Draw_Clip_l_ine(int xO,int yO, int xl, int yl, int color,
UCHAR *dest_burrer,intlpitch);
int DrawJllip_Linel6(int xO,int yO, int xl, int yl,
int color, UCHAR *dest_buffer,
intlpitch);
intaipJ-ine(int&xl,int&yl,intSbuffer, ship->width, ship->height);
// В 16-битовом режиме нам надо удвоить ширину, поскольку на
// пиксель здесь приходится 2 байта
FLip_Bitmap(ship2->buffer, 2*ship2->width, ship2->height);-

Прототип функции
int Copy_Bitmap(BITMAP_IMAGE_PTR dest^bitmap,
intdest_x, intdest_y,
BITMAP JMAGE_PTR source_bitmap,
int source^x, int source_y,
int width, int height);

Назначение
Функция Copy_Bitmap() копирует прямоугольную область из одного изображения
в другое. Исходная и целевая области могут быть в одном и том же изображении; однако
области не должны перекрываться — в противном случае результат не определен. В случае успешного выполнения возвращает TRUE.
Пример использования
// Загрузка .BMP-файла в память
BITMAP_FILE bitmapjile;
Load_Bitmap_File(&bitmap_file,"playfield.bmp");
// Инициализация объекта для хранения игрового поля
BITMAP JMAGE playfield;
Create_Bitmap(8iplayfield, 0,0,400,400,16);
//Загружаем данные
Load_Image_Bitmapl6(&playfield, &bitmap_fil.e, 0,0,
BITMAP_EXTRACT_MODE_ABSOLUTE);
// Копируем верхнюю часть изображения в нижнюю
Copy_Bitmap(&playfietd, 0,200,
aplayfieLd,Q,0,
200, 200);

Прототип функции
int Scroll_Bitmap(BITMAP_IMAGE_PTR image,int dx,int dy»0);

Назначение
Функция ScrolLBitmap() прокручивает переданное изображение вертикально или горизонтально на величины dx, dy вдоль осей х и у, соответственно. Положительные значения означают прокрутку вправо и вниз. В случае успешного выполнения возвращает TRUE.
Пример использования
// Скроллинг изображения вправо на 2 пикселя
ScrolLBitmapt&playtleld, 2,0);

Функции для работы с 8-битовыми палитрами
Данные функции по сути представляют собой интерфейс для работы с 256-цветными
палитрами. Они работают только в 8-битовом режиме. Учтите, что в оконном приложении
ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

169

первые и последние 10 цветов зарезервированы для использования Windows, и вы не имеете
права их изменять (в полноэкранном режиме вы имеете право изменять любой цвет).
Фундаментальная структура данных, использующаяся для хранения цвета, ~ структура PALETTEENTRY Win32.
typedef struct tagPALETTEENTRY {
BYTE peRed; //8 битов красного канала
BYTE peGreen; // 8 битов зеленого канала
BYTE peBlue; //8 битов синего канала
BYTE peFlags; // Управляющие флаги: PC_EXPLICIT для
// цветов Windows, PC_NOCOLLAPSE для всех
//остальных
} PALETTEENTRY;
Приступим к рассмотрению конкретных функций.
Прототип функции
int Set_Palette^Entry(
intcolorjndex, // Индекс изменяемого цвета
LPPALETTEENTRY color); // Новый цвет
Назначение
Функция Set_Palette_Entry() используется для изменения одного цвета палитры. Вы просто указываете номер изменяемого цвета в диапазоне 0..255 и указатель на структуру PALETTEENTRY, в которой хранится цвет— и это обновление палитры станет видимым в следующем кадре. Кроме того, функция обновляет теневую палитру. Заметим, что это достаточно
медленная функция, так что при необходимости обновления всей палитры следует пользоваться функцией Set_Patette(). В случае успешного выполнения возвращает TRUE,
Пример использования
//Делаем цветО черным
PALETTEENTRY black = {0,0,0,PC_NOCOLLAPSE>;
SeL Pa tette_ Entry (0,&bUck);
Прототип функции
int GeLPalette_Entry(
int colorjndex, // Индекс интересующего цвета
LPPALETTE ENTRY color); // Цвет

' .

.

• •• .. •

•-



»!•,

Назначение
Функция Get,Palette..Entry() получает информацию о записи текущей палитры. Функция очень быстрая, поскольку получает информацию из теневой палитры в оперативной
памяти. Таким образом, данную функцию можно вызывать в любой момент, поскольку
она не обращается к аппаратному обеспечению. В случае успешного выполнения возвращает TRUE.
Пример использования
// Получаем цвете индексом 100
PALETTEENTRY color;
Get_PaLette_Entry(100,&color);
Прототип функции
int Save_Palette_To_File(
char *fi(ename,
// Имя файла
LPPALETTEENTRY palette); // Палитра

170

ЧАСТЬ I. Введение в ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХ игр

Назначение
Функция $aveJ>aletteJo_FileO сохраняет переданную палитру в ASCII-файле на диске для
последующей загрузки или обработки. Функция очень удобна для генерации палитр "на лету"
и сохранения их на диске. Предполагается, что указатель указывает на 256-цветную палитру,
так что будьте внимательны. В случае успешного выполнения возвращает TRUE.
Пример использования
PALETTEENTRY my_paLette[256];

// Считаем, что палитра уже
// создана

// Сохраняем палитру. Имя может быть любым, но я предпочитаю
// использовать расширение *.paL
Save_Palette_To_FHe("/paLettes/customl.par,&mv_palette);
Прототип функции
int Load_Palette_FromJFile(
char "filename,
// Файл
LPPALETTEENTRY palette); // Палитра
Назначение
Функция Load_Palette_From_File() используется для загрузки предварительно сохраненной на диске при помощи функции Save_Palette_To_File() 256-цветной палитры. Данная функция не загружает палитру аппаратно— для этого вы должны использовать
функцию Set_PaletteQ- В случае успешного выполнения возвращает TRUE.
Пример использования
// Загружаем палитру с диска
Load_Palette_From_FiLe(7palettes/customl.pal",&my_palette);
При инициализации 256-цветного режима при помощи функции DDraw_Init() загружается стандартная палитра с именем PALDATA2.DAT.
Прототип функции
int $et_Palette(LPPALETTEENTRY set_palette);
Назначение
Функция Set_Palette() загружает переданную палитру в аппаратное обеспечение и обновляет теневую палитру. В случае успешного выполнения возвращает TRUE.
Пример использования
// Загрузка палитры в аппаратное обеспечение
Set_Palfitte(&my_palette);
Прототип функции
int Save J>aLette(LPPALETTEENTRY sav_palette);
Назначение
Функция Save_Palette() сканирует и сохраняет аппаратную палитру в sav_palette для
дальнейшего сохранения на диске или обработки. Переменная sav_palette должна иметь
достаточно места для загрузки 256 цветов.
Пример использования
// Получение текущей палитры DirectDraw
PALETTEENTRYhardware_palettei;256};
Save_Palette(&hardware_palette);

ГЛАВАЗ. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР ДЛЯ ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

171

Прототип функции
int Rotate Jlotorsf
int startjndex // Начальный индекс 0..255
intend_index); //Конечный индекс 0.. 255
Назначение
Функция Rotate_Colors() циклически смещает набор цветов, работая непосредственно
с аппаратным обеспечением. В случае успешного выполнения возвращает TRUE, в противном случае — FALSE.
Пример использования
// Смещение всей палитры
RotateJTolors(0,255);
Прототип функции
intB[ink__Co[ors(
int command,
// Команда процессора мигания
BLINKER_PTR newjight// Данные
int id);
//Идентификатор мигания
Назначение
Функция BIink__Co[ors() используется для создания асинхронной анимации палитры.
Функция слишком большая, чтобы подробно разбирать ее здесь, так что обратитесь к материалу предыдущей главы.
Пример использования
Отсутствует

Вспомогательные функции
Прототип функции
DWORD
Назначение
Функция Get_Clock() возвращает текущее показание часов в миллисекундах с момента
запуска Windows.
Пример использования
// Получение текущего показания часов
DWORD startjnme - Get_Clock();
Прототип функции
DWORD Start

Назначение
Функция Start_Uock() выполняет вызов Get_Clock() и сохраняет полученный результат
в глобальной переменной для дальнейшего использования функцией Wait_Clock(). Возвращает значение показания часов в момент вызова,
Пример использования
//Сохранение показаний часов в глобальной переменной
Start_Clock();
Прототип функции
DWORD WaitJIlockfDWORD count);

172

// Количество миллисекунд
// ожидания

ЧАСТЬ I. ВВВДЕНИЕв ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХ игр

Назначение
Функция Wait_Qock() ожидает, когда пройдет переданное ей в качестве параметра количество миллисекунд со времени последнего вызова Start_CLock(),Возвращает значение
показания часов в момент вызова, но возврат не происходит до тех пор, пока не истечет
время ожидания.
Пример использования
//Ждем 30 миллисекунд
Stait_Qock(};
// Некоторый код..,
Wait_CLock(30);
Позже в книге мы будем использовать высокопроизводительные таймеры Windows с
лучшим разрешением.
Прототип функции
intCollision_Test{
intxl,intyl, // Верхний левый угол объекта 1
int wl,inthl, // Ширина и высота объекта 1
int xHr int y2, // Верхний левый угол объекта 2
int w2, inth2}; // Ширина и высота объекта 2
Назначение
Функция CoUision_Test() проверяет наличие перекрытия переданных ей прямоугольников (прямоугольники могут представлять все, что угодно). Возвращает TRUE при перекрытии прямоугольников и FALSE в противном случае.
Пример использования
// Перекрываются ли эти два изображения?
if (CoLlision_Test(shipl->x, shipl->y,
shi p l->width,s hi pl->h eight
ship2->x, ship2->y,
ship2->width,ship2->neight))
{ // Да, перекрытие есть

Прототип функции
int CoLor_Scan(
int xl, int yl, // Верхний левый угол прямоугольника
int x2, int y2, // Нижний правый угол прямоугольника
UCHAR scan_start // Начальный цвет
UCHAR scan__end, // Конечный цвет
UCHAR *scan_buffer,// Сканируемая память
int scan_ (.pitch); // Шаг памяти
Назначение
Функция Color_Scan() представляет другой алгоритм определения столкновений, который сканирует прямоугольник на наличие одного 8-битового значения или последовательности значений в некотором непрерывном диапазоне. Вы можете использовать его
для определения того, имеется ли интересующий нас индекс цвета в некоторой области.
Естественно, эта функция работает с 8-битовыми изображениями, однако она легко
расширяема для 16-битового режима. Возвращает TRUE, если цвет(а) найден(ы).

ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

173

Пример использования
// Поиск цвета в диапазоне 122-124 включительно
Color_Scan{10,10, 50, 50,122,124,
backj}ufferr back_lpitch);

Процессор для работы с объектами блиттера
Хотя типа BITMAP_IMAGE в принципе достаточно практически для всего, что вы только
можете придумать, у него есть один серьезный недостаток — он не использует поверхности DirectDraw, а следовательно, не использует поддержку аппаратного ускорения. Поэтому я создал тип BOS (blitter object), который очень похож на спрайт. Спрайт — это не
более чем объект, который вы можете перемешать по экрану (обычно не затрагивая при
этом фоновое изображение). В нашем случае это не так, и поэтому я и назвал мой анимационный объект не спрайтом, а объектом блиттера.
Процессор для работы с объектами блиттера в данной книге будет использоваться
в очень незначительной степени, но, тем не менее, я бы хотел рассмотреть его, поскольку
это хороший пример использования поверхностей DirectDraw и полного двумерного ускорения. Начнем со структуры данных для ВОВ.
//Структура объекта блиттера
typedef struct BOB_TYP
{
int state;
// Состояние объекта
int anim_state; // Переменная состояния анимации
int attr;
// Атрибуты объекта
float x,y;
//Позиция вывода изображения
float xvfyv; // Скорость объекта
int width, height;// Ширина и высота
int width_fiU; // Используется внутренне для
//обеспечения ширины поверхности 8*х
int bpp; // Битов на пиксель (необходимо для
// поддержки 8/16-битовых режимов)
int counter_l; // Обобщенные счетчики
int counter_2;
int max_count_l; // Обобщенные пороговые значения
int max_count_2;
int varsl[16]; //Стек из 16 целых чисел
floatvarsF[16]; //Стек из 16 действительных чисел
int curr_frame; // Текущий кадр анимации
int num_frames; // Общее количество кадров анимации
int curr_animation;// Индекс текущей анимации
int anim_counter; // Используется при преобразовании
//времени анимации
intam'm_index; //Индексэлемента анимации
int anim_coLmt_max;// Количество циклов перед анимацией
// Последовательность анимации
int *animations[MAX_80B_. ANIMATIONS];
// Поверхность DirectDraw
LPDIRECTDRAWSURFACE7images[MAX_BOB^ FRAMES];
} BOB, *BOB_PTR;

BOB представляет собой графический объект, представленный одной или несколькими поверхностями DirectDraw (в соответствии с текущими определениями tfdefine —
до 64). Вы можете перемешать, выводить, анимировать ВОВ и настраивать параметры его
174

ЧАСТЬ1. ВВЕДЕНИЕ В ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

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

Animation [0] - (0,1, 2, 3,2,1)
Animation [1] - {1,2,4,4,3, 2,1)

Т

Animation [n] - {0,0,0,10,10)

Анимационные последовательности содержат номера
кадров воспроизводимого растрового изображения

Рис. 3.10. Анимация объекта блиттера
Объекты блиттера поддерживают 8- и 16-битовые изображения и анимационные последовательности, так что вы можете загрузить набор кадров и анимационную последовательность, которая будет воспроизводиться, — что является очень ценным достоинством ВОВ! Кроме того, объекты блиттера сами разбираются, с какой глубиной цвета работают, так что единственные функции, которая имеют разные версии для 8- и 16-битового
режима — это Load_Frame_BOB*() и Draw_BOB*().
Все функции в случае успешного выполнения возвращают TRUE, в противном случае - FALSE.
Прототип функции
intCreate_BOB(
BOELPTR bob, // Указатель на создаваемый объект
intx, inty, // Начальная позиция
int width,
int height // Размер объекта
int numjrames, //Общее количество кадров
intattr,
//Атрибуты
int mem_flags-=0, // Флаг памяти поверхности {0 - VRAM)
USHORT color_key_value-0,// Значение цветового ключа,
// рассматриваемое либо как 8-битовый
// индекс, либо как 16-битовое
// RGB-значение в зависимости от
// параметра Ьрр
int Ьрр-8); // Количество битов на пиксель

ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

175

Назначение
Функция Create_BQB() создает и настраивает объект блиттера. В дополнение к созданию отдельных поверхностей DirectDraw для каждого кадра, она присваивает значения
всем внутренним переменным. Большинство параметров функции очевидно, и пояснения требует только параметр attr.* В табл. 3.1 приведены возможные значения атрибутов,
которые могут объединяться с использованием побитовой операции ИЛИ.
Таблица 3.1. Атрибуты объекта блиттера
Значение

Описание

80B_ATTILSINGLE_FRAME

Создание ВОВ с одним кадром

BOB_ATTR_MULTI_FRAME

Создание ВОВ с несколькими кадрами; при этом анимация ВОВ
представляет собой линейную последовательность кадров О..п

BOB_ATTR_MULTI_ANIM

Создание ВОВ с несколькими кадрами и поддержкой последовательностей анимации

BQB_ATTR_ANIM_ONE_SHOT

Если этот флаг установлен, то последовательность анимации будет воспроизведена однократно (определяется внутренней переменной anim_set). Для повторного воспроизведения требуется
сбросить значение данной переменной

BOB_ATTR_BOUNCE

Этот флаг заставляет ВОВ отражаться при движении от границ
экрана. Этот атрибут имеет значение только при вызове функции
Move_BOB()

BOB_ATTR_WRAPAROUND

Этот флаг заставляет ВОВ при движении и пересечении границы
экрана появляться с другой стороны. Этот атрибут имеет значение только при вызове функции Move_BOB()

Пример использования
// Создание 8-битового объекта с одним кадром в позиции
// (50,100) с размером 96x64:
ВОВ саг;

// Объект-автомобиль
//Создаем объект

if(!Create_BOB(
&саг, 50,100,
// Объект и его позиция
96,64
// Размер объекта
1,
// Количество кадров
BOB_ATTR_SINGLE_FRAME,//Атрибуты
О,
//Флаг памяти
О,
// Цветовой ключ
8))
// Битов на пиксель
{/* error*/}
//Создание 16-битового объекта с 8 кадрами
//и размером 32x32:
BOB ship;
if(!Create_BOB(
&ship, 0,0,
32,32,
176

// Космический корабль
// Создаем объект
// Объект и его позиция
// Размер объекта
ЧАСТЬ!. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ игр

8,
// Количество кадров
BOBJUTR.MULTLFRAME, // Атрибуты
О,
// Флаг памяти
О,
// Цветовой ключ
16))
// Битов на пиксель

{/* error*/}
//Создание 8-битового объекта с поддержкой анимационных
// последовательностей
BOB greeny;//Зеленый человечек
//Создание объекта
if(JCreate_BOB(&greeny,0,0,
32,32,32,B08_ATTR_MULTI_ANIM,0,0,8))
{/* error'/}
Обратите внимание на наличие у последних трех параметров функции значений по
умолчанию. Если значения 0,0,8 вас удовлетворяют, вы можете их не вводить.

Прототип функции
int Destroy_BOB(BOB_PTR bob); // Указатель на уничтожаемый
// объект

Назначение
Функция Destroy_BOB() уничтожает предварительно созданный ВОВ. Глубина цвета
значения не имеет.
Пример использования
// Уничтожение ранее созданного объекта блиттера
Destroy_B О В (& greeny);

Прототип функции
int Draw_BOB(BOB_PTR bob, // Указатель на выводимый объект
LPDIRECTDRAWSURFACE7 dest);// Поверхность назначения
// 16-битовая версия
int Draw_BOB16(BOB_PTR bob,// Указатель на выводимый объект
LPDIRECTDRAWSURFACE7 dest); // Поверхность назначения

Назначение
Функция Draw_BOB*Q выводит объект блиттера на переданной поверхности DirectDraw. ВОВ выводится в текущей позиции из текущего кадра (определяется параметрами анимации). Необходимо убедиться в том, что используется корректная версия
функции, иначе можно получить только половину объекта!
Для нормальной работы функции поверхность назначения должна быть НЕ заблокирована.

Пример использования
// Многокадровый объект блиттера - позиционирование в точке
// (50,50) и вывод первого кадра на вторичную поверхность
BOB ship; // Космический корабль
// Создаем 8-битовый объект
if (!Create_BOB(&ship, ОД
32,32,8,BOB_ATTR_MUITI_FRAME,0})

ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

177

{/«error*/}
// Загрузка объекта
// Установка позиции и кадра объекта
ship.x = 50;
ship.y = 50;
ship.curr_frame = 0;
// Вывод объекта
Draw_BOB(&ship, Ipddsback);

Прототип функции
int Draw_Scaled_BOB(
BOB^PTR bob,
// Указатель на объект
int swidth, int sheight. // Новые размеры
LPDIRECTDRAWSURFACE7 dest); // Поверхность назначения
// 16-битовая версия
intDraw_Scaled_BOB16(
BOB_PTR bob,
// Указатель на объект
int swidth, int sheight, // Новые размеры
LPDIRECTDRAWSURFACE7 dest); // Поверхность назначения

Назначение
Функция Draw_.Scaled_BOB*() работает так же, как и Draw_BOB(), с тем лишь отличием,
что она получает размеры объекта и перед выводом он будет масштабирован. Это очень
удобная функция, позволяющая сделать объект выглядящим как трехмерный, что очень
важно для трехмерных игр.
Пример использования
// Пример вывода корабля размером 128x128, несмотря на то,
// что его исходные размеры— 32x32
Draw_Scated_BOB(&ship, 128,128,lpddsback);

Прототип функции
int Load_Frame_BOB{
BOB_PTR bob,
// Указатель на объект
BITMAP_FILE_PTR bitmap,//Указатель на файл
int frame,
// Номер кадра
int ex, int су,
// Положение ячейки или
// абсолютные координаты
int mode);
// Режим сканирования
// 16-битовая версия
int Load_Frame_BOB16{
BOB_PTR bob,
// Указатель на объект
BITMAP_FILE_PTR bitmap,//Указатель на файл
int frame,
// Номер кадра
int ex, int су,
// Положение ячейки или
// абсолютные координаты
int mode);
// Режим сканирования

Назначение
Функция Load__Frame_BOB*() работает идентично функции Load_Image_Bitmap(), так что
детальное описание данной функции можно посмотреть на стр. 166. Единственное до-

178

ЧАСТЬ!. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

бавление — номер кадра. Например, если вы создали объект, который имеет четыре кадра, вы должны загружать кадры один за другим. Кроме того, вы должны использовать
функцию, соответствующему видеорежиму экрана.
Пример использования
// Загрузка 4 кадров в 16-битовый ВОВ
//из файла в режиме ячеек
BOB ship; // Объект
// Загружаем кадры 0,1,2,3 из позиций
//(0,0), (1,0), (2,0), (3,0)
for (intindex=0; indexetJ>ound_Freq(
inti'd, // Идентификатор звука
int freq); // Новая частота воспроизведения 0-100000

Назначение
Функция DSound_Set_Sound_Freq() изменяет скорость воспроизведения звука. Поскольку все загружаемые звуки должны иметь формат 11 kHz моно, вот как можно удвоить скорость воспроизведения.
Пример использования
DSound_Set_Sound._Freq(fireJdr 22050);
// Скорость воспроизведения можно и уменьшить...
DSound_5et_Sound,_Freq(fireJd, 6000);

Прототип функции
int DSound_$et_Soimd_Pan{
intid, //Идентификатор звука
int pan); // Значение баланса от -10000 до 10000

Назначение
Функция DSound_Set_$ound_Pan() устанавливает относительную интенсивность звука
из правого и левого динамиков. Значение -10000 приводит к звучанию только левого динамика, а 10000 — правого. Для того чтобы громкость динамиков была одинакова, надо
использовать значение 0.
Пример использования
//Звук справа
DSound_SeL$oundJ>an{fire_id, 10000);

API оболочки DirectMusic
API DirectMusic еще проще, чем API DirectSound. Я разработал функции для инициализации DirectMusic, создания всех необходимых СОМ-объектов и загрузки и воспроизведения MIDI-файлов. Вот список основных функциональных возможностей API:
• инициализация и завершение работы DirectMusic;
• загрузка МШЬфайлов с диска;
• воспроизведение MIDI-файла;
• остановка воспроизведения MIDI-файла;
• проверка текущего состояния MIDI-сегмента;
• автоматическое подключение к DirectSound, если он был инициализирован;
• удаление MIDJ-сегмента из памяти.
Если не сказано иное, все функции при успешном завершении возвращают TRUE (1),
и FALSE в противном случае.

194

ЧАСТЬ I. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Прототип функции
int D Musi c_Init( void);
Назначение
Функция DMusic_Init() инициализирует DirectMusic и создает все необходимые СОМобъекты. Вы должны вызвать эту функцию до всех прочих вызовов библиотеки DirectMusic. Кроме того, если вы хотите использовать в программе DirectSound, то вы должны
инициализировать DirectSound до вызова DMusic_Init().
Пример использования
if (!DMusic_J.nit())
{/* Ошибка*/}
Прототип функции
int DMusicJ>hutdown(void);
Назначение
Функция DMusic_Shutdown() завершает работу подсистемы DirectMusic. Она освобождает все СОМ-объекты, и выгружает все ранее загруженные MIDI-сегменты. Эта функция должна быть вызвана в конце вашего приложения, но до завершения работы DirectSound — если, конечно, вы использовали DirectSound в вашей программе.
Пример использования
if (!DMusic__ShutdownO)
{/* Ошибка */}
// Теперь можно завершить работу DirectSound...
Прототип функции
int DMusic__Load__MIDI(char "filename);
Назначение
Функция DMusic_Load_MIDI() загружает MlDI-сегмент в память и выделяет ему память
в массиве midijds[]. Функция возвращает идентификатор загруженного сегмента или -1
в случае неуспешного завершения. Возвращенный идентификатор используется для обращения к MIDI-сегментувдругих функциях API.
Пример использования
// Загружаем файлы
int explodejd = DMusic_Load_MIDI("expLosion.mid");
int weaponjd = DMusic_Load_MIDI("laser.mid");
// Проверка корректности загрузки
if (explodejd ==-11| weapon_id «= -1)
{/* У нас проблемы! */}
Прототип функции
int DMusic_Delete_MIDI(intid);
Назначение
Функция DMusic_Delete_MIDI() удаляет предварительно загруженный сегмент из системы (соответствующий переданному идентификатору).
Пример использования
if(!DMusic_Delete_MIDI(explode_id) ||
!DMusic_Delete_MIDI(weaponJd))
{/* Ошибка */}

ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ игр

195

Прототип функции
int DMusic_Delete_AlLMIDI(void);
Назначение
Функция DMusic_.Oelete_AU_MIDI() удаляет из системы все MJDI-сегменты одним вызовом.
Пример использования
//Удаляем все загруженные сегменты
if (IDMusicJJeleteJULMIDIQ)
{/* Ошибка */}
Прототип функции
intDMusic_Play(intid);
Назначение
Функция DMusic_Ptay() воспроизводит MIDI-сегмент, идентификатор которого передается функции в качестве параметра.
Пример использования
//Загружаем файл
intexplodejd = DMusic_Load_MIDI("explosion.mid");
// Воспроизводим его
if(!DMusic_Play(explode_id))
{/«Ошибка*/}
Прототип функции
int DMusic_Stop(intid);
Назначение
Функция OMusic_Stop() останавливает воспроизведение сегмента, идентификатор которого передан функции в качестве параметра. Если сегмент уже остановлен и не воспроизводится, функция не выполняет ни каких действий.
Пример использования
//Останавливаем воспроизведение
if (!DMusic_Stop(weapon_id))
{/'Ошибка*/}
Прототип функции
int DMusic_Status_MIDI(int id);
Назначение
Функция DMusic_Status() проверяет состояние MIDI-сегмента, идентификатор которого передается функции в качестве параметра. Имеются следующие коды состояния.
#defineMIDI_NULLO //Объект не загружен
#define MIDLLOADED 1 // Объект загружен
fldefine MIDI_PI_AYING 2 // Объект загружен и воспроизводится
tfdefine MIDI_STOPPE() 3 // Объект загружен, но остановлен
Пример использования
//Основной цикл игры
white(l)

-;

if (DMusic_Status(explode_id) — MIDI_STOPPED)
game_state - GAM£_MUSIC^OVER;
}//while

196

ЧАСТЬ I. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Вот и все, что касается API DirectSound и DirectMus'ic. Как я говорил, позже вы увидите примеры использования этих API. В настояший момент я хочу представить вашему
вниманию окончательную версию консоли игры T3D.
В настоящий момент у нас имеется три основных CPPJH модуля, составляющих библиотеку T3D:
• T3DLIB1.CPPJH — DirectDraw и графические алгоритмы;
• T3DLIB2.CPP|H-DirectInput;


T3DLIB3.CPP|H — DirectSound и DirectMusic.

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

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

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

Запуск системы
Мы предполагаем наличие в графическом интерфейсе виртуального компьютера
функции Create_Window() со следующим прототипом.
Create_Window(int width, int height int bit_depth);
Эта функция берет на себя все заботы о настройке графической системы, включая открытие окна необходимого размера с указанной глубиной цвета. В нашей реальной реализации эта функциональность разделена на два разных вызова функции,
что связано с нашим стремлением отделить функциональность Windows и DirectX.
Первая функция представляет собой вызов стандартной функции Windows для создания обычного окна.
А теперь внимание; если вы создаете полноэкранное приложение, то должны использовать флаг WM_POPUP, но если приложение будет оконным, то вам потребуется другой
флаг, наподобие WM_OVERLAPPEDWINDOW. Следовательно, настройка графической системы представляет собой двухступенчатый процесс.
ГЛАВА 3. ВИРТУАЛЬНЫЙ компьютер для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

197

1. Выполняем вызов функции Win32 API Create_Window() с параметрами, соответствующими полноэкранному или оконному приложению. В полноэкранном приложении обыно не используются никакие управляющие элементы.
2. Вызов функции-оболочки DirectX, которая получает дескриптор окна (хранящийся в
глобальной переменной) и все необходимые параметры для завершения работы по
настройке графической системы.
Основная работа выполняется на втором шаге. Именно здесь инициализируется DirectDraw, создаются буферы кадров, генерируется палитра для 8-битового режима, присоединяются отсекатели...
Поскольку мы намерены использовать шаблон консоли игры, наш план состоит в
том, чтобы в функции Win Main () создавалось окно, а затем производился вызов функции
Game_Init(), которая, в свою очередь, вызывает DDrawJnitQ, выполняющую всю черновую
работу. Напомним прототип этой функции.
int DDraw_Init(int width, int height intbpp,
int windowed-0);
Как видите, ничего сложного. В виртуальном компьютере все это достигается при помощи одного вызова Create_Window(); в реальности эта функциональность реализуется
при помощи двух вызовов — один из них создает окно Windows, а второй — инициализирует DirectDraw и присоединяет его к окну.

Глобальные отображения
Первое, чем мы занимаемся в нашем виртуальном графическом интерфейсе, — это
два буфера кадров: видимый и внеэкранный. Мы называем их первичным и вторичным
видеобуферами (рис. 3.12).
Эти буферы являются линейно адресуемыми при любом разрешении и глубине цвета
(при этом шаг памяти для перехода от одной строки к другой может не совпадать с количеством пикселей в строке, так что нам нужна отдельная переменная, отслеживающая
данную величину). Вот какие имена переменных используются для виртуальных буферов
и их шагов памяти.
UCHAR *primary_buffer; // Первичный буфер
int primary_pitch; // Шаг памяти в байтах
UCHAR *secondary_buffer;// Вторичный буфер
int seconday_pitch; // Шаг памяти в байтах
Наша реальная библиотека T3DLIB1.CPP по сути идентична сказанному. В ней для указанных целей используются следующие переменные.
LPDIRECTDRAWSURFACE7 Ipddsprimary; // Первичная поверхность
LPDIRECTDRAWSURFACE7 Ipddsback; // Вторичная поверхность

/

UCHAR *primary_buffer; // Первичный видеобуфер
UCHAR *back_buffer; // Вторичный видеобуфер
int primary_Lpitch; // Шаг памяти
int back_lpitch;
// Шаг памяти
Обратите внимание на две дополнительные переменные, связанные с использованием DirectX. Это указатели на поверхности DirectDraw для первичной и вторичной поверхностей, использующиеся в ряде вызовов функций, так что без них вам не обойтись.
Кроме того, ширина и высота первичного и вторичного буферов всегда совпадают.
Хотя клиентская область первичного буфера может быть окном, а не всем рабочим сто198

ЧАСТЬ!. ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕТРЕХМЕРНЫХИГР

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

"Вторичный буфер

'Первичный буфер

V

Дополнительная
память

КХО)

Строка 0

Строка О* И

Строка п

Строка п

Шаг памяти

Рис. 3. }2. Буферы кадров

256-цветный режим
Хотя вы уже познакомились с функциями для работы с 256-цветными палитрами из
библиотеки T3DLIB1, я хочу еще раз вернуться к этому вопросу. Итак, вот какие функции
предназначены для работы с палитрой.
int Set_Palette_Entn/ (int coloMndex,
LPPALETTEENTRY color);
int Get_Palette_Errtry(int colorjndex,
LPPALETTEENTRY color);
int LoacLPaLette_From_File(char "filename,
LPPALETTEENTRY palette);
int Save_P alette J"o_File( char ^filename,
LPPALETTEENTRY palette);
int 5ave_Palette(LPPALETTEENTRY sav_palette);
intSet_Palette(LPPALETTEENTRYsetjJatette);

Здесь выделены функции, представляющие наибольший интерес. Они используются
для изменения либо одной записи палитры, либо всей палитры как единого целого.
В большинстве случаев изменять по одной записи палитры весьма неэффективно, так что
лучше сделать все необходимые изменения в памяти и изменить всю палитру целиком.
Функциям передается указатель либо на одно запись PALETTEENTRY, либо на массив таких
записей. Чтобы освежить вашу память, я приведу определение этой структуры еще раз.
typedef struct tagPALETTEENTRY {
BYTE peRed; // 8 битов красного канала
BYTE peGreen; // 8 битов зеленого канала
BYTE реВЫе; // 8 битов синего канала
BYTE peFlags; // Управляющие флаги: PC^EXPUCIT для
// цветов Windows, PC_NOCOLLAPSE для всех
// остальных
} PALETTEENTRY;
ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

199

И последнее — если вы выбираете 256-цветный режим, будет загружена палитра
PALDATA2.PAL Я предпочитаю использовать эту палитру как хорошо покрывающую все
цветовое пространство. Конечно, вы всегда можете загрузить палитру с диска, или палитру, связанную с растровым изображением.

Функции блокировки
Взглянем на четыре функции, которые необходимы нам для блокирования и разблокирования первичной и вторичной поверхностей. В интерфейсе виртуального компьютера они выглядят следующим образом.
Lock_Primary(UCHAR ''*primary_buffer, int *primary_pitch);
Unlock_Primary(UCHAR *primaryjjuffer);
Lock_Secondary(UCHAR **secondary_buffer,
int *secondary_pitch);
Unlock_5econdary(UCHAR *secondary ^buffer);
В реальной графической библиотеке необходимая функциональность реализуется
следующими функциями.
UCHAR*ODraw_Lock_Primary_Surface(void);
int DDraw_lMock_Primary_Surface(void);
UCHAR*DDraw_Lock_8ack_Surface(void);
int DDraw_Unlock_Back_Surface{void};
Единственное отличие реальных функций от виртуальных в том, что они работают с
глобальными переменными
UCHAR *primary_bufrer; // Первичный буфер
int primary_pitch; // Шаг памяти буфера
UCHAR *secondary_buffer;// Вторичный буфер
int seconday_pitch; // Шаг памяти буфера
а потому не требуют передачи им каких-либо параметров.

Функции анимации
Последняя функция, о которой я хочу упомянуть — это функция, переключающая первичный и вторичный буферы (или, в некоторых случаях, копирующая содержимое вторичного буфера в первичный). Если вы помните, в интерфейсе виртуального компьютера она
имеет имя Flip_ Display (). В нашей реальной графической библиотеке этим занимается функция
int DDraw_ Flip (void);
Единственное отличие — в имени функции. Выполняемые ею действия в точности те
же, что и в виртуальной модели. Данная функция эффективно справляется со своими
обязанностями как в полноэкранном, так и в оконном режимах.
Теперь, когда мы разобрались с тем, какие реальные функции соответствуют функциям виртуальной модели, можно приступить к главной задаче — созданию окончательного
варианта консоли игры. Я не поклонник больших кусков кода в книге, но код консоли
игры достаточно важен, чтобы привести его полностью.

Консоль игры — окончательный вариант
Теперь вы достаточно подготовлены к тому, чтобы не только увидеть окончательный вариант консоли игры., но и разобраться в нем. Именно этот шаблон используется в дальней200

ЧАСТЫ, ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ игр

шем в качестве основы всех демонстрационных программ книги. Конечно, это не мертвая
схема, и вы можете изменять ее, добавляя собственные возможности, но в принципе консоль содержит всю необходимую для создания полноценной игры функциональность.
Вы можете спросить— что случилось с T3DCONSOLE.CPP|EXE? В предыдущей книге
мною была создана консоль игры с таким именем, но, поскольку данную книгу можно рассматривать как продолжение книги Программирование игр для Windows. Советы профессионала, я предпочел использовать последовательную нумерацию
и добавить 2 в имя файла новой консоли игры — T3DCONSOLE.CPP|EXE.
// T3DCONSOLE2.CPP - шаблон консоли игры. Этот шаблон можно
// использовать для любого приложения— достаточно только
// изменить в нем некоторые параметры, наподобие разрешения
//экрана, указания оконности или полноэкранное™
// приложения, выбора устройств ввода и т.п. В настоящее
// время приложение представляет собой оконное приложение с
// экраном 640x480x16. Таким образом, для работы данного
// приложения вы должны находиться в 16-цветном режиме. Если
// вы хотите получить полноэкранное приложение, вам надо
// только изменить значение WINDOWED_APP на FALSE (0). Для
// использования другой глубины цвета вам надо изменить
// параметры вызова функции DDrawJnitQ в функции
// GameJmtQ
// ВАЖНО!
// При компиляции убедитесь, что в проект включены
// библиотеки DDRAW.LIB, DSOUND.LIB, DINPUT.LIB и WINMM.UB,
// а также модули T3DLIB1.CPP, T3DUB2.CPP и T3DLIB3.CPP, а
// заголовочные файлы T3DLIBl.H,T3DLIB2.H и T3DLIB3.H
// находятся в рабочем каталоге компилятора

tfdefinelNITGUID //Делает доступными все СОМ-интерфейсы.
// Вместо этого в проект можно включить
// библиотеку DXGUID.LIB
«define WIN32_LEAN_AND_MEAN
tfincLude // Поддержка функциональности Windows
^include
((include
tfinclude // Поддержка функциональности C/C++
#intlude
tfinclude

tfinclude
tfinclude
tfinclude
ftinclude
tfinclude
#incLude
tfinclude
tfincLude

ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

201

tfinclude // DirectX
tfi'nclude
^include
^include
tfinclude
^include
#include
tfindude "T3DU61.h" //Библиотека игры
^include "T3DLIB2.h"
«include "T3DLIB3.h"
// Макро
// Интерфейс Windows
tfdefine WINDOW_CLASS_NAME "WINSDCLASS" // Имя класса
«define WINDOW_TITLE "T3D Graphics Console Ver 2.0"
#defineWINDOW_WIDTH
640
// Размер окна
«define WINDOW_HEIGHT 480
Sdefine WINDOW_BPP
16// Глубина цвета (8,16 и т.д.)
// Примечание: в случае оконного приложения глубина цвета
//должна соответствозать системной глубине цвета
^define WINDOWED^APP I // 0 - полноэкранное
// приложение, 1 - оконное
// Прототипы //////У//////////////////////////////////////
// Консоль игры
int Game_Init(void *parms=NULL);
int Game_Shutdown(void *parms=NULLJ;
irtt Game_Mairt(void *parms=NULL);
// Глобальные переменные//////////////////////////////////
HWND maJn__window_hand(e - NULL;//Дескриптор окна
HINSTANCE mainjnstance =• NULL;//Экземпляр
char buffer[256];
//Для вывода текста
// Функции ////////////////////////////////////////////////
LRESULT CALLBACK WindowProcfHWND hwnd,
UINTmsg,
WPARAM wparam,
LPARAM Iparam)
I
// Главный обработчик сообщений
PAINTSTRUCT ps; // Используется WM_PAINT
HOC
hdc; //Дескриптор контекста устройства
// Тип сообщения
switch(msg)
I
caseWM CREATE:
202

ЧАСТЬ!. ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

I // Инициализация
return(O);
} break;
case WM_PAINT:

{

// Начало рисования
hdc •= BeginPaint(hwnd,&ps);

// Конец рисования
EndPaint(hwnd,&ps);
return(O);
} break;
case WM JDESTROY:

{

// Завершение приложения
PostQuitMessage(O);
return (0);
} break;
default: break;
} // switch
// Обработка остальных сообщений
return (DefWindowProc(hwnd, msg, wparam, Iparam));
}//WinProc

// WinMain iUIIIIIiilillilllillliiHIIUIIIIHHIItlllllll
int WINAPI WinMain(HINSTANCE hinstance,
HINSTANCE hprevinstance,
LPSTR Ipcmdline,
int ncmdshow)
WNDCLASSEX wi nclass; // Класс окна
HWND
hwnd; //Дескриптор окна
MSG
msg;
// Сообщение
HDC
hdc; // Контекст графического устройства
// Заполнение структуры класса
wi nclass. cbSize - si zeof (WNDCLASSEX);
winclass.style
= CS^DBLCLKS | CS_OWNDC [
CS_HREDRAW ( CSJ/REDRAW;
winclass.lpfnWndProc = WindowProc;
winclass.cbClsExtra = 0;
winclass.cbWndExtra = 0;
win class, hinstance
•= hinstance;
winclass.hlcon
= LoadIcon(NULU
IDI_APPLICATION);
winclass.hCursor
= LoadCursor(NULL, IDC_ARROW);

ГЛАВА З. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

203

winclass.fibrBackground - (H8RUSH)
GetStockObject(0LACK_BRUSH);
wirrclass.lpszMenuName = NULL;
winclass.lpszClassName = WINDOW_OASS_NAME;
win class. hlconSrn
- LoadlconfNULL,
MISAPPLICATION);
// Регистрация класса окна
if(!RegisterClassEx(&windass))
return(O);
//Создание окна
if (J(hwnd = CreateWindowEx(
NULL,
// Расширенный стиль
WINDOW_CLASS_NAME,// Класс
WINDOWLTTTLE, // Заголовок
(WINDOWED.APP? (WS_OVERLAPPED |
WS.SYSMENU | WS_CAPTION):
(WS.POPUP I WS_WSIBLE)),
0,0,
// Начальные координаты х,у
WINDOW.WIDTH.WINDOW^HEIGHT, // Ширина, высота
NULL, //Дескриптор родителя
NULL, // Дескриптор меню
hinstance,// Экземпляр приложения
NULL))) //Дополнительные параметры
return (0);
// Сохранение в глобальных переменных
main_window_hanclle = hwnd;
main_instance
= hinstance;
// Изменение размеров окна
if (WINDOWEDJXPP)
1
// Изменяем размер окна таким образом, чтобы
// клиентская область имела запрошенный размер
// (учет границ окна и управляющих элементов)
RECTwindow_rect = {0,0,WINDOV\LWIDTH-1,
WINOOW_HEIGHT-1};
// Вызов для изменения window_rect
AdjustWindowRectEx(&window_rect,
GetWindowStyle(main_window__handle),
GetMenu(main_windoiOandle) !-= NULL,
GetWindowExStyle(main_window_handle));
// Сохранение глобальных переменных, необходимых для
// работы DDraw_Flip()
vrindow_ch'ent_xO = -window_rect.[eft;
window_c[ient_yO = -window_rect.top;
// Изменение размеров окна
Move Window(main_window_ handle,
О, // Координатах

204

ЧАСТЬ!. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ игр

О, // Координата у
window_rect. right - window^rect.left, // Ширина
win do wjrect, bottom - window j-ect.top,// Высота
FALSE);

// Вывод окна
ShowWindow(maiivwindow_handl.e, SW_SHOW);
}// if windowed
// Инициализация консоли игры
Game_Init();
// Запрет реакции на CTRL-ALT-DEL, ALT-TAB.
// Закомментируйте эту строку, если она вызывает крах
// системы
SystemParametersInfo(SPCSCREENSAVERRUNNING,TRUE,NULL,0);
// Вход в главный цикл событий
whiLe(l)
'
if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
i
// Проверка на запрос выхода
if (msg. message -=- WM_QUIT)
break;
// Преобразование клавиш
TranslateMessage(&msg);
// Передача сообщения обработчику
DispatchMessage (&msg) ;
// Главная функция игры
Game_Main();
} // while
// Завершение игры и освобождение ресурсов
Game_Shutdown();
// Разрешение реакции на CTRL-ALT-DEL, ALT-TAB.
// Закомментируйте эту строку, если она вызывает крах
// системы
SystemParametersInfo(SPI_SCREENSAVERRUNNING,FALSE,NULL,0);

// Выход в Windows
return(msg.wParam);
} // WinMain
// Функции консоли игры T3D П////////////////////////////
4

intGame_Imt(void parms)
I
// В этой функции выполняется вся инициализация игры

ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

205

// Настройка DirectDraw (измените параметры на
// необходимые вам)
DDraw_Init(WINDOW_ WIDTH, WIN DOW_.HEIGHT,
WINDOW_BPP, WINDOWED_APP);

// Инициализация Pirectlnput
DlnputJnitO;
// Захват клавиатуры
DInput_Imt_Keyboard();
// Здесь могут быть вызовы для захвата других устройств
// вводаХ/Инициализация OirectSound и DirectMusic
DSouncUnitQ;
DMusicJnit();
//Скрытие мыши
if(!WINDOWED_APP)
ShowCursorfFALSE);
// Инициализация генератора случайных чисел
srand(Start_Clock());
//Другой код инициализации...
\
//Успешное завершение
return(l);
}//Gamejnit
I//////////////////////////////////////////////////////////
intGame_Shutdown(void *parmsj
I
//Эта функция завершает работу игры и освобождает все
// захваченные ресурсы
//Освобождение всех захваченных вами ресурсов....
//Завершение работы DirectMusic
DMusic_Delete_AlLMIDI();
DMusic__Sriutdown();
//Завершение работы DirectSound
DSound_Stop_AU_Sounds();
DSound_Delete_AlLSounds();
DSound_Shutdown();
// Освобождение захваченных устройств ввода
DInput_Release_Keyboard();
// Завершение работы Directlnput
DInput_$riutdownQ;
206

ЧАСТЬ I. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

// Завершение работы DirectDraw
DDraw_Shutdovjn();
// Успешное завершение
return(1);
} // Gamejihutdown

IIUIIIiilliHIUUUIilUHHUIIUHliilliiiilliltlHUil
intGame_Main(void *parms)
f
// Эта функция постоянно вызывается в реальном времени и
// в ней располагается вся необходимая функциональность
// игры.
int index; // Переменная цикла
//Запуск тай мера
StartJbckQ;
// Очистка поверхности вывода
DDraw_FiU_Surface{lpddsback, 0);
// Считывание клавиатуры и других устройств ввода
Dlnput_Read_Keyboard();
// Логика игры...

// Переключение поверхностей
DDraw_FLip();
// Синхронизация для достижения скорости 30 fps
Wait_Clock(30);
// Проверка на запрос окончания игры
if (KEY_DOWN(VK_ESCAPE) || keyboard_ state [DIK_ESCAPE])
{
PostMessage(mam_windowJiandle,WM_.DESTROY,0,0);

// Успешное завершение
reUirn(l);
}// Game_Main
///////////У///////////////////////////////////////////////
Как видите, в листинге выделены некоторые особо важные для понимания фрагменты. Сейчас мы рассмотрим их более детально.

Открытие окна консоли игры
Первые выделенные строки кода относятся к разделу макроопределений.
«define WINDOW_WIDTH
^define WINDOW_HEIGHT

640
480

// Размер окна

ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ игр

207

«define WINDOWJJPP
16// Глубина цвета (8,16 и т.д.)
// Примечание: в случае оконного приложения глубина цвета
//должна соответствовать системной глубине цвета
^define WINDOWED^APP
If/ 0 - полноэкранное
// приложение, 1 - оконное
Это весьма важная часть кода, которая управляет размером окна (или размером
экрана), глубиной цвета и видом приложения (оконное или полноэкранное). В настоящий момент это — оконное приложение размером 640x480 и глубиной цвета
16 битов на пиксель. Указанные параметры используются в ряде мест в коде приложения. Самые главные применения указанных параметров — при создании окна в
функции WinMain() и в вызовах DDraw_Init() и GameMainQ. Начнем с рассмотрения
Если вы взглянете на вызов CreateWindow() в функции WinMainQ, то увидите, что в ней
используется тернарный оператор ?:, проверяющий, является ли данное приложение
оконным.
//Создание окна
if (!(hwnd e Create WindowExf
NULL
// Расширенный стиль

WINDOWJ:LASS_NAME,// класс

WINDOWjmE, //Заголовок
(WINDOIVED^APP ? (WS^OVERLAPPED |
WS_SVSMENU | WS_CAPTION) :
(WS^POPUP | WS_ VISIBLE)),
0,0,
// Начальные координаты х,у
WINDOW. WIDTH,WINDOW_HEIGHT, // Ширина, высота
NULL, //Дескриптор родителя
NULL, //Дескриптор меню
hinstance,// Экземпляр приложения
NULL))) //Дополнительные параметры
return (0);
Таким образом, функция создает окно с соответствующими режиму приложения
флагами Windows. Если это оконное приложение, следовательно, у окна должны быть
рамка и ряд управляющих элементов, поэтому дая оконного приложения используются
флаги WS_OVERLAPP!:D | WS.SYSMENU | WS_CAPTION.
С другой стороны, при работе в полноэкранном режиме размер окна равен размеру
поверхности DirectDraw, но без управляющих элементов. Следовательно, должен быть
использован стиль окна WM_POPUP.
Код после создания окна достаточно интересен. Он изменяет размер окна таким образом, чтобы клиентская область имела запрашиваемый размер (а не размер, меньший на
величину размера рамок и управляющих элементов).
Вспомните программирование в Windows — когда вы создаете окно с размером WINDOW_WIDTHxWINDOW_,HEIGHT, это не означает, что клиентская область также имеет размер
WINDOW_WIDTHxWINDOW_HEIGHT. Это — размер всей области окна. Таким образом, если у
окна нет рамок и управляющих элементов, размер клиентской области совпадает с запрошенным размером; если же они есть— размер оказывается несколько меньшим
(рис. 3.13).

208

ЧАСТЬ |, ВВЕДЕНИЕ а ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Строка заголовка имеет
определенный размер
(О, 0)
/

Рамка имеет
определенный размер

Заголовок окна

Высота

Клиентская область меньше общего
размера окна из-за наличия рамки,
заголовка, управляющих элементов и т.п.

{Ширина-1, Высота -1)
Ширина

и

Общий размер окна равен WindowJWidth x Window_Height
Рис. 3.13. Область окна и клиентская область
Для решения проблемы нам надо изменить размер окна таким образом, чтобы размер
клиентской области в точности соответствовал переданным размерам. Вот код, решающий поставленную задачу:
// Изменение размеров окна
if(WINDOWED_APP)
{
// Изменяем размер окна таким образом, чтобы
// клиентская область имела запрошенный размер (учет
// границ окна и управляющих элементов)
RECT window_rect - {0,0,WINDOW_WIDTH-1,
WINDOVLHEIGHT-l};
// Вызов для изменения window._rect
AdjustWindowRectEx(&window_rect,
GetWindowStyLe(main_window__handLe)f
GetMenu(main_windowJiandle) != NULL,
GetWindowExStyletmainjrtrindowJiandLe));
// Сохранение глобальных переменных, необходимых для
// работы DDraw_Flip()
window_client_xO = -window_rect.Left;
window_dient_yO = -windovoect.top;
// Изменение размеров окна
MuveWindow(main_window_handle,
О, // Координата х
О, // Координата у
window_rect.n'ght- windowj'ect.l.eft,// Ширина

ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

209

window_rect bottom - window_recttop,// Высота
FALSE);
// Вывод окна
$howWindow(main_jvindovv_hand{e, $W_$HOW);
}//if windowed
Имеются и другие возможности добиться того же результата, но обычно я поступаю
таким образом, как показано выше. Можно, например, выяснить, какие управляющие
элементы имеет окно, запросить их размеры у Windows, а затем вычислить, каким должен быть размер окна с управляющими элементами. Каким бы образом вы ни поступали, главное, чтобы в результате размер клиентской области окна был равен WINDOW WIDTHxWINDOW HEIGHT.
Если WINOOWED_APP равно 0, окно создается с флагом WMJ>OPUP и не имеет никаких
управляющих элементов, так что нет необходимости в изменении его размеров.
После того, как окно оказывается созданным, а его размеры (при необходимости) изменены, вызывается функция Game_Init(). В эту функцию позже вы добавите всю необходимую инициализацию вашей игры. В настоящий момент эта функция выполняет необходимые в любом случае действия, в частности — инициализацию DirectDraw.
DDraw_Init{WINDOW_ WIDTH, WINDOW__HEIGHTr
WINDOW_BPP, WINDOWED_APP);
Так что, как видите, вам всего лишь достаточно указать необходимые величины в некоторых макроопределениях и не заботиться об остальном.
Вы могли обратить внимание на вызовы System Para meters InfoQ вокруг главного цикла
сообщений. Эти вызовы заставляют Windows думать, что включен режим сохранения
экрана, и игнорировать нажатия . Дело е том, что если вы не обрабатываете нажатие этих клавиш (а вы его не обрабатываете), приложение DirectX может работать некорректно. Вызовы System Pa rameter$Info() позволяют вам забыть об этом вопросе. Если вас интересуют технические подробности — обратитесь к DirectX SDK.
Вкратце— при потере приложением фокуса ввода и получении его обратно вы
должны восстановить все утраченные поверхности, заново захватить устройства
ввода и т.д. — словом, появляется масса причин для головной боли...

Использование и компиляция консоли игры
Рассмотренный нами код в файле T3DCONSOLE2.CPP — всего лишь заготовка, шаблон,
который вы используете для создания реального приложения. Вам просто надо поместить свой код в функции Game_*(). Однако для того, чтобы скомпилировать его, вам нужны следующие файлы;


T3DLIB1.CPP|H — модуль работы с DirectDraw;

• T3DLIB2.CPPJH — модуль работы с Directlnput;
• T3DLIB3.CPP|H — модуль работы с DirectSound и DirectMusic.
Кроме того, в вашем каталоге должны быть файлы


PALDATA1|2,PAL — палитры по умолчанию для 256-цветного режима

Кроме того, вы должны компоновать ваше приложение вместе с
• DDRAW.LIB, DSOUND.LIB, DINPUT.LIB и DINPUT8.UB
210

ЧАСТЬ!, ВВЕДЕНИЕВ ПРОГРАММИРОВАНИЕТРЕХМКРНЫХИГР

Не забудьте установить в вашем компиляторе опцию создания .ЕХЕ-приложения
Win32, добавить все необходимые библиотечные файлы DirectX в список компоновки,
а также указать пути поиска компилятора таким образом, чтобы он мог найти все необходимые заголовочные файлы.
Просто интереса ради я скомпилировал консоль без каких-либо добавлений, получив
файл T3DCONSOLE2.EXE. Это приложение ничего не делает— просто создает оконный
дисплей 640x480x16. Однако для запуска этого приложения вы должны находиться
в 16-битовом режиме.

Е&ШЕ&ВШ

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

Я бы легко мог сделать приложение полноэкранным — для этого понадобилось бы
просто внести маленькое изменение в одну строку.
tfdefine WINDOWED_APP
О// О - полноэкранное
// приложение, 1 - оконное

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

Образцы приложений T3DLIB
Мы добрались почти до конца данной главы. Теперь для полноты изложения я просто
хочу показать вам несколько примеров приложений, созданных на основе консоли игры.
Эти приложения — уже не просто пустое окно; они выполняют реальные действия.
Далее в книге к уже имеющимся частям игрового процессора T3DLIB будут добавлены
новые модули, а пока мы ограничимся только имеющимися в нашем распоряжении возможностями работы с DirectX.

Оконные приложения
В качестве примера оконного приложения (файлы DEMOII3_1.CPP|EXE) я использовал
преобразованную демонстрационную программу из первой книги. Копия экрана этой
программы показана на рис. 3.14. Не забудьте — для того, чтобы эта программа корректно работала, ваш экран должен находиться в 16-битовом режиме!
Эта программа показывает не только работу в 16-битовом режиме, но и загрузку растровых изображений, использование объектов блиттера и звуковых эффектов. Право же,
код этого приложения стоит того, чтобы внимательно его изучить!

Полноэкранное приложение
Написание полноэкранного приложения благодаря наличию разработанной библиотеки
ничем по сути не отличается от написания оконного приложения. Более того, такое приложение даже лучше в том плане, что при его работе не возникает проблем, связанных с совмест-

ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

211

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

Рис. 3.14. Копия экрана оконного приложения, демонстрирующего работу искусственного интеллекта
В качестве основы для демонстрации полноэкранного режима (файлы DEMOII3_2.CPP|EXE)
я также взял программу из первой книги. Копия экрана программы приведена на
рис. 3.15. В этой программе также использованы загрузка изображений, объекты блигтера, а также 256-цветная палитра.

Рис. 3.15. Полноэкранная демонстрационная программа с
8-битовой глубиной цвета

212

ЧАСТЬ!. ВВЕДЕНИЕ в ПРОГРАММИРОВАНИЕ ТРЕХМЕРНЫХ ИГР

Звук и музыка
Работа со звуком и музыкой под управлением DirectSound и DirectMusic не столь
сложна, но и простой ее не назовешь. Однако модуль T3DLIB2.CPP]H делает использование
звука и музыки очень простым, в чем вы можете сами убедиться, познакомившись с демонстрационным приложением DEMOII3_3.CPP|EXE, копия экрана которого показана на
рис. 3.16. Это простое приложение Windows с меню, при помощи которого вы можете
выбрать для воспроизведения MlDI-мелодию, на фоне которой воспроизвести те или
иные звуковые эффекты (также выбираемые при помощи меню).

Рис. 3.16. Демонстрационная программа воспроизведения музыки и звуков

Данная программа основана на несколько урезанном шаблоне консоли игры
(поскольку нас не интересует графика), зато она использует разные ресурсы, такие как
меню и курсор. Соответственно, вы должны добавить в проект следующие файлы:


DEM.OII3_3,RC —файл ресурсов Windows с меню;

• DEMOII3_3RES.H — заголовочный файл с идентификаторами ресурсов;


T3DX.ICO — пиктограмма курсора, используемого в программе.

Эта программа — обычное приложение DirectX, так что вы должны включить в проект библиотечные файлы DirectX. Технически для компиляции данной программы библиотека DDRAW.UB не нужна, но пусть она останется в списке компонуемых файлов — для
безопасности. Поскольку в программе не используется никакая функциональность из
модулей T3DLIB1.CPP и T3DUB2.CPP, включать их в проект не надо.

Работа с устройствами ввода
Последние примеры демонстрируют работу с разными устройствами ввода. Как вы
знаете, в играх в первую очередь используются три следующие устройства ввода:
• клавиатура;
• мышь;
• джойстик.
Сегодня понятие "джойстик" объединяет множество разных устройств — обычно все
устройства, не являющиеся мышью или клавиатурой, классифицируются как джойстик.
Использование библиотеки для работы с устройствами ввода очень простое. Все
функции библиотеки содержатся в модуле T3DLIB2.CPP|H и поддерживают работу с
клавиатурой, мышью и джойстиком. Всего лишь несколькими вызовами функций вы
ГЛАВА 3. ВИРТУАЛЬНЫЙ КОМПЬЮТЕР для ПРОГРАММИРОВАНИЯ ТРЕХМЕРНЫХ ИГР

213

можете инициализировать Directlnput, захватить устройство ввода и считать с него
информацию в основном цикле событий. На прилагаемом компакт-диске имеются
демонстрационные приложения для каждого из перечисленных устройств ввода.
Конечно, в своих программах вы можете использовать все их одновременно — в демонстрационных программах они используются по отдельности исключительно в
методических целях.

Клавиатура
Как и прочие демонстрационные программы, пример приложения, работающего с
клавиатурой, основан на коде из предыдущей книги. Это программа, находящаяся на
прилагаемом компакт-диске под именем DEMOII3_4.CPP|EXE, копия экрана которой показана на рис. 3.17. Здесь при помощи клавиатуры вы можете управлять перемещением
объекта блиттера по экрану. Если вы обратитесь к коду данной программы, то увидите,
что чтение клавиатуры сводится к заполнению массива из 256 байтов, каждый из которых
представляет одну клавишу.
UCHAR keyboard_state[256j;// Таблица состояния клавиатуры

Рис. 3.17. Демонстрационная программа использования
клавиатуры
Для доступа к элементам этой таблицы используются константы DK_* Directlnput,
перечисленные в табл. 3.2. Все, что вы должны сделать, — это проверить, нажата ли
интересующая вас клавиша. После того как вы получили информацию о состоянии
клавиатуры при помощи вызова DInput_Read_Keyboard(), вы можете воспользоваться
проверкой наподобие следующей.
if(( • ' 'F I* Ii f\
J 1
1
: 2 3 4 5 6 7 В 9 10 11 12 ...
Ось абсцисс +х
)

-—

Квадрант III

Квадрант IV

-•-13

f
Ось-у

?. 4. L Декартова система координат

Таблица 4.3. Знаки координат в квадрантах
Квадрант

Знак ж

-

Знаку

;

IV
Многие графические алгоритмы оптимизированы для работы в первом квадранте,
а затем решения для других квадрантов (октантов в трехмерном случае) получаются
в результате использования преобразований симметрии.

Наконец, для того, чтобы указать положение любой точки в двумерной декартовой
системе координат, нам необходимо указать х и у-компоненты координат. Например,
p{5,3J обозначает, что значение координаты х данной точки равно 5, а значение координаты у—3, как показано на рис. 4.2. Все слишком просто для вас? Потерпите немного,..

224

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

-'V
Л
10
ч

В
!

х у

'-.
!

3
2

«-ь-ь-ь

+-

•b'b'H'J—t—I

1 2 3 4 5 6 7 В 9 10 11 12


P«c. 4.2. Положение точки в декартовой системе координат

Двумерные полярные координаты
Следующий вид системы координат, поддерживающей две степени свободы, — полярные координаты. Полярные координаты используются, например, в игре Wolfenstein
и в технологии расчета лучей. Полярные координаты основаны на том, что положение точки на плоскости можно определить не только двумя координатами (х,у), но и направлением и расстоянием от начала координат (на рис. 4.3 показана стандартная полярная система
координат). Как видите, для указания положения точки используются две переменные:
расстояние г от начала координат, или полюса, и направлением, или углом 0. Таким образом, запись р(г,9) означает, что точка р расположена под углом 8 относительно начальной
оси (обычно это х-ось), измеряемым в направлении против часовой стрелки, и на расстоянии г в данном направлении. На рис. 4.4 показаны примеры расположения точек р, (10,30°)
и рл(6,150°) в полярной и декартовой системах координат.

ГЛАВА 4. ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

225

P(r, 0)

X = Г - COS 9

у = г • sin в

Рис. 4.3. Двумерная полярная система координат



, 30')

-13--12 -11-10-9 -8 -7 -6 -5 -4 -3 -2 -1

1 2 3 Л 5 6 7 8 9 10 11 12 13


.



Лис, 44 Пример расположения точек в двумерной полярной системе координат

Преобразования между полярными и декартовыми координатами
Случается (и не так редко), что нужно преобразовать полярные координаты некоторой точки в декартовы (или наоборот). Как выполнить такое преобразование? Это очень
просто — надо лишь немного вспомнить тригонометрию. Взгляните на рис. 4.5, на кото226

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

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

Квадрант I
Р(г, 0} = Р(х, у)

Противолежащая
сторона

х = г - cos в

у = г • sine
Р =

V

Рис. 4.5. Геометрическая интерпретация преобразования между по~
лярными и декартовыми координатами

Уравнение4.1. Преобразование полярных координат р(г.В) в декартовы р(х,у)
х =r-cos6
у = г-sine
Если вы не сильны в тригонометрии, потерпите немного — позже мы расскажем и о
ней, а пока просто примите эти формулы на веру.

Преобразование из декартовых координат в полярные немного хитрее. Проблема
в том, что нам нужен угол, который гипотенуза, проведенная из начала координат в точку
р(х,у) , образует с осью х, и ее длина. И здесь нам на помощь вновь приходит тригонометрия. Для поиска угла мы можем воспользоваться тангенсом, а значение г определяется из теоремы Пифагора.
Уравнение 4. 2. Преобразование декартовых координат р(х,у) в полярные р(г,6)

e~arctg(y/x)

ГЛАВА 4. ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

227

Применим приведенные формулы к точке (3.4J, располагающейся, как видно из рис. 4.6,
в первом квадранте.

43.4}

1

2

3

4

5

6

1-Х

Примечание: это знаменитый
треугольник 3:4:5

V



Л/с. 46. Пример преобразования декартовых координат в полярные
Подставляя значения х=3, у = 4 в уравнение 4.2, получаем:
r = V3'+42 =5
e = arctg(4/3) = 53.1°
Если посмотреть на рис. 4.6, то корректность вычислений становится очевидной.
Если внимательно посмотреть на уравнение 4.2, то можно заметить проблему оси
ординат (х = 0). Другими словами, при углах 0 = 90° и 270° значение тангенса не
определено (точнее, оно равно бесконечности). Таким образом, используя уравнение 4.2, необходимо дополнительно проверять выполнение условия х = 0 (как вы
знаете, угол при этом равен 90°).
В заключение следует сказать, что полярная система координат часто оказывается
очень полезной, так что понимание того, как преобразовывать декартовы координаты в
полярные и обратно, очень важно для успешного решения множества задач, в том числе
задач слежения и навигации.

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

ЧАСТЬ И. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Трехмерные декартовы координаты
Трехмерные декартовы координаты идентичны двумерным координатам, но к ним
добавлена третья z-ось. Таким образом мы получаем систему с тремя степенями свободы,
основанную на трех взаимно ортогональных (перпендикулярных) осях. Следовательно,
чтобы определить местоположение точки р в трехмерном пространстве, нам нужны три
координаты: х, у и z, или, в компактной записи, p(x,y,z). Кроме того, три оси координат
образуют три плоскости: х-у, x-z и y-z (рис. 4.7). Эти плоскости очень важны; каждая из
них делит пространство на два полупространства. Это очень важная концепция, с которой мы встретимся во множестве алгоритмов. А теперь — микрозадача: у нас есть три
плоскости, каждая из которых делит пространство на два полупространства. Сколько
всего различных подпространств образуется в такой трехосной системе? Ответ — восемь!
Итак, в трехмерной системе у нас имеется восемь октантов (рис. 4.8).

Левая система координат
Рис. 4,7. Трехмерная декартова система координат

Однако в такой системе тоже не без проблем — поскольку у оси z есть два возможных
направления (ориентации), возможны две различные трехмерные декартовы системы
координат — левая и правая.
Чтобы понять, откуда произошли эти названия, попробуйте расположить большой,
средний и указательный пальцы руки наподобие осей координат. Если ось х соответствует большому пальцу, а у ~- среднему, то указательный палец указывает направление оси z в соответствующей системе координат: если это левая рука — то в
левой, а если правая — то в правой. Еще один вариант — раскройте ладонь и отставьте большой палец. Если большой палец направлен вдоль оси х, воображаемая
ось у выходит из ладони, то пальцы указывают направление оси z в соответствующей
системе координат.

ГЛАВА 4. ЗАПУГАННЫЙ МИР МАТЕМАТИКИ

229



Октант I





Левая система координат
А*с. 4.8. Октанты в трехмерной системе координат

Левая система координат
Левая система координат (left-handed system, LHS) показана на рис. 4.9. В этой системе координат, если оси х и у представляют собой горизонтальную и вертикальную оси на
экране, то положительное направление оси z идет вглубь экрана.

Правая система координат
Правая система координат (right-handed system, RHS) показана на рис. 4.10. В этой
системе координат, если оси х и у представляют собой горизонтальную и вертикальную
оси на экране, то отрицательное напраштение оси z идет вглубь экрана, а положительное,
соответственно, от экрана к наблюдателю.
На самом деле принципиальных различий между этими системами координат нет,
но при рассмотрении трехмерных виртуальных машин чаще будет использоваться
левая система координат, хотя для лучшего понимания местами может использоваться и правая.

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

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

230

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

и обе находят свое применение. Ближе всего к двумерной полярной системе координат
трехмерная цилиндрическая система координат, поскольку это по сути полярная система
координат, к которой прибавлена третья координата z.

i\

Экран

v

Рис. 4.9. Левая трехмерная система координат
'V

Экран

N f

-v

Рис. 4.10. Правая трехмерная система координат

ГЛАВА 4. ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

231

Стандартная цилиндрическая система координат показана на рис. 4,11. Вы можете
увидеть, что плоскость х-у при z = 0 образует стандартную двумерную полярную систему
координат, а цилиндрические координаты определяют положение точки, сперва указывая ее положение р{г,9) в двумерной системе координат, а затем "поднимая" ее вдоль
оси z на необходимую высоту, являющуюся третьей координатой точки. Обратите внимание, что здесь использована правая система координат, и она повернута иначе, чем ранее — для лучшей наглядности.

р(х,у)
X = Г•COS 0

у = г - sin в

Правая система координат
Рис. 4,11. Цилиндрическая система координат

Преобразование между декартовыми и цилиндрическими
координатами
Преобразование между декартовыми и цилиндрическими координатами тривиально:
достаточно использовать двумерное преобразование и положить z = z .
Уравнение 4.3. Преобразование цилиндрических координат p(r,e,z)
в декартовы p(x.y.z)
х =r-cos0
у = г - sin 9
г =z
При преобразовании декартовых координат в полярные мы вновь используем двумерное преобразование и полагаем z = z .
Здесь, как и в двумерных координатах, имеется проблема, связанная с углами
9 = 90° и 8 = 270°.

232

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Уравнение 4.4. Преобразование декартовых координат р(х,у,г)
в цилиндрические p(r,e,z)
Дано: х 1 +у" =г
г = ^/х^+у2
6 = arctg(y/x)

Цилиндрические координаты удобны для решения ряда задач, например, таких как
управление камерой в "стрелялках" для отображения среды.

Трехмерные сферические координаты
Трехмерные сферические координаты немного сложнее других систем координат.
В них положение точки определяется двумя углами и расстоянием от начала системы координат (рис. 4.12). Соответственно, координаты точки записываются как р(р,ф,9), где
р — расстояние от начала координат до точки р, ф — угол, который образует с положительным направлением оси z отрезок от начала координат к точке р (здесь используется
правая система координат), и в — угол, образуемый проекцией отрезка от начала координат до точки р на плоскость ху (так же, как и в случае двумерных полярных координат;
диапазон допустимых значений 0< в < 2л).



х = г - cos Q = р • sin ф • cos 9

у = г • sin в = р • sin ф - sin в

Рис. 4.12. Сферическая система координат

При выводе сферических координат мы вновь использовали свои знания о двумерных
полярных координатах. Рассмотрим теперь преобразование координат из сферических
в декартовы и обратно.
Сначала рассмотрим внимательнее рис. 4.12, поскольку описать решение при помощи
одного только текста весьма затруднительно. Для преобразования сферических координат в
ГЛАВА 4. ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

233

декартовы мы можем использовать двухшаговый процесс. Сначала мы проецируем отрезок
ог начала координат к интересующей нас точке на плоскость х-у, после чего задача сводится к уже решенной двумерной. Затем мы возвращаемся к поиску z через (р,ф,6) .
Уравнение 4.5. Преобразование трехмерных сферических координат
в декартовы p(x,y,z)
Из проекции отрезка между началом координат и точкойполучаем
r=p-sin
z=p-cos

Рассматривая проекцию на плоскости х-у, получим:
х = г • cos 9
у = г -sinG
Подстановкой г в формулы для х, у получаем окончательное решение:
х = p-sin-cos6
y = p-sin-sin6

Уравнение 4. 6. Преобразование трехмерных декартовых координат р(х,у./}
асферические р(р,ф,6)
:

г

:

:

2

2

2

Дано: х + у + г = р и, аналогично, х + у = г . Тогда

8 = arctg(y/x).

Значение ф можно найти из соотношения г = р • sin ф , откуда
ф = агс8ш(г/р) .
Можно также воспользоваться соотношением г=р-со$ф, тогда

Тригонометрия
Основная часть тригонометрии основана на анализе прямоугольного треугольника
(рис. 4. 1 3). Прямоугольный треугольник, как и любой другой, имеет три внутренних угла
и три стороны. За точку отсчета обычно берется один из острых углов, который мы будем
считать базовым. Сторона, противоположная базовому углу, называется противолежащей, сторона, соединяющая базовый угол с прямым — прилежащей, а сторона, соединяющая эти две — гипотенузой. Хотя все это звучит тривиально, постарайтесь не впасть в
ошибку недооценки важности треугольников— я видел не так уж много графических
алгоритмов, где бы так или иначе не использовались треугольники. Поэтому вы должны
знать треугольники даже лучше своих пяти пальцев.

234

ЧАСТЬ II, ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Р(х, у)

< Противолежащая
сторона = у = В

-1-х

_У_ Е
~г,—
г С
. ,

Прилежащая сторона

Гипотенуза

„». п X А _ Противолежащая сторона
cose — —~ — ———— —————- • - — ————•
С
Гипотенуза

Рис. 4.13. Прямоугольный треугольник

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

Таблица 4.4. Радианы и градусы
Угол в градусах

Угол в радианах

360°



180°

тс

57.296°

ЗбО°/2я = 1

1.0°

271/360 = 0.0175

Факт 1. Полная окружность составляет 360°, или 2л радиан. Запомните— компьютерные функции sin() и cos() Pаботают с радианами, а не с градусами^. Некоторые часто
встречающиеся значения показаны в табл. 4.4.
Факт 2. Сумма внутреннихуглов треугольника 6,+ 6, + 63 = тс радиан (или 180°).
Факт 3. Обращаясь еще раз к рис. 4.13: сторона прямоугольного треугольника, противоположная углу 9,, называется противолежащей, ниже ее — прилежащей (обе они являются катетами), а длинная сторона называется гипотенузой.
Факт 4. Сумма квадратов длин катетов равна квадрату длины гипотенузы. Этот факт называется теоремой Пифагора. В обозначениях рис. 4.13 можно записать: А1 +В~ =С ! . Таким образом, зная две стороны прямоугольного треугольника, всегда можно найти третью,
ГЛАВА4. ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

235

Факт 5. Есть три основные тригонометрические функции — синус, косинус и тангенс.
Их определения представлены ниже.
cos 6 =

Гипотенуза

=

_ (область определения Ойб£2л,областьзначений [-1,1]).
г

Противолежащая сторона у . с
= -^~ - - - = — (область
определения 0£вх + Ь очевидно: m - x + ( - l ) y + b = Q. Несколько сложнее записать в общем виде уравнение прямой,
проходящей через две точки tx 0 ,y 0 ) и (х м у,)? Можно например, вычислить значение наклона т, подставить его в уравнение 4.20 и привести его к общему виду. Проведите все необходимые вычисления самостоятельно и сравните их с конечным результатом:
Уо

х

о

Для каждого конкретного типа вычислений в большей степени может подходить тот
или иной вид уравнения прямой. Однако при вычислении точки пересечения двух прямых лучше всего воспользоваться общим видом, поскольку при этом мы получаем знакомую нам систему уравнений, которую мы уже умеем решать:
а, • х + bt • у = с,
а, • х + Ь, • у = С2

Можно решить данную систему уравнений, преобразовав ее к матричному виду
А - Х = В ,где
"а, Ц
а: Ь,
Как мы уже знаем, решение данного матричного уравнения находится как X = А~1 -В,
а обратную матрицу можно вычислить с помощью уравнения 4.17. Я думаю, читатели
вполне смогут сделать это самостоятельно.
лор

Если определитель матрицы А равен 0, единственного решения системы уравнений
не существует.

Прямые в трехмерном пространстве
Представление прямых в трехмерном пространстве — несколько более сложная задача. Кроме того, оно откровенно уродливо! На практике в большинстве случаев будет использоваться параметрическое представление прямых, но о параметрическом представлении я бы хотел поговорить отдельно, в целом, без конкретной привязки к прямым.
Сейчас же я всего лишь намерен показать, каким образом можно вывести представление
трехмерной прямой. Взгляните на рис. 4.31, на котором изображена прямая в трехмерном
пространстве, которая проходит через точки P0(x0,y0,z0) и рДхрУ^г,) , и единичный
вектор v = (a,b,c) от точки р„ к точке р,. Вот как при этом выглядит параметрическое
уравнение, определяющее прямую.
Уравнение 4. 22. Параметрическое задание трехмерной прямой

При изменении t от 0 до |v , вектор р проходит от точки р„ к точке р,. Это представление уже должно быть вам знакомо, так как мы сталкивались с его аналогом в
двумерном случае.
ГЛАВА4. ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

265

'v

Примечание: v аналогичен "наклону"
в трехмерном пространстве

У

Рис. 4.31. Прямая в трехмерном пространстве
А вот как выглядит явный вид прямой в трехмерном пространстве.
Уравнение 4.23. Явное симметричное задание трехмерной прямой

По сути, здесь не одно, а целых три уравнения, но из-за связи их в единое целое вам понадобятся только два из них. Здесь a, b и с — компоненты единичного вектора v . Основная проблема при работе с таким видом уравнения прямой — в его неуклюжести. Рассмотрим, например, уравнение прямой, проходящей через точки р0 (1,2,3) и р, (5,6,7) . Тогда

= {0.577,0.577,0.577} * (a, b,c).
Подсташшя полученные значения в уравнение 4.23 , получаем
0.577 0.577 0.577 '
Умножая на 0.577 для устранения знаменателя и разделяя уравнения, получим систему
уравнений
(Х-1ИУ-2),
(y-2) = (z-3),

или, после упрощения;
х-у = -1,
y-z— I.
Согласитесь, что параметрическое представление выглядит значительно красивее...
266

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

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

Рис. 4.32. Бесконечная плоскость
Итак, какие же основные свойства плоскостей?


Все плоскости представляют собой бесконечные "листы" в трехмерном пространстве,

• Плоскости делят пространство на два полупространства (что очень важно для различных алгоритмов разделения пространства и определения столкновений).
Имеется ряд методов для генерации уравнений плоскости. Мы взглянем на проблему с
геометрической точки зрения и попробуем вывести уравнение плоскости самостоятельно.
Умение выводить что-либо самостоятельно вообще очень помогает в жизни, Впрочем, я отвлекаюсь. Итак, взгляните на рис. 4.33, где изображена плоскость с нормалью к ней
п = (а,Ь,с) иточками p 0 (\ 0 ,y 0 ,z 0 ) и р(х,у,г) на плоскости. Мы представляем себе эту плоскость, но как записать ее уравнение? Заметим, что поскольку точки р(1 и р лежат на плоскости,
вектор pji тоже принадлежит плоскости, а значит, он перпендикулярен вектору нормали п,
какой бы ни была точка р. Это уже что-то, так как мы знаем, что если два вектора перпендикулярны, то их скалярное произведение равно 0., т.е. п-р 0 р = 0 , а это и есть уравнение плоскости! Конечно, его лучше привести в более понятную форму.

ГЛАВА 4. ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

267

Многоугольник лежит
в плоскости Р

Примечание: нормаль к плоскости
и многоугольник параллельны

Рис. 4.33. Плоскость, построенная на базе многоугольника
Уравнение 4.24. Определение плоскости через точку на ней и вектор нормали
{а,Ь,с}- (х - х„,у -у{,,2 - z0) = 0, или, выполняя умножение:

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

Уравнение в общем виде удобно в том случае, когда требуется вычислить пересечение
двух или большего количества плоскостей. В этом случае получается система двух (или
иного числа) уравнений относительно х, у и z; понятно, что проще всего такая система
получается при использовании уравнений в обшем виде.

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

268

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Отрицательное
полупространство

Р(х, у, z)
• Проверяемая точка

•-Х

Нормаль
n =

Положительное
полупространство
Плоскость
-V

Отрицательное
полупространство

ть x-z Положительное
iu полупространство



Рис. 4,34. Плоскость и полупространства
Для проверки принадлежности точки тому или иному полупространству предположим, что наше уравнение плоскости записано в следующем виде:
Все, что нам надо, — это подставить координаты интересующей нас точки в данное
уравнение и вычислить значение hs.
• Если hs = 0 , точка лежит на плоскости.
:

'

• Если hs > 0 , точка находится в положительном полупространстве.

1

• Если hs < 0 , точка находится в отрицательном полупространстве.
Это один из способов проверки принадлежности точки многоугольнику. В качестве
конкретного примера рассмотрим плоскость xz, нормаль к которой равна (ОД 0} , а точка,
принадлежащая плоскости, — (0,0,0) . Тогда
Как видите, координаты х и z точки не имеют никакого значения (как и следовало
ожидать), и все определяется координатой у проверяемой точки. Так, например, для точки р(10,ЩЮ} , которая определенно находится в положительном полупространстве, значение hs равно 10 (положительное значение подтверждает сделанный вывод о размещении точки в положительном полупространстве). Понятно, что в общем случае вам не удастся обойтись проверкой значения одной координаты точки.
ГЛАВА 4. ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

269

Пересечение плоскости и трехмерной прямой
Рассмотрим очередную задачу, связанную с плоскостью, а именно — задачу поиска
пересечения прямой и плоскости в трехмерном пространстве. Это очень важная задача в
трехмерных играх, возникающая, например, при определении столкновений, вычислении отсечений и т.п. В качестве примера мы рассмотрим плоскость и прямую, изображенные на рис. 4.35. Для простоты я выбрал плоскость xz, так что нормаль равна (0,],0),
а точка, принадлежащая плоскости,— (0,0,0). Прямая, пересечение которой с плоскостью нас интересует, проходит через точки р, (4,4,4) и р, (5,-5,-4). Теперь посмотрим,
как нам найти координаты точки пересечения д.

Рис. 4.35. Пересечение прямой и плоскости
Уравнение плоскости, определяемой точкой и вектором нормали, имеет вид:
Подставляя сюда вектор нормали и точку р(|! мы получим уравнение плоскости в виде
у = 0 . Уравнение трехмерной прямой в общем виде —

Переменные (а,Ь,с) представляют единичный вектор2 в направлении прямой, и могут
быть вычислены следующим образом:
2
Заметим, что в силу однородности уравнения прямой здесь может использоваться вектор
произвольной длины (мы всегда можем умножить все часта уравнения на одно и то же значение).
Таким образом, из описанных далее вычислений можно смело убрать вычисление единичного
вектора и в качестве величин а, Ь и с использовать компоненты вектора v, т.е. значения 1, -9 и -8.
Вы можете убедиться самостоятельно, что такое изменение никак не повлияет на поиск точки пересечения, но при этом несколько уменьшит трудоемкость ее вычисления. — Прим. ред.

270

ЧАСТЬ N. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Соответственно, единичный вектор вычисляется как
v = v/|у| = (1,-9.-8)/ Jl2+(-9)4(-8)2 = (0.08,-0.74, -0.66).
Подставляя точку р, и вектор v в уравнение прямой, получаем
х - 4 _ у--4 _ z-4
0.08 ~ -0.74 ~ -0.66 '
Вторым уравнением в интересующей нас системе является уравнение плоскости, которое, как мы уже выяснили, представляет собой простое равенство у ~0. Итак, в конечном итоге наша система уравнений имеет следующий вид.
(х-4)/0.08 = -(у-4)/ОЛ4,
-(y-4)/0.74 = -(z~4)/0.66,
у=0.
Решая эту простейшую систему уравнений, мы находим точку пересечения
р{ (4.43,0,0.43), которая, судя по рисунку, найдена верно.

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

Двумерные и трехмерные параметрические прямые
Взгляните на рис. 4.36. На нем показаны две точки на плоскости ху: Р 0 (х 0 ,у 0 ) и р^х,^).
Мы знаем, что вектор между этими точками v = p0p, =(*, ~х 0 ,у, -у0) ,т.е. если мы добавим v
к р(1! то получим точку р,. Вопрос ставится так — каким образом записать это параметрически,
с тем, чтобы при изменении некоторого значения t в некотором закрытом интервале [a,b]
прямая пробегала от точки р„ к точке р, вдоль вектора v?
Ответ достаточно прост — множество точек (х,у) между ри и р, описывается следующим образом.
Уравнение 4.26. Уравнение в обобщенном параметрическом виде

Единственная проблема при этом заключается в том, что интервал t в действител ьности
неизвестен. Этот интервал можно вычислить, но здесь есть несколько тонких моментов,
Уравнение 4.26 представляет собой обобщенное параметрическое уравнение прямой, которая простирается в бесконечность, так что значение t может изменяться в пределах от -°°
до +« , и в этом смысле нам не надо особо беспокоиться о величине v. Однако зачастую нам
требуется не вся прямая, а конкретный отрезок, а здесь главное отличие в том, что интервал
значений параметра должен быть точно определен — и здесь есть два основных способа определения значения v и, соответственно, интервала значений t.

ГЛАВА 4. ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

271

Рт

. V

v = ;=,

•Ч-Н-4

Р отслеживает перемещение
по прямой из точки РО в точку PI
при изменении t


Рис. 4.36. Параметрическое представление прямой

Параметрическое представление отрезка с помощью
стандартного вектора направления
В предыдущем разделе мы определили v = p0p, =(x,-x0,y, ~у0); таким образом,
длина вектора v в точности равна расстоянию от точки р„ до точки р,, как показано
на рис. 4.36. Поэтому вполне очевидно, что значения t при этом должны находиться
в интервале [0,1].
Запись интервала с использованием квадратных скобок означает закрытый интервал, т.е; включающий конечные точки, в то время как круглые скобки означают открытый интервал, в который указанные конечные точки не входят, Так, запись (ОЛ)
означает все действительные числа от 0 до 1, включая 1, но исключая 0.
Убедимся в этом, рассматривая конечные точки интервала. При t = 0 формула
P = Po+v ' t превращается в р = р 0 , как и ожидалось. При t = l мы получаем
P = p0 + v - l = p0 + (p1-p0) = p,, что также совершенно правильно. Следовательно, интервал [0,1] описывает интересующий нас отрезок от точки р„ до точки р,. Мы можем использовать и другие значения t, но при этом получающиеся точки будут лежать на прямой, проходящей через точки р„ и р,, но вне пределов указанного отрезка. Это очень важное свойство, использующееся при решении параметрических систем уравнений. Если
получаемое при этом значение t выходит за рамки интервала, мы знаем, что полученная в
результате точка находится вне отрезка, что может оказаться крайне важной деталью при
решении задач трехмерной геометрии.
Использование интервала [0,1] очень удобно с разных точек зрения, но давайте рассмотрим еще одно представление — с использованием нормализованного вектора v.

272

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Параметрическое представление отрезка с помощью
единичного вектора направления
Следующий вариант параметрического представления прямой аналогичен предыдущему, но с одним небольшим изменением: вместо вектора v мы используем нормализованный вектор v ; p = p0 + v - t . Здесь v = v/|v|, где, как и ранее, v = p 0 p L ~{xi-x 0 ,y,-y 0 ).
При этом встает вопрос о том, какой же интервал значений t определяет отрезок между точками р(| и р„ как показано на рис. 4.37.
*



Длине "|v

Prfxo.yo)

< - » — I — 1 4 1 1 'l-t— l-Ц-'-Ь-f—t-

"*•*•!

t

I' I "I

I' 14

> I

I >

Параметрическое представление
использует единичный вектор;
при этом интервал, определяющий
отрезок, равен [0, |v|J



Рис. 4.37. Интервал, определяющий отрезок между точками p0upt
Если вы внимательно изучили предыдущий материал, то можете дать верный ответ не
задумываясь; это интервал [o,|vQ . Убедитесь сами.
При t = 0 имеем p = p0 + v - 0 = p 0 . Когда t = |v|, мы получаем

т.е. именно то, что и требовалось. В конечном счете между этими двумя представлениями
нет особой разницы. Аналогично можно разработать множество вариантов параметрических представлений— например, представление, когда отрезку соответствует интервал
[-1,1] • Попробуйте вывести такую формулу самостоятельно.

Параметрическое представление трехмерных прямых
Трехмерное параметрическое представление прямых абсолютно идентично двумерной версии, надо только учесть, что теперь и точки, и вектор v имеют по три компонента.
Векторное же уравнение остается тем же; р = р0 + v • t , где v = p, - рг .
Обратите внимание, насколько прост оказался переход от двумерного к трехмерному
пространству и насколько он был сложен при явном использовании координат для представления прямых. Вот почему работа с прямыми в трехмерном игровом процессоре
ГЛАВА 4. ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

273

осуществляется с использованием параметрического представления — в явном виде
очень сложно осуществить все необходимые вычисления.

Вычисление пересечения параметризованных прямых
Рассмотрим задачу движения двух кораблей по плоскости, показанную на рис. 4,38.
Как видно из рисунка;
• корабль 1 движется из точки pfl в точку р,;
# корабль 2 движется из точки р2 я точку р3.


х

P»(x, У) = (3,6) + -tj

Система уравнений,
которую необходимо
решить

Я«с. 4.38. Движение кораблей по плоскости

Мы хотим определить, пересекутся ли их траектории. Заметьте, мы ничего не говорим о том, столкнутся ли сами корабли, поскольку время не входит в нашу задачу.
Пока что нас интересует только возможность пересечения их путей. Первое решение, которое приходит на ум, состоит в том, чтобы построить соответствующие прямые и найти точку их пересечения — но как после этого вы определите, пересекаются ли интересующие нас отрезки"? Вот почему так удобно параметрическое представление — в этом случае мы определяем значения параметров t в точке пересечения.
Однако если t находится за пределами интервала, определяющего отрезок, мы знаем,
что отрезки не пересекаются (рис. 4.39).
Начнем с построения двух параметрических прямых с параметрами в интервале [0,1]
для простоты решения. Заметим, что если интервалы будут разными, то мы окажемся
в глупой ситуации сравнения литров с метрами.
274

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

-IK

Отрезки Р0Р, и Р2Р3 не пересекаются,
несмотря на пересечение прямых,
на которых они лежат



Рис. 4.39. Пересечение прямых не означает пересечение отрезков
Итак, приступим к математической записи задачи.
• Корабль 1 движется из точки р0(1,1) в точку р,(8,5), вектор направления
v,={8-l,5-l) = {7,4).
• Корабль 2 движется из точки р2(3(6) в точку рэ(8,3), вектор направления
v,={8-3,3-6} = c -\~
__ •&•_ d- i f Iv d a •c + b •d b ' - a - .
c : +d'
c'+d1
c 2 +d 2
Может, выглядит и несколько громоздко, но зато частное двух комплексных чисел
приведено к желаемому виду суммы действительной и мнимой частей a + b - i .

Мультипликативный обратный элемент
Последнее математическое свойство, необходимое для того, чтобы множество комплексных чисел было замкнутым, — наличие мультипликативного обратного элемента, т.е.
комплексного числа, которое, будучи умноженным на данное, даст в результате 1. После
небольшого анализа становится очевидным способ поиска мультипликативного обратного:
надо просто разделить 1 на исходное комплексное число при помощи обычной операции
деления и привести полученное значение к стандартному виду комплексного числа умножением числителя и знаменателя на комплексное сопряженное знаменателя:
z =a +b>i ,
1
1
а - Ь -\ a- b i
а
Ь
z a + b ' i a + b - i a-b-i a 2 + b a 2 +b 2 a3 +b2
Нетрудно убедиться, что умножение полученного числа на исходное дает, как и требовалось, l + 0-i .

Комплексные числа как векторы
Сейчас я хочу еще раз обратиться к представлению комплексных чисел в виде векторов в двумерной плоскости. Обратитесь еще раз к рис. 4.41, где комплексное число представлено в виде точки в декартовых координатах (х-координата точки равна действительной части комплексного числа, а у-координата — мнимой). Таким образом, комплексное
число z = a + b - i можно представить в виде вектора z = a-(l,0) + b-(0,i) или, более компактно, z = (a,b). На рис. 4.42 показано рассмотренное нами представление комплексного числа в виде вектора.
Такое представление позволяет нам преобразовывать комплексное число как вектор,
получая при этом совершенно корректные результаты. Кроме того, такое представление
обеспечивает визуализацию абстрактной математической записи комплексных чисел,
облегчая понимание соотношений, которые иначе можно представить только в чисто математической форме.
ГЛАВА 4. ЗАПУТАННЫЙ мир МАТЕМАТИКИ

281

Рис. 4.42. Представление комплексных, чисел в виде векторов

Норма комплексного числа
Зачастую возникает задача поиска "длины" комплексного числа. Ясно, что с точки
зрения чистой математики, особого смысла в этом нет, но при использовании представления комплексного числа в виде вектора понятие длины приобретает простой и понятный смысл. Длина, или норма комплексного числа, вычисляется следующим образом.
Уравнение 4.28. Норма комплексного числа

Заметим также, что если воспользоваться комплексно сопряженным числом, то норму
можно записать следующим эквивалентным образом:
г
2 = V/—
Z-2 .

Гиперкомплексные числа
Кватернионы представляют собой не что иное, как гиперкомплексные числа. Термин
"гиперкомплексные" означает, что это комплексные числа с более чем одной мнимой
компонентой. В случае кватернионов имеется одна действительная часть и три мнимых.
Кватернионы можно записать многими различными способами, но обычно в общем
виде они записываются следующим образом.
Уравнение 4.29. Запись кватернионов
ИЛИ

282

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Здесь q, — действительные числа, a i, j и k — мнимые числа, которые образуют векторный
базис кватерниона. Значение q,, — действительное, не имеющее мнимого коэффициента.
Мнимый базис (i.j,k) имеет ряд интересных свойств. Его можно рассматривать как
множество трехмерных взаимно перпендикулярных единичных векторов в мнимой системе координат, как показано на рис. 4.43, Мнимый базис обладает следующим интересным свойством.
Уравнение 4.30. Произведения элементов базиса кватернионов

Рис. 4.43. Гиперкомплексное трехмерное пространство
Элементы мнимого базиса (i,j,k) выделены полужирным шрифтом, поскольку в силу их дуальной природы их можно рассматривать и как переменные, л как векторы.

Часть " = i - j - k " , наверное, вызывает определенное удивление, но она совершенно
корректна. Более того, из уравнения 4.30 не так сложно вывести другие свойства умножения элементов базиса кватернионов (попробуйте сделать это самостоятельно):
L =j k=-k-j,

Теперь я хочу оговорить несколько соглашений, которые будут использоваться
при записи кватернионов. Зачастую кватернионы записывают с использованием
строчных символов, разбивая кватернионы на действительную и мнимую части,
причем мнимая часть записывается в соответствии с соглашениями, принятыми для
записи векторов;

nie q v =q,

ГЛАВА 4, ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

283

Итак, i + 4 - j + 5 ' k , либо в чисто векторном
виде: а = (-1,3,4,5). В зависимости от того, что именно мы делаем с кватернионами,
в книге могут использоваться оба варианта записи, но я думаю, что это не вызовет у вас
никаких трудностей.
Большим достоинством кватернионов (как и любых гиперкомплексных числовых
систем) является то, что сложение, умножение, обращение и т.п. математические операции выполняются так же, как и в обычной теории комплексных чисел, просто с большим
количеством элементов. Следовательно, можно считать, что вы уже знаете, как работать с
кватернионами, и можно переходить к следующим темам. Конечно, я пошутил, и мы все
же бегло ознакомимся с тем, как выполняются те или иные математические действия с
кватернионами.

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

Например:

В векторной форме это сложение будет выглядеть следующим образом:
(3, 4, 5, 6} + (-5, 2, 2, -3} = (-2, 6, 7, 3) .
Пока все просто, но далее нам придется все время помнить, что у нас целые три мнимые компоненты. Чтобы не забывать об этом, мы будем использовать запись с раздельным указанием действительной и мнимой частей.

Аддитивный обратный элемент и аддитивный нулевой элемент
Аддитивным обратным произвольного кватерниона q является число, которое, будучи
добавленным к данному, даст аддитивный нулевой элемент, который в случае кватернионов представляет собой величину
Очевидно, что аддитивным
-q = _q0 - q v , поскольку

обратным

кватерниона

q

является

кватернион

k.

Умножение кватернионов
Сложение и вычитание кватернионов выполняется просто и компактно, чего не скажешь об умножении. Оно просто, но далеко не компактно — поскольку требуется вы284

ЧАСТЬ II, ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

полнить попарное умножение всех слагаемых кватерниона с учетом правил умножения
элементов базиса. Давайте попробуем и посмотрим, что у нас получится...
Итак, у нас есть сомножители

Их произведение равно

Если у вас наметанный глаз, возможно, вы заметите определенную структуру в полученном выражении— где-то вам попадется скалярное произведение, где-то— векторное... Можно воспользоваться уравнением 4.30 и упростить полученное выражение, но
вы сделаете это и сами, а я сразу покажу вам окончательный вариант представления умножения кватернионов.
Уравнение 4.31 . Произведение кватернионов
Для кватернионов

их произведение равно

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

Поскольку скалярное произведение векторов является скаляром, а векторное — вектором, первый член (р„ -q c -(pv -q v )) представляет собой действительную часть произведения г„, а член (р0 q v + q0 pv + pv x q v } — мнимую (векторную) часть произведения гу.
Заметим, что мультипликативным единичным элементом (аналогом "1" в мире обычных чисел) у кватернионов является кватернион q t = l+(M + 0- j+0-k , поскольку, как
легко убедиться, 4 • QI - QL • Q = Ч •

Сопряженный кватернион
Сопряженный кватернион q* вычисляется так же, как и сопряженное комплексное
число — изменением знака мнимой части кватерниона qv.
Уравнение 4.32. Вычисление комплексно сопряженного кватерниона
Для данного кватерниона q = q n +qj -i+q, -j + q^k =q 0 +q v комплексно сопряженный
кватернион вычисляется путем изменения знака мнимой части:

Давайте теперь найдем произведение кватерниона на сопряженный с использованием
уравнения 4. 31.
ГЛАВА 4. ЗАПУТАННЫЙ мир МАТЕМАТИКИ

285

Уравнение 4.33. Произведение кватерниона на сопряженный кватернион

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

Норма кватерниона
Норма кватерниона вычисляется точно так же, как и норма комплексного числа.
Уравнение 4.34. Норма кватерниона

ч=
Заметим, что (q*-q) = (q-q*). Произведение кватерниона на сопряженный кватернион не зависит от порядка сомножителей, что в общем случае произведения кватернионов неверно: q - p / p - q . Кроме того, очевидно выполнение тождества

Мультипликативный обратный элемент
Мультипликативный обратный кватернион имеет особое значение для нас, поскольку
позже он будет использоваться для упрощения поворота кватернионов. Итак, мультипликативный обратный кватернион q~' представляет собой кватернион, обладающий следующим свойством:

q q '=q ' q = i.

Давайте умножим каждую часть уравнения на комплексно сопряженный кватернион q':
(q.q-').q' = (cosx =

= cosx-sinx+cosx-sinx = 2-cosx-sinx.
Правда, все очень просто?
Давайте теперь займемся поиском производной функций типа f (x) = a - s i n ( b > x + c ) .
Начнем мы с вывода одного общего правила, а именно— с рассмотрения функции
a - f (х) , где а — константа. Рассматривая это выражение как произведение двух функций
и учитывая, что производная от константы равна нулю, получим

Возвращаясь к функции f (х) = а • sin (Ъ • х + с) , мы можем сразу сказать, что
f'(x) = a -- sin(b- х + с) .
QX

Теперь нам надо найти производную функции f (x) = sin(b-x + c). Это очень просто сделать, если заметить, что данную функцию можно рассматривать как составную, где
f(g) = sing , a g(x) = b - x + c . Тогда

= cos ( g) • b = b - cos (b - x + c) .

Вспоминая о коэффициенте а, мы получаем окончательный ответ:
(a-sin(b-x+c)) = a - b - c o s ( b - x + c ) ,
На этом мы закончим наш маленький экскурс в дифференцирование. После знакомства с материалом данного раздела вы должны уметь дифференцировать полиномы,
тригонометрические функции, произведения и частные функций, а также составные
функции. Этого в основном вполне достаточно для программирования игр.

Интегралы
Следующий вопрос, который мы (тоже вкратце) обсудим, — интегрирование. Операция интегрирования обратна к операции дифференцирования. Если мы дифференциру-

ГЛАВА 4. ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

303

ем функцию f (х) и получаем новую функцию f'(x), то как мы можем получить исходную функцию f(x), если у нас есть функция Г(х)? Этот процесс поиска исходной
функции (она называется первообразной) по ее производной и называется интегрированием. Имеется ряд способов математического определения интегрирования; наряду с ними есть геометрические и физические интерпретации интегрирования, и потому я постараюсь познакомить вас с различными точками зрения на интегрирование.
Первый взгляд на интегрирование — через его определение: для данной функции
f(x) найти функцию F(x) такую, что Ffx) = f(x). Рассмотрим, например, функцию
f (х) = 2 • х . Производная от какой функции равна 2-х? Поскольку мы только что занимались производными, вероятно, вы можете сами дать ответ, что это функция F(x) = x 2 ,
/
поскольку
—х2=2-х2-'=2.х.
dx

Как видите, интегрирование ~ просто обратное действие по сравнению с дифференцированием. По аналогии с только что разобранным примером очевидно, что для
f(x) = a х" мы получим функцию F(x) = (a/(n-f-I)}-x" + '+c. Самостоятельно убедитесь
в том, что выполняется условие F(x) = f (х). О том, откуда взялся член с, я скажу буквально через несколько строк.
Теперь, когда вы на конкретном примере увидели, что такое интегрирование, поговорим о нем как о новом операторе.

Определение интегрирования
Интегрированием называется процесс
(записывается с использованием знака J):

вычисления

первообразной

функции

Jf(x)dx = F(x) + c.

Здесь f (х) — функция, для которой мы ишем первообразную, F(x) — искомая первообразная функция, а с — константа интегрирования. Давайте посмотрим, откуда взялась эта константа.
Возьмем, например, уже рассмотренную нами функцию 2-х, первообразной для которой, как мы уже выяснили, является функция х 2 , так как (х3) = 2 • х , Давайте теперь попробуем найти производную функции х' +1, У вас достаточно знаний, чтобы самостоятельно определить, что (х ! +1) = 2 - х . Так какая из функций— х~ или х 2 +1 —является
первообразной для функции 2 • х ? Ведь производные обеих функций одинаковы.
Дело в том, что первообразная функция определяется с точностью до константы, т.е. на
самом деле существует не одна первообразная функция, производная которой равна исходной, а целое семейство функций. Если к любой первообразной функции прибавить константу, получится другая первообразная функция, ничуть не худшая первой. Именно это свойство первообразных и отражено членом "с" в приведенной ранее формуле.

Геометрическая интерпретация интегрирования
Как я говорил ранее, дифференцирование представляет собой вычисление изменения, или наклона функции, т.е. когда вы дифференцируете функцию f (х), вы находите

304

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

новую функцию f'(x), которая представляет собой не что иное, как значение наклона
исходной функции для произвольного х. Интегрирование имеет подобную геометрическую интерпретацию в обратном смысле. Взгляните на рис. 4.52. На нем вы видите
:
обычную параболическую функцию f(x) = x на отрезке [0,5]. Область под кривой заштрихована. Площадь этой области и вычисляется путем интегрирования. Давайте убе2
димся в этом. Итак, у нас имеется функция f (х) = х . Интегрируя ее (вы уже знаете, как
искать интеграл от степенной функции), мы получим
3

f|x~dx
2А =—+с.
х

Рис. 4.52. Геометрическая интерпретация интегрирования

Чему же равно с в данном случае? Здесь нам надо вспомнить о том, что мы рассматриваем исходную функцию на отрезке [0,5]. До сих пор мы рассматривали неопределенные
интегралы, которые давали нам семейство первообразных функций с неизвестной константой с, но определенные интегралы позволяют вычислить ее точное значение. Определенный интеграл на отрезке [а,Ь] записывается следующим образом.
Уравнение 4.43. Определенный интеграл
Jf(x)dx = F(b)-F(a).
Отрезок [а,Ь] представляет собой пределы интегрирования. Вычисление определенного интеграна производится следующим образом.
Шаг 1. Находим первообразную функцию, принимая значение с равным какой-то
конкретной величине (здесь для простоты мы используем с = 0):
2
F(x)=
V }
J fx dx = — + с = —.
3
3
Шаг 2. Подставляем в первообразную значения пределов интегрирования и вычитаем первообразную от нижнего предела интегрирования из первообразной от верхнего предела:

ГЛАВА 4. ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

305

.

Это и есть значение определенного интеграла, которое в данном случае дает нам площадь под графиком функции f (х)-х 1 . У нас нет другого способа точно вычислить указанную площадь, так что мы оценим ее приближенно, как показано на рис. 4.53 — с помощью аппроксимации треугольником. Площадь треугольника равна половине произведения его основания на высоту, что в данном случае составляет 0.5-5'25 = 62.5 и, как
видите, весьма близко к точному значению, вычисленному путем интегрирования.

Аппроксимация
треугольником

D

. , 62.5-44.66 ппы
Ошибка= —-—— = 39%
44.66

Рис. 4.53. Аппроксимация площади под кривой треугольником
(От редактора. Пожалуй, с методической точки зрения, более поучительно было бы
рассмотреть треугольник, образованный прямой у = а • х , осью х и прямой у = b . Понятно, что площадь такого прямоугольного треугольника равна, с одной стороны, половине
произведения длин катетов, т.е. S = (l/2)-b-a-b = a-b 2 /2, а с другой — определенному интегралу от функции ,а-х на отрезке [0,b]. Первообразная функции а - х — а-х 2 /2,так
что площадь, определяемая величиной определенного интеграла, равна
н

_ jгa .

_ р/1Л

pfn^
flfel
j -_JLJ11
_ _JLff!.
_ -.__

Как видите, площади, вычисленные при помощи интеграла и геометрическим путем, совпадают, что и является иллюстрацией геометрической интерпретации интегрирования.)

306

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Формулы интегрирования
Теперь у нас есть математическое определение интегрирования, его геометрическая
интерпретация, и мы знаем, как находить неопределенные и определенные интегралы.
Чтобы помочь вам в повседневной работе, в табл. 4.8 представлены первообразные некоторых распространенных функций.
Таблица 4,8. Формулы интегрирования
Интеграл

Первообразная

[а • u"du

a-u n+1 /(n + l) + c

г du
•U-u + b

ln(a-u + b)/a

[sin u du

-cosu+c

[cos u du

sinu + c

[sec u du

tgu + c

[cosec2 u du

-ctgu + c

[secu- tgudu

secu + с

[cosec u • ctg u du

-cosecu+c

[e"udu

e""/a + c

[inudu

ulnu-u+c

1

Суммирование и физическая интерпретация интегрирования
На самом деле, интегрирование — это попросту суммирование значений функции от
одной точки до другой. Это очень полезная операция, абсолютно необходимая при разработке физических моделей. Почему? Потому что очень часто они выглядят аналогично
следующей модели:
x(t) = x 0 + v - t ,
v(t) = v 0 +a-t.
Здесь ускорение а представляет собой константу. Мы знаем, что производная координаты x(t) равна скорости, т.е. x'(t) = v(t). Это значит, что если у нас есть формула для зависимости скорости от времени, то ее первообразная должна быть равна x(t). Интегрируя обе части уравнения, находим:
Jx'(t)dt = Jv(t)dt.
Левая часть этого уравнения по определению равна х (t), поскольку первообразная от
производной равна исходной функции. Таким образом, получаем:
x(tbjv(l)dt.
ГЛАВА 4. ЗАПУТАННЫЙ МИР МАТЕМАТИКИ

307

Поскольку мы знаем, что v (t) = v0 + а • t , мы можем подставить эту формулу в предыдущую и выполнить интегрирование:
3

x(t) = J(v 0 +a-t)dt*=v 0 -t+a-t /2 + c.
Теперь требуется найти значение с. Для этого заметим, что в начальный момент времени t = 0 значения координаты и скорости имеют следующие значения: х(0) = х„ и
v (t) = v 0 . Подставляя нулевое значение времени в предыдущую формулу, получаем:
Итак, мы получаем окончательную формулу для равноускоренного движения:
Главное в рассмотренном выводе формулы — то, что для нахождения положения объекта мы должны проинтегрировать его скорость. Позже, когда мы перейдем к рассмотрению физических моделей, я покажу вам, каким образом можно выполнять численное интегрирование вместо символьного.

Резюме
Мне никогда не приходилось писать математические книги, но программирование
игр заставляет делать это. Я постарался подать материал не "вглубь", а "вширь", чтобы
вы могли познакомиться с как можно большим количеством тем — векторами, матрицами, комплексными числами, кватернионами и прочим.
Однако из-за столь большого разнообразия тем преподнести их сколь- нибудь глубоко
просто невозможно, так что я настойчиво рекомендую вам приобрести учебники по линейной алгебре, дифференциальному исчислению, физике и всерьез изучить эти вопросы. Игры становятся все сложнее и сложнее и по сути близки к переходу в новую категорию — имитационного моделирования. Лично я думаю, что лет через пять используемые
в играх физические модели будут абсолютно реалистичны — по крайней мере, в пределах
классической физики. А это все — математика, математика и еще раз математика! Так
что если вы хотите достичь успехов в качестве программиста игр— не ограничивайтесь
лишь этой книгой. Без серьезных учебников и прочных знаний вам просто не обойтись.

308

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

ГЛАВА 5

Создание
математической
библиотеки
В этой главе...


Краткий обзор математической библиотеки

310

• Типы и структуры данных

313



324

Математические константы

• Макросы и встраиваемые функции

327

• Прототипы

336



341

API математической библиотеки

» Работа математического сопроцессора


Замечания по использованию математической
библиотеки

» Замечания об оптимизации

390
406
407

Первоначально я намеревался заняться созданием математической библиотеки в рамках предыдущей главы, но обычная для этой
книги проблема объема материала заставила меня изменить мои
планы и посвятить математической библиотеке отдельную главу.
Бэтой библиотеке тем или иным способом должно быть реализовано в виде кода все то, о чем шла речь в главе 4, "Запутанный мир
математики". В большинстве случаев рассматривающиеся здесь
функции — это просто перевод на язык программирования C/C++
того, что было сказано в предыдущей главе языком математики,
Вот схема данной главы:

• краткий обзор математической библиотеки;
• математические константы;
• структуры данных;
• макросы и встраиваемые функции;
• прототипы;
• глобальные переменные;
• системы координат;


матрицы;

• двумерные и трехмерные прямые;
• трехмерные плоскости;
• кватернионы;
• математика с фиксированной точкой;
• математика с плавающей точкой.

Краткий обзор математической библиотеки
Данная глава является непосредственным продолжением предыдущей и в каком-то
смысле ее частью — здесь на практике реализуется все то, о чем в предыдущей главе говорилось сугубо теоретически. Разрабатываемая здесь библиотека являетсячастью общей
библиотеки, предназначенной для разработки трехмерных игр, которую я буду постоянно обновлять, добавлять в нее новые функции и совершенствовать ее код. Отметим, что
это не самая эффективная математическая библиотека на свете, поскольку я не собираюсь заниматься ее оптимизацией на низком уровне.
Однако тем, где это было возможно, я использовал алгоритмическую оптимизацию
высокого уровня — так, например, при вычислении определителя матрицы я использую
выделение общих сомножителей вместо вычисления "в лоб", что приводит к несколько
меньшему количеству выполняющихся умножений. Кроме того, в каждой игре интенсивно используется своя математика, так что вы сможете позаботиться о дополнительной
оптимизации узких мест при написании собственных игр. Ну, а если вы этого и не сделаете — то не сильно беспокойтесь: наличие процессоров с тактовой частотой свыше
1 ГГц делает эту проблему не такой уж острой.

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


T3DLIB4.CPP — исходный текст на C++;



T3DLIB4.H — заголовочный файл.

На рис. 5.1 представлена схема, поясняющая взаимоотношения математической
библиотеки с остальными частями нашей системы. Обратите внимание на то, что код
в файле математической библиотеки T3DLIB4.CPP зависит от некоторых структур из
файла T3DLIB1.CPP; таким образом, мы должны всегда компоновать эти файлы вместе
(впрочем, файл T3DUB1.CPP все равно всегда входит в состав игры, так что данное требование по сути ограничением не является). Кроме того, для компиляции файла
310

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

T3DLIB1.CPP требуется файл DDRAW.H, так что последний включен с помощью директивы ((include и в файл T30LIB4.CPP. Таким образом, если вы хотите использовать рассматриваемую здесь математическую библиотеку отдельно, где-то в другом месте,
то вам придется перенести ряд функций и макроопределний из файлов T3DLIB1.CPP
и T3DLIB1.H в файлы T3DLIB4.CPP и T3DLIB4.H.
Примечание; должны компоноваться совместно

GAME.CPP/H
• Зависит от
T3QLIB1.CPP/H

T3DLIB2.CPP/H

Необязательно: не требуется
для чисто графических или
математических программ

Рис. 5.1. Связь математической библиотеки с другими частями игры

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

Соглашения об именах
Здесь, как и в предыдущей моей книге — Программирование игр для Windows. Советы
профессионала— при обновлении какой-либо функции я добавляю к ее имени число,
указывающее, что хотя функция и выполняет те же действия, что и исходная, но делает
это несколько иначе. Например, функция Multiple() позже в книге может превратиться

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

311

в Multip(e2(). Именование функций — задача вообще тяжелая: имя должно быть достаточно понятным, говорящим о том, какая именно функциональность скрыта за ним, но
при этом коротким, так как вряд ли вы захотите набирать имена из пары десятков символов. Для того, чтобы имена были разумны, я буду использовать в них, наряду с описанием выполняемых действий имена используемых структур, разделяя отдельные части имени символами подчеркивания (_). Например, функция для умножения вектора на матрицу будет выглядеть следующим образом:
void Mat_MuLVECTOR3D_3X3(VECTOR3D_PTR va,
MATRIX3X3_PTR mb,
VECTOR3D_PTRvprod);

Такое объявление функции вполне понятно говорит о том, для чего она предназначена и с какими типами данных работает. Подобные соглашения об именовании функций,
кстати, используются в OpenGL. Согласитесь, что это гораздо приятнее, чем пытаться
понять, что же делает функция MMV3D33(...).
Кроме того, имена классов и структур у меня всегда используют строчные буквы, так
же как и макроопределения tfdefine. Однако ряд встраиваемых функций используют
те же соглашения об именах, что и обычные функции.

Обработка ошибок
Обработка ошибок в этой библиотеке предельно проста — потому что ее просто нет!
Эта книга предназначена для программистов выше среднего уровня, так что если вам
нужна обработка ошибок— позаботьтесь о ней сами. В большинстве случаев функции
проверяют наличие самых тупых ошибок, типа деления на ноль, указывая об их наличии
возвращаемым значением. Если функция возвращает значение, то обычно 1 (true) означает успешное ее выполнение, а 0 (false) — наличие проблем.

Заключительное слово о C++
Годами занимаясь программированием игр и книгами, посвященными этому вопросу, я пришел к окончательному выводу, который вряд ли кому-то удастся поколебать:
C++ отличный язык, и я использую его для работы, но для того, чтобы учить других, —
он не годится. Вы не задумывались, почему в университетах до сих пор часто учат студентов языкам Pascal, Modula II, Ada? Потому что это хорошо структурированные языки,
в которых каждая строка кода имеет единственное точно определенное значение. Разбираться же в программе на C++ без предварительного изучения введенных автором классов — дело весьма сложное. Стоит ли при изучении алгоритма усложнять задачу наличием классов и перегруженных операторов?
Таким образом, поскольку данная книга призвана учить, я буду в основном использовать язык программирования С. Могу только сказать, что вам никто не мешает самостоятельно перевести математическую библиотеку (или любую другую) на C++ для использования в своей повседневной работе.
Что касается лично меня, то я считаю, что работать с векторами или матрицами при
помощи перегруженных операторов очень удобно, но совершенно неверно с методической точки зрения. Да и кроме того, при каждом обновлении библиотеки с использованием классов C++ придется вводить новые классы, порождая их из старых или перегружая их. При обновлении функции MuLti'pLeQ я лучше ограничусь тем, что напишу функцию Multiple2().
312

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Типы и структуры данных
Начнем рассмотрение математической библиотеки с используемых ею типов и структур данных. Большинство типов и структур библиотеки — новые, но в ней используются
ряд структур, которые находятся в файлах T3DLJB1.CPP|H, так что в описаниях вам будут
время от времени встречаться комментарии, гласящие, что данные типы и структуры
можно найти в T3DLIB1.H.
Математическая библиотека поддерживает множество типов данных, включая точки,
векторы, матрицы, кватернионы и т.п. Мы рассмотрим каждый класс данных отдельно.

Векторы и точки
Математическая библиотека поддерживает двух-, трех- и четырехмерные точки и векторы, где четырехмерность означает однородные координаты (x,y,z,w). Однако
в большинстве случаев четвертая координата нами использоваться не будет (она всегда
равна 1). Поскольку вектора и точки в действительности обозначают одно и то же, для их
хранения используются одни и те же структуры. Кроме того, в структурах я решил использовать объединения, что позволяет использовать различные соглашения об именах.
Например, иногда удобно обращаться к координатам трехмерной точки как к р.х, р.у, p.z,
а иногда— как к элементам массива: р.М[0], р.М[1], р.М[2] или каким-либо иным образом. Этот способ используется мною во всех структурах данных. Вот как это выглядит
в реальном коде.
// Двумерный вектор или точка, без w //////////////////////
typedef struct VECTOR2D_TYP
{
union
i
float M[2]; // Массив для хранения
// Явно указанные имена
struct

1

float х,у;
}; // struct
}; // union
} VECTOR2D, PQINT2D, *VECTOR2D_PTR, *POINT2D_PTR;
// Трехмерный вектор или точка, без w/////////////////////
typedef struct VECTOR30_TYP
{

union
{

float M[3]; // Массив для хранения
// Явно указанные имена
struct
I

float x,y,z;
}; // struct
}; // union
} VECTOR3D, POINT3D, *VECTOR3D_PTR, *POINT3D_PTR;

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

313

// Четырехмерный однородный вектор или точка (с w} ////////
typedef struct VECTOR4DJTYP
(

union
I

float M [4]; // Массив для хранения
// Явно указанные имена
struct
{

float x,y,z,w;
}; // struct
};// union
} VECTOR4D, POINT4D, *VECTOR4D_PTR, *POINT4D_PTR;
Вот несколько старых определений вершин из файла T30LIB1.H.
//Двумерная вершина
typedef struct VERTEX2DI_TYP
{

intx,y;// Вершина
} VERTEX2DI, *VERTEX2DI_PTR;
//Двумерная вершина
typedef struct VERTEX2DFJYP
float x,y;// Вершина
} VERTEX2DF, *VERTEX2DF_PTR;
Позже в этой главе, при работе со структурами данных, содержащими и представляющими трехмерные объекты, мы создадим представления для трехмерных вершин.

Параметризованные прямые
В большинстве случаев алгоритмы используют параметрические представления прямых, создаваемые "на лету", в соответствии с постановкой конкретной задачи. Однако
мне кажется, что стоит иметь набор функций для работы с явно заданными параметрическими прямыми. В соответствии с этим я разработал набор функций для двух- и трехмерных параметрических прямых. Вот структура данных, использующаяся при работе
с двумерной пряной.
//Двумерная параметрическая прямая///////////////////////
typedef struct PARMLINE2D_TYP
I

POINT2D pO; // Начальная точка
POINT2D pi; // Конечная точка
VECTOR2D v; // Вектор направления

} PARMLINE2D, *PARMIJNE2D_PTR;
На рис. 5.2 точка р() представляет начальную точку, р( — конечную точку, a v — вектор
от точки р(] к точке р,. Из аналогии с двумерной прямой легко понять, что при работе с
трехмерной прямой используется следующая структура.
//Трехмерная параметрическая прямая//////////////////////
typedef struct PARMUNE3DJYP
I

31 4

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

POINT3D рО; // Начальная точка
POINT30 pi; // Конечная точка
VECTOR3D v; // Вектор направления
// M-|pO->pl|
} PARMLINE3D, *PARMLINE3DJ>TR;

v =Ро -> р, = р, -Ро =

Т-У
Рис. 5.2, Двумерная параметрическая прямая

Единичный',
вектор
РО =< *О.УО.

v = Р, - Ро =

•г

V

Рис. 5.3. Трехмерная параметрическая прямая

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

315

Трехмерная параметрическая прямая показана на рис. 5.3, Все обозначения на нем те
же, что и на рис. 5.2. Обратите внимание, что на рисунке показана левая система координат. Имеет ли это значение? Никакого! Прямая есть прямая, независимо от того, в какой
системе координат она рассматривается. Пока вы используете прямую в той же системе
координат, в которой определяете ее, неприятностей не предвидится.

Трехмерные плоскости
С трехмерными плоскостями мы сталкиваемся в основном при работе с многоугольниками. Однако я считаю, что в библиотеке надо иметь тип трехмерной плоскости — для
работы алгоритмов разбиения, отсечения, определения столкновений и т.п. Имеется несколько способов представления плоскости в трехмерном пространстве, и я выбираю
способ определения плоскости при помоши вектора нормали и точки. Однако это не означает, что вы не можете использовать в своих разработках другие виды представлений,
например, в общем виде ах + by + cz + d - 0.
Итак, определение плоскости при помощи точки и нормали естественным образом
приводит к следующей структуре данных.
// Трехмерная плоскость ////////////////////////////////У//
typedef struct PLANE3D_TYP
{
POINT3D рО; // Точка на плоскости
VECTOR3D л; // Нормальный (необязательно
//единичный) вектор
} PLANE3D, *PLANE3D_PTR;
На рис. 5.4 изображено рассмотренное представление плоскости в левой системе координат. Здесь р„ — точка на плоскости, a n — нормальный вектор. Заметим, что мы не
делаем предположений о единичности вектора п.

Примечание: плоскость
продлевается
до бесконечности

Нормальный вектор
РО "

Код функции plot()

hi парный код

Main.obj

Компоновщик
Бинарный код

Вызова функции нет

Рис. 5.10, Использование обычных и встраиваемых функций

Итак, главный вывод— при написании математической библиотеки функции
в большинстве своем должны быть встраиваемыми, что позволит обойтись без осуществления вызова функций. К. чему это приводит, рассмотрим на примере вызова следующей
функции во внутреннем цикле.
ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

327

void PLot_Pixel(UCHAR *video_buffer, int mempitch,
intx, inty, int color)
{
// Вывод пикселя
video_buffer[x-+y*mempitch] - color;
} //Plot-Pixel
Если я осушествляю вывод в своей программе, генерируется вызов функции. Например, рассмотрим следующий код.
void mainQ
I
UCHAR'videoJjuffer(UCHAR*)malf,oc(SCREEN_WIDTH-SCREEN_HEIGHT);
for (inty-0; у < SCREEN_HEIGHT; y++)
for (int x»0; x < SCREEN JYIDTH; x++)
Plot_Pixel(video_buffer, SCREEN_WIDTH, xry,5);
} //main
Хотя все в этой функции выглядит вполне корректно, давайте посмотрим, как выглядит дизассемблированный код, сгенерированный Visual C++ 6.0.
_mainPRQCNEAR
;52:{

push ebp
mov ebp, esp
subesp, 12

;OOOOOOOcH

;53:UCHAR*video_buffer(UCHAR *)matloc(SCREEN_WIDTH*SCREEN_HEIGHT);

push 307200
; 0004bOOOH
call_maltoc
add esp, 4
mov DWORD PTR _videojjuffer$[ebp], eax
;54:
;55:for(inty=0;yixel(videoJ>uffer, SCREEN_WIDTH, x,y,5);
push 5
mov edx, DWORD PTR л$[еЬр]
push edx
moveax, DWORD PTR_x$43917[abp]

push eax
push 640
;00000280H
mov ecx, DWORD PTR _video_buffer$[ebp]
push ecx
call ?PlotJ>ixel@@YAXPAEHHHH@Z ; PLot.Pixel
addesp, 20
;00000014H
jmp SHORT SL43919
$143920:
jmp SHORT SL43915
SL43916:
;58:
; 59 : } // end main
mov esp, ebp
pop ebp
retO
_main EN DP

Здесь полужирным шрифтом выделен весь код, необходимый для вызова функции
Plot^PixelQ — настройка кадра стека, вызов, сброс стека после вызова. Здесь не учтен код
самой функции, который выглядит следующим образом.
?PLot_Pixel@@YAXPAEHHHH@Z PROC NEAR; Plot_Pixel
;45:{

push ebp
mov ebp, esp
; 46 : // plots a pixel
; 47 : video_buffer[x+y*mempitch] •= color;
mov eax, DWORD PTR _y$[ebp]
imul eax, DWORD PTR _mempitch$[ebp]
mov ecx, DWORD PTR _x$[ebp]
add ecx, eax
mov edx, DWORD PTR _video^buffer$[ebp]
mov at BYTE PTR_color$[ebp]

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

329

mov BYTE PTR [edx+ecx], al
; 48 : } // PtotLPixeL

. •
popebp
reto
?Plot_Pixel@@YAXPA£HHHH@Z ENDP

; Plot__PixeL

Как видите, вызов Plot_Pixel() делает массу работы, аналогичной работе при вызове
функции! Однако, если сделать эту функцию встраиваемой, все это окажется излишним.
В этом легко убедиться, объявив функцию встраиваемой и дизассемблировав сгенерированный код.
_mainPROCNEAR

;52:{
push ebp
mov ebp, esp
sub esp, 16

; 00000010H

;53:UCHAR*videoJ>uffer(UCHAR*)malloc(SCREENJtfIDTH*SCREEN_HEIGHT);
push 307200; 0004ЫЭООН
call_maLloc
add esp, 4
mov DWORD PTR _video_buffer$[ebp], eax

;54:
; 55 : for (inty-0; у < SCREEN_HEIGHT; y++)
mov DWORD PTR _y$[ebp], 0
jmp SHORT $143914
SL43915:
mov eax, DWORD PTR _y$[ebp]
add eax, 1
mov DWORD PTR _y$[ebpj, eax
SL43914:
cmp DWORD PTR _y$[ebp], 480
jge SHORT $143916

; OOOOOleOH

; 56 : for {int x-0; x < SCREEN.WIDTH; х-н-)
mov DWORD PTR __x$43917[ebp], 0
jmp SHORT $143918
$L43919:
movecx, DWORD PTR_x$43917[ebp]
add ecx, 1
mov DWORD PTR _x$43917[ebp], ecx
$143918:
cmp DWORD PTR _x$43917[ebp], 640 ; 00000280H
jge SHORT $143920

330

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

; 57 : Plot_Pixel(video_buffer, SCREEN_WIDTH, x,y,5);
mov DWORD PTR $T43941[ebp], 5
movedx, DWORD PTR_y*[ebp]
imul edx, 640
; 00000280H
moveax, DWORD PTR^xS43917[ebp]
add eax, edx
mov ecx, DWORD PTR _video_buffer$[ebp]
movdU BYTE PTR $TA3941[ebp]
mov BYTE PTR [ecx+eax], dl
jmp SHORT $143919
$L43920:
jmp SHORT $143915
$143916:

;58:
; 59:}//main

Здесь полужирным шрифтом выделен встроенный вызов функции Plot_Pixel() — как
видите, ситуация сушественно улучшилась. Код стал несколько большим, но зато быстрее примерно в два раза.
Думаю, что я наглядно показал преимущества использования встраиваемых функций — естественно, когда эти функции имеют небольшой размер. Однако, к сожалению,
код встраиваемых функций должен быть доступен компилятору при их использовании,
так что единственное возможное место их размещения — заголовочные файлы. Однако
это не слишком удобно для программиста, поскольку приводит к быстрому росту файла
(и, как следствие, к сложности работы с ним и замедлению работы компилятора).
В результате программист вынужден искать золотую средину и решать, какие функции
будут встраиваемыми и размещены в .Н-файле, а какие — обычными и размещены в „СРРфайле, опираясь на интуицию и здравый смысл. Я размещаю в .Н-файле только макроопределения и простейшие встраиваемые функции. Вы можете разместить их по-своему.

Утилиты общего назначения и преобразование величин
Все эти макросы располагаются в файле T30LIB1.H.
// Вычисление минимального и максимального
// значения двух выражений
«define MIN(a, b) (((a) < (b)) ? (а): (b))
«define MAX(a, b) (((a) > (b)) ? (b): (a))
// Обмен значениями
«define SWAP(a,b,t) {t-a; a=b; b-t;}
// Некоторые математические макросы
«define DEG_TO_RAD(ang) ((ang)* PI/180.0)
«define RAD_TO_DEG(rads) ((rads)*180.0/PI)
«define RAND_RANGE{x,y) ((x) + (rand()%((y)-(x)+l)))

Работа каждого макроса очевидна, так что пояснять тут ничего не требуется.

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

331

Точки и векторы
Как я упоминал ранее в данной главе, формат данных для представления точек и векторов одинаков, и поэтому группирую макросы для точек и векторов вместе. Кроме того,
ряд макросов представляют собой встраиваемые функции, что обеспечивает выполнение
проверки типов компилятором. Да и к тому же, многие из них просто слишком велики
для записи в качестве макроопределения ^define.
// Макросы для работы с векторами (обратите внимание на
//инициализацию значения поля w четырехмерного вектора)
//Обнуление векторов
inline void VECTOR2D_ZERO(VECTOR2D_PTR v)
{(v)->x-(v)->y=0.0;}
inline void VECTOR3D_ZERO(VECTOR3D_PTR v)
{(v)->x = (v)->y - (v)->z = 0.0;}
inline void VECTOR4D_ZERQ(VECTOR4D_PTR v)
{(v)->x - (v)->y - (v)->z = 0.0; (v)->w - 1.0;}
// Инициализация векторов значениями компонентов
inline void VECTOR2D_INITXY(VECTOR2D_PTR v,
float x, floaty)

inline void VECTORS D..INITXYZ(VECTOR3D_PTR v, float x,
floaty, float 2)

inline void VECTOR4D_.IN!TXYZ(VECTOR4D_PTR v, float x,
floaty, float z)
{(v)->x = (x); (v)->y = (y); (v)->z = (z); (v)->w - 1.0;}
// Инициализация векторов другими векторами
inline void VECTOR2DJNIT(VECTOR2DJ>TR vdst
VECTQR2D._PTRvsrc)
{(vdst)->x = (vsrc)->x; (vdst}->y = (vsrc)->y; }
inline void VECTOR3DJMT(VECTOR3D_PTR vdst
VECTOR3D^PTR vsrc)
{{vdst)->x = (vsrc)'>x; (vdst)->y = (vsrc)->y;
(vdst)->z = (vsrc)->z; }
inline void VECTOR4D_INIT(VECTOR4D^PTR vdst
VECTOR4D_PTRvsrc)
{(vdst)->x = (vsrc)->x; (vdst)->y - (vsrc)->y;
(vdst)->z = (vsrc)->z; (vdst)->w= (vsrc)->w; }
// Копирование векторов
inline void VECTOR2D_COPY(VECTOR2D_PTR vdst
VECTOR2D_PTRvsrc)
{{vdst)->x - (vsrc)->x; (vd!it)->y = (vsrc)->y; }

332

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

inline void VECTOR3D_COPY(VECTOR3D_PTRvdst
VECTOR3D^PTRvsrc)
{(vdst)->x = (vsrc}-;>x; (vdst)->y « (vsrc)->y;

inline void VECTOR4D_COPY(VECTQR4D_PTRvdst
VECTOR4D_PTR vsrc)
{(vdst)->x = (vsrc)->x; (vdst)->y = (vsrc)->y;
(vdst}->z = (vsrc)->z; (vdst)->w = (vsrc)->w; }
// Инициализация точек
inline void POINT2DJNIT(POINT2D_PTR vdst POINT2D_PTR vsrc)
{{vdst)->x = (vsrc)->x; (vdst)->y = (vsrc)->y; }
inline void POINT3DJNn(POINT3DJ»TR vdst, POINT3D_PTR vsrc)
{(vdst)->x = (vsrc)->x; (vdst}->y = (vsrc)->y;
(vdst)->z-=(vsrc)->z;}
inline void POINT4D_JNIT(POINT4DJ>TR vdst POINT4D_PTR vsrc)
{(vdst)->x = (vsrc)->x; (vdst)->y = (vsrc)->y;
(vdst)->z = (vsrc)->z; (vdst)->w = (vsrc)->w; }
// Копирование точек
inline void POINT2Dj:OPY(POINT2D_PTR vdst POINT2D^PTR vsrc)
{(vdst)->x = (vsrc)->x; (vdst)->y - (vsrc)->y; }
inline void POINT3D_COPY(POINT3D^PTR vdst, POINT3D_PTR vsrc)
{(vdst)->x= (vsrc)->x; (vdst)->y= (vsrc)->y;
(vdst)->z= (vsrc)->z;>
inline void POINT4D_COPY(POINT4D_PTR vdst, POINT4D_PTR vsrc)
{{vdst)->x= (vsrc)->x; (vdst)->y= (vsrc)->y;
(vdst)->z = (vsrc)->z; (vdst)->w = (vsrc)->w; }

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

Матрицы
Далее следуют макросы для работы с матрицами. Здесь также имеются как макроопределения tfdefine, так и встраиваемые функции.
При вызове функции memsetQ многие компиляторы используют побайтовое заполнение
памяти. Этот процесс можно ускорить, используя заполнение машинными словами.
Следует учесть, что в общем случае представление целых чисел отличается от представления тех же чисел, но в формате с плавающей точкой, т.е., например, представление (float)5.0 отличается от представления (int)5; однако 32-битовое представление
числа 0,0 с плавающей точкой эквивалентно представлению целого 32-битового числа
О, так что упомянутый трюк с использованием машинного слова при обнулении массивов вполне применим и для массивов чисел с плавающей точкой.
// Макросы для работы с матрицами

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

333

// Макросы для обнуления матриц
«define MAT_ZERO_2X2(m) {memset((void *)(m), 0,\
sizeof(MATRIX2X2));}
«define MAT_ZER(L3X3(m) {memset{(void *)(m), 0,\
sizeof(MATRIX3X3)};}
«define MAT_ZERO_4X4(m) {memset((void *)(m), 0,\
sizeof(MATRIX4X4));}
«define MAT_ZERO_4X3(m) {memset((void *)(m), ОД
sizeof(MATRIX4X3));}
// Макросы для инициализации единичными матрицами
«define MAT_IDENTITY_2X2(m) {memcpy({void *)(m), \
(void *)&IMAT_2X2, sizeof(MATRIX2X2});}
«define МАТ_ШЕМПГУ..ЗХЗ(т) (memcpy((void *)(m), \
(void *)&IMAT_3X3, :>izeof(MATRIX3X3));}
«define MAT_IDENTITY_4X4(m) {memcpy((void *)(m),\
(void *)&IMAT_4X4, £,izeof(MATRIX4X4));}
«define MAT_IDENTITY_4X3(m) {memcpy((void *)(m), \
(void *)&IMAT_4X3, sizeof(MATRIX4X3));>
//Макросы копировсшия матриц
«define MAT_COPY_^2X2(src_mat, dest^mat) \
{memcpy((void *}(dest_mat), (void *)(src_mat), \
sizeof(MATRIX2X2)};}
«define MAT_COPY^3X3(src^mat dest_mat) \
{memcpy{(void *)(desLTiat)f (void *)(src_mat), \
sizeof(MATRIX3X3));}
«define MAT__COPY_4X4(src_mat dest^mat) \
{memcpy((void *)(dest_mat), (void *)(src_mat), \
sizeof(MATRIX4X4)); >
#define MAT^COPY_4X3(src_mat dest^mat) \
{memcpy((void ')(desLmat), (void *)(src_mat), \
sizeof(MATRIX4X3));)
//Транспонирование матриц
inline void MAT_TRANSPOSE^3X3(MATRIX3X3_PTR m)
{ MATRIX3X3 mt;
mt.MOO = m->MOO; mt.MOl = m->MlO; mt.M02 = m->M20;
mt.MlO - m->M01; mt.Mll - m->Mll; mt.Ml? = m->M21;
mt.M20 = m->M02; mt.M21 = m->M12; mt,M22 = m->M22;
memcpy((void *)m,(voitl *)&mt si2eof(MATRIX3X3)); >
inline void MAT_TRANSPO$E_4X4(MATRIX4X4_PTR m)
{ MATRIX4X4mt;
mtMOO - m->MOO; mt.MOl = m->MlO;
mt,M02 = m->M20; mt:.M03 - m->M30;
mt.MlO - m->M01; mt.Mll =- m->Mll;
mt.M12 - m->M21; mt.M13 = m->M31;
mt.M20 = m->M02; mt.M21 = m->Ml2;
mt.M22 = m->M22; mt.M23 = m->M32;
mt.MSO = m->M03; mt.M31 = m->Ml3;
mt.M32 = m->M22; mt.M33 •= m->M33;
memcpy((void *)m,(void *)Smt sizeof(MATRIX4X4)}; }
334

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

inline void MAT_TRANSPOSE_3X3(MATRIX3X3_PTR m,
MATRIX3X3_PTR mt)
{ mt->MOO = m->MOO; mt->M01 - m->M10; mt->M02 = m->M20;
mt->M10 - m->M01; mt->Mll - m->Mll; mt->M12 - m->M21;
mt->M20 = m->M02; mt->M21 =- m->M12; mt-:>M22 - m->M22; }
inline void MAT_TRAN$POSE_4X4(MATRIX4X4_PTR m,
MATRIX4X4_PTR mt)
{ mt->MOO • m->MQO; mt->M01 - m->MlO;
mt->M02 = m->M20; mt->M03 - m->M30;
mt->M10 = m->M01; mt->Mll - m->Mll;
mt->M12 = m->M21; mt->M13 =« m->M31;
mt->M20 - m->M02; mt->M21 • m->Ml2;
mt->M22 « m->M22; mt->M23 = m->M32;
mt->M30 • m->M03; mt->M31 - m->M13;
mt->M32 = m->M22; mt->M33 - m->M33;}
//Следующие функции можно переписать как макросы, но при
// этом снизится надежность из-за отсутствия проверки типов
//Обмен столбцов
inlinevoidMAT_COLUMN__SWAP_2X2(MATRIX2X2_PTRm,intc,
MATRIX1X2_PTR v)
{ m->M[0]td-v->M[0]; m
inline void MAT_COLUMN_SWAP_3X3(MATRIX3X3^PTR m, int c,
MATRIX1X3_PTR v)

{ m->M[Ol[c]-v->MtOl; m->M[l][c]=v->M[l];
m->M[2][c]-v->M[2];}
inline void MAT__COLUMN_SWAP^4X4(MATRIX4X4_PTR m, int c,
MATRIX1X4_PTR v)
{ m->M[0][c]=v->M[0]; m->M[l)[c]-v->M[l];
m->M[2][c]-v->M[2l; m->M[3][c]-v->M[3]; }
inline void MAT^.COLUMN_SWAP_4X3(MATRIX4X3_PTR m.intc,
MATRIX1X4_PTR v)

{ m->M[0][c]-v->M[0]; m->M[l][c]=v->M[l];
]; m->M[3][c]=v->M[3]; }
Обратите внимание на то, что все функции для работы с матрицами используют указатели.

Кватернионы
Следующий набор макросов предназначен для работы с кватернионами. Все эти
функции являются встраиваемыми.
// Функции для работы с кватернионами
inline void QUAT_ZERO(QUATJ>TR q)
{(q)->x - (q)->y - (q)->z - (q)->w = 0.0;}
inline void QUAT_INITWXYZ(QUAT_PTR q, float w, float x,
float y, float z)

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

335

{ (q)->w - (w); {q)->x = (x); (q)->y « (y); (q)->z = (z); }
inline void QUATJNIT_VECTOR3D(QUAT_PTR q, VECTOR3D_PTR v)
{ (q)->w = 0; (qj->x = (v->x);

inline void QUAT_INIT(QUAT_PTR qdst QUAT^PTR qsrc)
{(qdst)->w •= (qsrc)->w; (qdst)->x = (qsrc)->x;
(qdst)->y - (qsrc)->y; (qdst)->z - (qsrc)->z; }
inline void QUAT_COPY(QUAT_PTR qdst QUAT_PTR qsrcj
{(qdst)->x- (qsrc)->x; (qdst)->y= (qsrc)->y;
(qdst)->z = (qsrc)->z; (qdst)->w = (qsrc)->w; }

Назначение этих функций очевидно как из названий, так и из кода — очень простого,

Математика с фиксированной точкой
Последний набор макросов предназначен для преобразования чисел с фиксированной точкой и выделения целой и дробной частей этих чисел. Другие функции для работы
с такими числами являются обычными, не встраиваемыми функциями. Дело в том, что в
них используется встроенный ассемблер, который мне не хотелось использовать в заголовочном файле.
// Выделение целой и дробной частей числа с
//фиксированной точкой в формате 16.16
^define FIXPl6_WP(fp) ((fp) » FIXP16_SHIFTJ
«define FIXPl6_DP(fp) ((fp) && FIXP16^DP_MASK)
// Преобразование целых чисел и чисел с плавающей
//точкой в числа с фиксированной точкой в формате 16.16
tfdefine INT_TO_FIXP16(i) ((i) *< FIXP16_SHIFT)
tfdefine FLOAT__TO_FIXPl6(f) \
(({float)(f) * (float)FIXPl6_MAG+0.5))
// Преобразование числа с фиксированной точкой
// в число с плавающей точкой
^define FIXP16_TO_FLOAT(fp) ( ((float)fp)/FKPl6_MAG)

Прототипы
Теперь перейдем к прототипам функций математической библиотеки. Сейчас я просто перечислю эти функции; пояснения по поводу работы конкретных функций будут
даны позже. Заметим, что здесь есть несколько функций из файлов T3DLIB1.CPPJH. Все
функции разбиты на группы в соответствии с их функциональностью.
//Тригонометрические; функции
float Fast_Sin{float thetaj;
float Fast_Cos(float theta);
// Функции для работы с расстояниями (из T3DLIBl.CPP|H)

int Fast_Distance_2D(intx,inty);
float Fast_DistancOD(float x, float y, float z);

336

ЧАСТЬ П. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

// Полярные, цилиндрические и сферические функции
)
void PQLAR2D_ToJ OINT2D(POLAR2D_PTR polar,
POINT2D_PTR rect);
void PQLAR2D_To__RectXY(PQLAR2D_PTR рЫаГ(
float Ч float *y);
void POINT2DJ"oJ>OLAR2D(POINT2D_PTR rect,
POLAR2D_PTR polar);
void PQINT2DJTo_PolarRTri(POINT2D_PTR rect,

float Ч float *theta);
void CYLINDRICAL3D_To_POINT3D{CYLINDRICAL3D_PTR cyL
POINT3D_PTRrect);
void CYLINDRlCAL3D_To^RectXYZ(CYLINDRICAL3D_PTR cyL
float Ч float *y, float *z);
void POINT3D_To_CYLINDRICAL3D(POINT3D_PTR rect
3
CYLINDRICAL3DJ TRcyl);
void POINT3D_To_CyLindricaLRThZ{POINT3D_PTR rect float *r,
float *theta, float *z);
void SPHERICAL3D_To_POINT3D(SPHERICAL3D_PTR sph,
POINT3D_PTR rect);
void SPHERICAL3D_To_RectXYZ(SPHERICAL3D_PTR sph, float Ч
float*y, float *z);
void POINT3D_To_SPHERICAL3D(POINT3Dj>TR rect
SPHERICAL3D_PTRsph);
void POINT3D_To_SphericalPThPh(POINT3D_PTR rect float *p,
float *theta, float *phi);
// Функции для работы с двумерными векторами
void VECTOR2D_Add(VECTOR2D^PTR va, \/ECTOR2DJ>TR vb,

VECTOR2DJ>TRvsum);
VECTOR2D VECTOR2D_Add(VECTOR2D_PTR va, VECTOR2D_PTR vb);
void VECTOR20^Sub(VECTOR2D_PTR va, VECTOR2DJ>TR vb,
VEaOR2D_PTR vdiff);
VECTOR2D VECTOR2D_Sub(VECTORaD_PTR va, VECTOR2D_PTR vb);
voidVECTOR2D^Scale(floatk,VECTOR2D_PTRva);
void VECTOR2D_Scale(float k, VECTOR2D_PTR va,
VECTOR2D_PTRvscaled);
float VECTOR2D_Dot(VECTOR20_PTR va, VECTQR20_PTR vb);
float VECTOR2D_Length(VECTOR2D,PTRva);
float VECTOR2D_Length_Fast(VECTOR2Q_PTR va);
void VECTOR2D^Normalize(VECTOR2D__PTR va);
void VECTOR2D J4ormalize(VECTOR2D .JTR va, VECTOR2D__PTR vn);
void VECTOR2D_Build{VECTOR2D_PTR init, VECTOR2D_PTR term,
VECTOR2D^_PTR result);
float VECTOR2D_CosTh(VECTOR2D__PTR va, VECTOR2D_PTR vb);
void VEaOR2DJ3rint(VECTQR2DJ>TR va, char *name);
// Функции для работы с трехмерными векторами
void VECTOR3D_Add(VEaOR3D_PTR va, VECTOR3D_PTR vb,
VECTORBDJ'TRvsum);
VECTOR3D VECTOR3D_Add(VECTOR30^PTR va, VEaOR3D_PTR vb);
void VECTOR3D_Sub(VECTOR3D_PTR va, VECTOR3D_PTR vb,
VECTOR3D_PTR vdiff);
VECTOR3DVECTOR3D_SLib(VECTOR3D_PTRva,VECTOR3D_PTRvb);

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

337

void VECTOR3D^Scale(float k, VECTOR3D_PTR va);
void VECTORSD.Scateffloat k, VECTOR3D_PTR var
VECTOR3D_PTRvscaled);
float VECTOR3D_Dot(VECTOR3D_PTR va, vKTOR3DJ>TR vb);
void VECTOR3D__Cross(VECTOR3D_PTR va, VECTOR3D_PTR vb,
VECTOR3D..PTRvn);
VECTOR3D VECTOR30j:ross(VECTOR3D_PTR va, VECTOR3D.PTR vb);
float VECTOR3D_Length(VECTOR3D^PTRva);
float VECTOR3D_Length_Fast(V£CTOR3D_PTR va);
void VECTOR3D_Normalize(VECTOR3D_PTR va);
void VECTOR3D_Norinalize(VECTOR3D_.PTR va, VECTOR3D_PTR vn);
void vKTOR3Ouild(VECTOR3D_PTR init VECTOR3D_PTR term,

VECTOR3D_PTR result);
float VECTOR3D,CosTh(VECTOR3D_PTR va, vECTOR3D_PTR vb);
void VECTOR3DJ>rint(VECTOR3D_PTR va, char 'name);
// Функции для работы с четырехмерными векторами
void VECTOR4D_Add{VECTOR4D_PTR va, VECTOR4D_PTR vb,
VECTOR4D_PTRvsum);
VECTOR4D VECTOR4D_Add(VECTOR4D_PTR va, VECTOR4D_PTR vb);
void VECTOR4D_Sub(VECTOR4D_PTR va, VECTOR4D^PTR vb,
VECTOR4D__PTRvdiff);
VECTOR40 VECTOR4D._S(jb(VEC70R4D_PTR va, VECTOR4D^PTR vb);
void VECTOR4D_Scate(float k, VECTOR4D_PTR va);
void VECTOR4D_Scale(float k, VECTOR4D^PTR va,
VECTOR4D_PTR vscaled);
float VECTOR4D_Dot(VECTOR4D^PTR va, VECTOR4D_PTR vb);
void VECTOR4D_Cross{VECTOR4D_PTR va, VECTOR4D_PTR vb,
VECTOR4D_PTRvn);
VECTOR4D VECTOR4D_Cross(VECTOR4D_PTR va, VECTOR40_PTR vb);
float VECTOR4D_Length(VECTOR4D_PTR va);
float VECTOR4D_lengt^.Fast(VECTOR4D_PTR va);
void VECTOR4D_Norm;jlize(VECTOR4D_PTR va);
void VECTOR40_Normali?e(VECTOR4D_PTR va, VECTOR4D_PTR vn);
void VECTOR4D_Build(VECTOR4D_PTR init VECTOR4D_PTR term,
VECTOR4D_^PTR result);
ftoat VECTOR4D^CosTh(VECTOR4D_PTR va, VECTOR40_PTR vb);
void VECTOR4D_Print(VECTOR4D_PTR va, char "name);
// Функции для работы с матрицами 2x2 (из T3DLIB1.CPP|H)
void MaUnit_2X2(MATR:iX2X2._PTR ma,

float mOO, float mOl, float mlO, float mil);
void Print_MaC2X2(MATRIX2X2_PTR ma, char "name);
float Mat_Det_2X2(MATR!X2X2_PTR m);
void Mat_Add_2X2(MATRIX2X2_PTR ma, MATRIX2X2_PTR mb,
MATRIX2X2_PTR msum);
void Mat_Mul_2X2(MATRIX2X2_PTR ma, MATRIX2X2^PTR mb,
MATRIX2X2_PTR mprod);
int MaLlnverse_2X2(MATRIX2X2_PTR m, MATRIX2X2_PTR mi);
int Solve_2X2_System(MATRIX2X2_PTR A, MATRIX1X2^PTR X,
MATRIX1X2_PTR 8);
//Функции для работы с матрицами 3x3

338

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

//(часть из T3DLIB1.CPP|H)
int Mat_Mul_lX2_3X2(MATRIXlX2J>TR ma, MATRIX3X2_PTR mb,
MATRIX1X2_PTR mprod);
int Mat_Mul_lX3_3X3(MATRIXlX3_PTR ma, MATRIX3X3_PTR mb,
MATRIX1X3_PTR mprod);
int Mat_Mul_3X3(MATRIX3X3_PTR ma, MATRIX3X3_PTR mb,
MATRIX3X3_PTR mprod);
inline int Mat_IniC3X2(MATRIX3X3 J>TR ma, float mOO,
float mOl, float mlO, float mil,
float m20, float m21);
void Mat_Add_3X3(MATRIX3X3_PTR ma, MATRIX3X3_PTR mb,
MATRIX3X3_PTRmsum);
void MaCMul_VECTQR3D_3X3(VECTOR3D^PTRva, MATRIX3X3J>TR mb,
VECTOR3D_PTRvprod);
int MatJnversOX3(MATRIX3X3_PTR m, MATRIX3X3_PTR mi);
void Mat_Init_3X3(MATRIX3X3^PTR ma, float mOO, float mOl,
float m02, float mlO, float mil, float m!2,
float m20, float m21r float m22);
void Print_Mat_3X3(MATRIX3X3_PTR ma, char *name);
float MatJDet_3X3(MATRIX3X3_PTR m);
int Solve_3X3_System(MATR!X3X3_PTR A, MATRIX1X3_PTR X
MATRIX1X3_PTR B);
// Функции для работы с матрицами 4x4
void MatJ\dd_4X4(MATRIX4X4^PTR ma, MATRIX4X4_PTR mb,
MATRIX4XOTR msum);
void Mat_MuL4X4(MATRIX4X4_PTR ma, MATRIX4X4_PTR mb,
MATRIX4X4_PTR mprod);
void Mat_MuL_lX4_4X4(MATRIXlX4_PTR ma, MATRIX4X4_PTR mb,
MATR1X1X4_PTR mprod};
void Mat_Mul_VECTOR3D_4X4(VECTOR3D_PTR va, MATRIX4X4_PTR mb,
VECTOR3D^PTRvprod);
void Mat_MuLVECTOR3D_4X3(VECTOR3D_PTR va, MATRIX4X3J>TR mb,
VECTOR3D__PTRvprod);
void MaLMuM/ECTOR4D_4X4(VECTOR4D_PTR va, MATRIX4X4_PTR mb,
VECTOR40_PTRvprod);
void Mat_MuM/ECTOR4D_4X3(VECTOR4D_PTRva, MATRIX4X4_PTR mb,
VECTOR40_PTRvprod);
int Mat_Inverse_4X4{MATRIX4X4_PTR m, MATRIX4X4_PTR mi);
void Mat_Init_4X4(rtATRIX4X4_PTR ma, float mOO, float mOl,
float m02, float m03, float mlO, float mil,
float m!2, float ml3, float m20, float m21,
float m22, float m23, float m30, float m31,
float m32, float m33);
void Print_Mat_4X4(MATRIX4X4J>TR ma, char *name);
// Функции для работы с кватернионами
void QUAT_Add(QUAT_PTR ql, QUAT_PTR q2, QUAT_PTR qsum);
void QUAT_5ub(QUAT_PTR ql, QUAT.PTR q2, QUAT_PTR qdiff);
voidQUAT^Conjugate(QUAT_PTRq,QUAT_PTRqconj);
void QUAT^Scale(QUAT_PTR q, float scale, QUAT_PTR qs);
void QUAT_Scale(QUAT_PTRq, float scale);
float QUAT_Norm((lUAT__PTR q);

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

339

float QUAT_Norm2(QUAT_PTR q);
void QUAT,Normalize(QUAT_PTR q, QUAT_PTR qn);
void QUAT_Normalize(QUAT_PTR q);
void QUAT^Unit_Inverse(QUAT_PTR q, QUAT_PTR qi);
void QUAT_Unit_Inverse(QUAT_PTR q);
void QUAT_Inverse(QUAT_PTR q, QUAT^PTR qi);
void QUAT_Inverse(QUAT_PTR q);
void QUAT^Mul(QUAT _PTR ql, QUAT.PTR q2, QUAT^PTR qprod);
void QUATJriple_ProdiJct(QUAT_PTR ql, QUAT_PTR q2,
QUAT_PTR q3, QUAT^PTR qprod);
void VECTOR3DJ"heta.Jo J)UAT(QUAT_PTR q, VECTOR3D_PTR v,
float theta);
void VECTOR4DJ~hetaJo_QUAT(QUA-LPTR q, VECTOR4D_PTR v,
float theta);
void EulerZYX_To^QUAT(QUAT_PTR q, float theta_2,
float theta_y, float theta_x);
void QUAT_To_VECTOR3D_Theta(QUAT_PTR q, VECTOR3D_PTR v,
float *theta);
void QUAT_Print(QUAT^PTR q, char *name);
// Работа с двумерными параметрическими прямыми
void ImXParm_Line2D(POINT2D_PTR pjnit, POINT2D_PTR p_term,
PARMUNE2D__PTRp);
void Compute.Parm_.Line2D(PARMLINE2D_PTRp, float t
POINT2D.PTR pt);
int Intersect_Parm_L.ines2D(PARMUNE2D_PTR pi,
PARMLENE2DJ)TR p2, float 41,
float 42);
int Intersect_ParrruLines2D(PARMLINE2D_PTR pi,
PARMLINE2D_PTRp2,
POINT2D_PTRpt);
// Работа с трехмерными параметрическими прямыми
void Imt_ParmJ.ine3D(P01NT3D_PTR p_im"t
POINT3D_PTR p_term, PARMLINE3D_PTR p);
void Compute^Parm^Line3D(PARMLINE3D_PTR p, float t
POINT3D_PTR pt);
// Работа с плоскостями в трехмерном пространстве
void PLANE3D_Init(PLANE3D^PTR plane, POINTSD^PTR pOf
VECTOR3D_PTR normal int normalize);
float Compute_Point_In,.Plane3D{POINT3D^PTR pt,
PLANE3D_PTR plane);
int Intersect_Parm_Line3D_Plane3D(PARMLINE3D_PTR pline,
PLANE3D_PTR plane,
float 4,POINT3D_PTRpt);
// Функции для работы с числами с фиксированной точкой
FIXP16 FIXP16__MUL(FJXP16 fpl, FIXP16 fp2);
FIXP16 FIXP16__DIV(FIXP16 fpl, FIXP16 fp2);
void FIXPl6_Print(FIXFl6 fp);

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

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Глобальные переменные
В нашей математической библиотеке глобальных переменных очень мало, причем, все
они — из упоминавшихся ранее файлов T3DLIBl.CPP|H. Это — таблицы поиска для быстрого вычисления значений синусов и косинусов,
//Таблицы поиска для вычисления тригонометрических функций
extern float cos_look[361];
extern float sin_look[361];
Обратите внимание, что данные таблицы содержат значения функций для углов, выраженных в градусах, а не радианах. Таблицы содержат по 361 записи, для углов от 0° до 360°
включительно. Да, конечно, 360° — это то же, что и 0°, но наличие такой записи в таблице
облегчает реализацию некоторых алгоритмов. Вам только надо не забыть инициализировать таблицы при помощи вызова Buud_Sin_Cos_JabLes() 11 начале вашего приложения.
В более надежной и интеллектуальной математической библиотеке может иметься
как гораздо большее число глобальных переменных, отслеживающих состояние
библиотеки, гак и более сложные структуры данных.

API математической библиотеки
А сейчас начинается самая интересная часть этой главы. Вы узнаете, что именно делает каждая из функций математической библиотеки, причем этот матерь ал снабжен
многочисленными демонстрационными примерами. Кроме того, я покажу вам
"внутренности" некоторых функций, чтобы вы могли увидеть, как разрабатываются такие функции. Описания функций, которые являются частью математической поддержки
библиотеки T30LIB1.CPPJH, даны в главе 3, "Виртуальный компьютер для программирования трехмерных игр".

Тригонометрические функции
Прототипы функций
float Fast_Sin(fLoat theta);
float Fast_Cos(float theta};
Исходный текст функции
float Fast_Sin(float theta)
// Функция использует таблицу sin_look[] для поиска
// значения синуса. Обрабатывает отрицательные значения
// углов. Для дробных значений выполняется интерполяция
theta = fmodf(theta,360);
// Делаем угол положительным
if (theta < 0) theta+=360.0;
// Вычисляем целую и дробную часть для интерполяции
int thetajnt - (int)theta;
float theta Jrac = theta - thetajnt;
// Обратите внимание на корректность обработки угла 359
// градусов из-за наличия в таблице угла 360 градусов
ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

341

return (sin__look[theta_int] +

theta._frac*(sin_Look[theta_int-*-l] sinjook[theta_int]});
} // Fast.Sin
Назначение
Функции Fast_5in() и Fast_Cos() вычисляют синус и косинус переданного в качестве
параметра угла с использованием таблиц поиска и линейной интерполяции для получения уточненного значения. Данные функции работают быстрее соответствующих встроенных функций математической библиотеки C/C++. Передаваемый параметр должен
предстаапять собой величину угла в градусах. На рис. 5.11 проиллюстрировано применение интерполяции для вычисления тригонометрических функций.
sin_look[361]

0

0.0

1

0.0174

Ошибка:
.|0.7682 - sin (50°25) | =< 0.0001



50

0.7660

51

0.7771

Выходные данные

0.7771 - -

Интерполируемая точка

0.7660- -

.

360

0

sin50°

sin51°

Входные данные

Рис. 5. И. Использование интерполяции в функциях Fo5t_'

Пример использования
// Инициализация таблиц
BuilOLAR2D(&pr, &pp);

Прототип функции
void POINT2D_To^PolarRTh(POINT2D_PTR rect
float *r, float 4heta);

Назначение
Функция преобразует двумерные декартовы координаты в явные значения полярных
координат.
Пример использования
POLAR2Dpp = {3,PI};
floatr=0,theta = 0;
// Преобразование декартовых координат в полярные
POINT2DJXPolarRTh(8,pp, &r, 8
Sat Jan 29 22:36:00.520, 2000
Velocity=[ 1.000000, 2.000000, 3.000000, ]
Closing Error Output File.

Функции для работы с матрицами
Очередной набор функций предназначен для выполнения преобразований и математических операций с матрицами. В основном при разработке трехмерных игр
используются матрицы 3x3 и 4x4, но иногда мы будем "химичить" и работать с матрицами 4x3, считая последний, фиктивный столбец таких матриц имеющим предопределенное значение. Впрочем, как бы я ни пытался учесть все случаи преобразований или умножения матриц, какие-то варианты окажутся пропущенными — однако дописать нужные функции и внести их в библиотеку достаточно просто.
И последнее замечание — в библиотеке T3DLIBl.CPP|H имеются некоторые функции
для работы с матрицами 2x2.

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

353

Хотя функции для работы с матрицами и похожи на функции для работы с векторами
в том, что имеются варианты функций для выполнения одинаковых действий с матрицами разных размерностей, здесь я не стал объединять их в группы с одинаковой
функциональностью. Это объясняется тем, что иногда функции для более высоких
размерностей реализованы не так, как для низких. Поэтому мы будем сначала рассматривать функции для работы с матрицами 2x2, затем — 3x3 и 4x4.

Прототип функции
void Mat_IniL2X2(MATR!X2X2_PTR ma,
float mOO, float mOl,
fioatmlO, float mil);

Назначение
Функция void Mat_Init_2X2() инициализирует матрицу ma значениями с плавающей
точкой (передаваемыми построчно).
Пример использования
MATRIX2X2 та;
// Инициализация единичной матрицы
Mat_Init_2X2(&ma, 1, О, 0,1);

Прототип функции
void Mat_Add_2X2(MATRIX2X2_PTR ma, MATRIX2X2_PTR mb,
MATRIX2X2_PTR msum);

Назначение
Функция void Mat_Add_2X2() суммирует матрицы (ma+mb) и сохраняет результат в матрице msum.
Пример использования
MATRIX2X2 ml = {1,2, 3,4};
MATRIX2X2m2-{4,9'5,6};
MATRIX2X2 msum;
// Сложение матриц
Mat_Add_2X2(&ml, &m2, &msum);

Прототип функции
void Mat_Mul_2X2(MATRIX2X2_PTR ma, MATRIX2X2_PTR mb,
MATRIX2X2_PTR mprod);

Исходный текст функции
void Mat_Mul_2X2(MATRIX2X2_PTR ma, MATRIX2X2_PTR mb,
МАШХ2Х2 PTR mprod)
I
// Умножение двух матриц 2x2
mprod->MOO - ma->MQO*mb->MOO + ma->MGl*mb->MlO;
mprod->M01 - ma->MQO*mb->MQ: + ma->M01*mb->Mll;
mprod->M10 = ma->M10*mb->MOO + ma->Mll*mb->M10;
mprod->Mll = ma->MlO*mb->M01 + ma->Mll*mb->Mll;
}//Mat_Mul_2X2

Назначение
Функция void Mat:_MuL2X2() умножает матрицы (ma*mb) и сохраняет полученный результат в матрице mprod. Обратите внимание, что для повышения скорости матрицы перемножаются "в лоб", без использования циклов.
354

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Пример использования
MATRIX2X2ml-{l,2,3A};
MATRJX2X2 m2 - {1,0, ОД};
MATRIX2X2 mprod;
// Умножение; заметим, что ml*m2 = ml, т.к. m2=I
Mat_MuL2X2(&ml, &m2, fcrnprod);
Прототип функции (изТ301ДВ1.СРР|Н)
int MatJ4uL_lX2_3X2(MATRIXlX2_PTR ma, MATRIX3X2_PTR mb,
MATRIX1X2_PTR mprod);

Исходный текст функции
int Mat_MuLlX2_3X2(MATRIXlX2_PTR ma, MATRIX3X2_PTR mb,
MATRIX1X2_PTR mprod)
// Функция перемножает матрицу 1x2 на матрицу 3x2
// Используется фиктивный элемент для того, чтобы
// матрицу 1x2 к виду 1x3 для корректности умножения
for (int coUO; colM[index][coL]);
} //forindex
// Последний элемент умножаем на 1
sum+-mb->M[index][col];
// Вставка полученного элемента в результат
mprod->M[col] - sum;
} // for col
return(l);
}//Mat_Mul_lX2_3X2
Назначение
Функция int Mat_Mul_lX2_3X2() представляет собой специализированную функцию
для умножения матрицы 1x2 (представляющую в основном двумерные точки) на матрицу
3x2, которая представляет поворот и перенос. Для корректности операции требуется добавление фиктивного элемента, чтобы внутренние размерности матриц стали одинаковыми (рис. 5.17).
Пример использования
MATRIX1X2 pl-[5,5},p2;// Точка
// Поворот и перенос
MATRIX3X2 m - (cos(th), sin(th),
-sin(th), cos(th),
dx dy};
// Умножение матриц
Mat_Mul_lX2^3X2(&pl, &m, &p2);

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

355

m
т

Pi
т,

[xy]

т,
т

Операция
не определена,
т.к. 2 it 3

Для умножения требуется
изменить pi

m

Пример вычисления
х

- ( • moo*у- т,0 + 1 • тм.х • т0| +• у тм + г • mai)

Pi*/
i

х 3|

Добавленное
значение

m1

m,

m

m
3 X 2

Рис. 5.17. Выполнение умножения матриц ]х2иЗх2
Приведенную функцию можно ускорить, развернув циклы. Однако код с использованием циклов понятнее в силу итеративности решаемой задачи. Запомните — никогда не приступайте к оптимизации, пока это не является совершенно необходимым.
Приступив к оптимизации, начинайте с оптимизации алгоритма. Лишь когда возможности оптимизации алгоритма исчерпаны, можно приступать к оптимизации кода, и только исчерпав возможности оптимизации кода на высоком уровне, можно
использовать для оптимизации ассемблерные вставки.
Прототип функции
float Mat_DetL2X2(MATRIX2X2_PTR m);
Назначение
Функция float Mat_ Det_2X2() вычисляет и возвращает значение определителя матрицы 2x2.
Пример использования
MATRIX2x2 m = {1,2,4,8}; // Матрица сингулярна
// Вычисляем определитель
float det - Mat_Det_2X2(&m);
Прототип функции
int MatJnver$e_2X2(MATRIX2X2_PTR m, MATRIXZX2_PTR mi);
Назначение
Функция int Mat_Inverse_2X2() вычисляет матрицу, обратную матрице т, и сохраняет
ее в матрице mi. Если обратная матрица существует, функция возвращает 1; в противном
случае функция возвращает 0, а матрица mi является неопределенной.

356

ЧАСТЬ П. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Пример использования
MATRIX2XZmA-{l,2,4,-3};
MATRIX2X2 ml;
// Вычисление матрицы, обратной А
if (Mat_Inverse_2X2(&mA, &ml)>
{
// Обратная матрица существует...
}//if
else
{
// Обратная матрица не существует...
}//else
Прототип функции
void Print_Mat_2X2(MATRIX2X2_PTR ma, char *name);

Назначение
Функция void Print_Mat_2X2() выводит матрицу в удобочитаемом формате вместе с
именем, переданным в строке name. Вывод осуществляется в файл, открытый при помощи вызова функции Open_Error_FHe().
Пример использования
MATRIX2X2 m - {1,2,3,4};

// Выводим матрицу на экран
Open_Error_FUe("", stdout);
Print_Mat_2X2(&m,BMatrix m");
//Закрываем файл
Close_Error_FUe();
Прототип функции
void Mat_Init_3X3(MATRIX3X3_PTR ma,
float mOO, float mOl, float m02,
float mlO, float mil, float ml2,
float m20, float m21, float m22);
Назначение
Функция void Mat_Init_3X3() инициализирует матрицу ma непосредственными значениями элементов, передаваемых построчно.
Пример использования
MATRIX3X3 та;
// Инициализируем единичную матрицу
Mat_Init_3X3(&ma, 1,0,0,0,1,0,0,0,1);

Прототип функции (изТ301ЛВ1.СРР|Н)
inline int Mat_Init_3XE(MATRIX3X2_PTR ma,
float mOO, float mOl,
float mlO, float mil,
float m20, float m21);
Назначение
Функция void Mat_Init_3X2() инициализирует матрицу ma непосредственными значениями элементов, передаваемых построчно.
ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

357

Пример использования
MATRIX3X2 ma;
// Верхний левый угол делаем единичной матрицей
Mat_Init_3X2(&ma, 1,0, ОД, 0,0);
Прототип функции
void Mat_AdcL3X3(MATRIX3X3_PTR ma, MATRIX3X3_PTR mb,
MATRIX3X3_PTR msum);
Назначение
Функция void Mat_AdtL3X3() суммирует матрицы (ma+mb) и сохраняет результат в матрице msum.
Пример использования
MATRIX3X3 ml - {1,2,3,4,5,6,7,8,9};
MATRIX3X3 m2 = {4,9,7, -1,5,6,2,3,4);
MATRIX3X3 msum;
//Сложение матриц
MaCAdd_3X3(&ml, &m2, &msum);
Прототип функции
void Mat_Mul_VECTOR3D_3X3(VECTOR3D_PTR va, MATRIX3X3_PTR mb,VECTOR3D_PTR vprod);
Назначение
Функция void Mat_MuLVECTOR3D_3X3() умножает 1x3 вектор va на матрицу mb размером 3x3 (рис. 5.18).
Пример использования
VECTOR3D v={x,y,l}, vt;
MATRIX3X3 m - {1,0,0, 0,l,0,xtytl};
//Умножение v*m
Mat_MuLVECTOR3D_3X3(&v, &m, &vt);
Можете ли вы сообразить, что именно делает данное преобразование? Если учесть,
что VECTOR3D на самом деле представляет собой однородные координаты двумерной точки, то становится понятно, что точка перемещается в новое положение, описываемое
формулами
vt.x - v.x+xt;
vt.y - v.y+yt;
Прототип функции (изТ301ЛВ1.СРР|н)
int Mat_MuL_lX3_3X3(MATRIXlX3^PTR ma, MATRIX3X3_PTR mb,
MATRIXlX3_PTRmprod);
Назначение
Функция int Mat_..MuLlX3_3X3() умножает матрицу 1x3 (вектор-строку) на матрицу
размером 3x3. Эта функция, за исключением используемых типов, идентична функции
Mat MuL VECTOR3D ЗХЗ().
^'
i
Пример использования
MATRIX1X3 v={x,y,l}, vf
MATRIX3X3 m - {1,0,0, 0,l,0,xtyU};
358

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

//Умножение v*m
Mat_Mul_lX3_3X3(&v, &m, &vt);

ni

rni
*

rr\z
I

'11

"42

20/ "I2i

'"22

mo
I

10

Вектор-столбец

Например, m t =

+ г- ma),(x • m01

m21),(x

Рис. 5. IS. Умножение трехмерного вектора на матрицу 3x3
Прототип функции (M3T3DLIB1.CPPIH)
int Mat_MuL3X3(MATRIX3X3_PTR та, MATRIX3X3_PTR mb,
MATRIX3X3 J>TR mprod);

Назначение
Функция void Mat_Mut_3X3() перемножает матрицы размера 3x3 (ma*nib) и сохраняет
результат в матрице mprod.
Пример использования
MATRIX3X3 ml = {1,2,3- 4,5,6, 7,8,9};
MATRIX3X3 m2 = {1,0,0,0,1,0, 0,0,1};
MATRIX3X3 mprod;
// Умножение. Обратите внимание: ml*m2 = ml, т.к. m2-I
Mat_MuL3X3(&ml, &m2, &mprod);

Прототип функции
float Mat_Det_3X3(MATRIX3X3^PTR m);

Назначение
Функция Uoat Mat_Det_3X3() вычисляет и возвращает значение определитстя матрицы т.
Пример использования
MATRIX3x3 т - {1,2,0,4,8,9, 2,5,7};
// Вычисление определителя
float det = Mat__Det_3X3(&m);

Прототип функции
int Mat_Inverse_3X3(MATRIX3X3_PTR m, MATRIX3X3_PTR mi);

Назначение
Функция int Mat_Inverse_3X3() вычисляет матрицу, обратную матрице т, и сохраняет
ее в матрице mi. Если обратная матрица существует, функция возвращает 1; в противном
случае функция возвращает 0, а матрица mi является неопределенной.
ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

359

Пример использования
MATRIX3X3 mA = {1,2,9, 4,-3,6, 1,0,5};
MATRIX3X3 ml;
// Вычисление обратной к А матрицы
if (Mat_Inverse_3X3(&mA, &ml))

f

// Обратная матрица существует...

else

1

// Обратная матрица не существует...
}//eLse

Прототип функции
void Print_Mat_3X3(MATRIX3X3_PTR ma, char *name);

Назначение
Функция void Print_Mat_3X3() выводит матрицу в удобочитаемом формате вместе с
именем, переданным в строке пате. Вывод осуществляется в файл, открытый при помощи вызова функции Open_Error_File().
Пример использования
MATRIX3X3 m - {1,2,3, 4,5,6, 7,8,9};
//Открываем файл и выводим в него матрицу
Ореп_Еггог_/Не ("error.txt");
Print_Mat_3X3(&m,"Matrix m");
//Закрываем файл
CLose_Error_File();

Прототип функции
void Mat_Init_4X4(MATRIX4X4_PTR ma,
float mOO, float mOl, float m02, float m03,
float mlO, float mil, float ml2, float m!3,
float m20, float m21, float m22, float m23,
float m30, float m31, float m32, float m33);

Назначение
Функция voidMat_Init_4X4() инициализирует матрицу ma непосредственными значениями элементов, передаваемых построчно.
Пример использования
MATRIX4X4 та;
// Инициализация единичной матрицы
Mat Jnit_4X4(8.ma, 1,0,0,0, 0,1 ДО, 0,0,1,0, 0,0,0,1);

Прототип функции
void Mat_Add_4X4(MATRIX4X4_PTR ma, MATRIX4X4_PTR mb,
MATRIX4X4_PTR msum);

360

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Назначение
Функция void Mat_Add_4X4()cyMMHpyeT матрицы (ma+mb) и сохраняет результат в матрице msum.
Пример использования
MATRIX4X4 ml - {1,2,3,4, 5,6,7,8, 9,10,11,12,13,14,15,16);
MATRIX4X4 m2 - {4,9,7,3, -1,5,6,7,2,3,4,5, 2,0,5,3};
MATRIX4X4 msum;
//Сложение матриц
Mat_Add_4X4(&ml, &m2, Smsum};
Прототип функции
void Mat_MuL4X4(MATRIX4X4_PTR ma, MATRIX4X4^PTR mb,
MATRIX4X4_PTR mprod);

Назначение
Функция void Mat_MuL__4X4Q перемножает матрицы размером 4x4 (ma*mb) и сохраняет
результат в матрице mprod.
Пример использования
MATRIX4X4 ml = {1,2,3,4,4,5,6,7,7,8,9,10,11,12,13,14};
MATRIX4X4 m2 = {1.0ДО, ОД,0,0, 0,0,1,0, 0,0,0,1};
MATRIX4X4 mprod;
//Умножение; обратите внимание: ml*m2 = ml,т.к. m2«I
Mat_MuL4X4(&ml, &m2, &mprod);
Прототип функции
void Mat_Mul_lX4_4X4(MATRIXlX4_PTR ma, MATRIX4X4_^PTR mb,
MATRIX1X4_PTR mprod);

Исходный текст функции
void Mat_Mul_lX4_4X4(MATRIXlX4J)TR ma,
MATRIX4X4_PTR mb,
MATRIX1X4_PTR mprod}
I
// Функция умножает матрицу размером 1x4 на
// матрицу 4x4. Никаких хитростей, простое
// итеративное перемножение
for (int col=0; colM[row][col]};
}//for index
// Сохранение полученного элемента
mprod->M[col] =sum;
}//forcol
}//MaLMuLlX4_4X4

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

361

Назначение
Функция void Mat_Mul_lX4_4X4{) умножает матрицу 1x4 на матрицу 4x4 mprod =
(ma*mb). He делается никаких предположений об однородности координат и т.п., выполняется непосредственное умножение матриц. Ускорить функцию можно, использовав
явное выполнение всех математических операций, без использования циклов.
Пример использования
MATRIX1X4 v-{x,yAl}, vt;
MATRIX4X4 m = {1,0,0,0 0,1,0,0,ОД1Д xtytztl};
//Умножение v*m (перенострехмерной точки x,y,z)
Mat_MuLlX4_4X4(&v, &m, &vt);
Прототип функции
void Mat_MuLVECTOR3D_4X4(VECTOR3D_PTR va,
MATRIX4X4_PTR mb,
VECTOR3D_PTR vprod);

Назначение
Функция void Mat_MuLVECTOR3D_4X4() умножает трехмерный вектор на матрицу 4x4.
Для того чтобы такое умножение было осуществимо, функция предполагает наличие
фиктивного четвертого элемента вектора, равного 1.0. Кроме того, после выполнения
умножения результат представляет собой трехмерный вектор (а не четырехмерный).
Данная функция используется для преобразования трехмерных точек при помоши
матриц 4x4 (рис. 5.19).
Пример использования
VECTOR3D у{10ДОЛО}, vrt;
// Вращение вокруг оси х и перенос (tx,ty,tz)
MATRIX4X4 m = {1,
О, О, О,
О, cos(th), sin(th). О,
0,-sin(th},cos(th), О,
tx, ty, tz, 1);
// Выполнение преобразования
Mat_MuLVECTOR3D_4X4(&v, &m, &vrt);
Поскольку вектор и точка представляют собой синонимы, те же действия могут быть
выполнены и с типом POINT30.
РСЯМТЗОр1»{10ДО,10},р2;
// Вращение вокруг оси х и перенос (tx,ty,tz)
MATRIX4X4 m - (1, О, О, О,
О, cos(th),sin(th). О,
0,-sin(th),cos(th), О,
tx, ty, tz, 1};
// Выполнение преобразования
Mat_MuLVECTOR3D_4X4(8,pl, &m, &p2);
Прототип функции
void Mat_Mu[_VECTOR3D_4X3(VECTOR3D_PTR va, MATRIX4X3_PTR mb,
VECTOR3D^PTRvprod);
362

ЧАСТЬ II. ТРЁХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Подматрица обобщенного преобразования и масштабирования (3 х 3)

m

>/

ГПоо

^01

^02

^03

m10

mt1

m12

m,3

m20

m2,

m22

m23

' Вектор (3x1) — обычно [О О О ]



[*

V

у z/|]

.

t

Добавляется - - - . _ - _ - _ _ _ _ _ _ _ _ _ P _ _ _ _ _
только для
Tx
Ty
Тг
i
выполнения
i

*

умножения
Подматрица переноса (1 хЗ)

т=[(х

т21+ Ту),(х

t

1

у • m, a + z

Члены, определяющие перенос

Tz)]

!

Рис. 5.19. Умножение трехмерного вектора на матрицу 4x4
Назначение
Функция void Mat_Mul_VECTOR3D_4X3() очень похожа на функцию Mat_MuL_VECTOR3D_4X4(),
но она выполняет умножение вектора на матрицу размером 4x3, а не 4x4. Здесь также предполагается наличие фиктивного четвертого элемента вектора, равного 1.0. Поскольку в матрице
4x3 имеется только 3 столбца, умножение не требует последующего искусственного отбрасывания элемента, как в случае функции Mat_Mul_VECTOR3D_4X4().
Пример использования
РОШЗОр1={10ДОДО},р2;
// Вращение вокруг оси х и перенос (tx,ty,tz)
// Обратите внимание на отсутствие последнего столбца
MATRIX4X3 m - (1,
О,
О,
О, cos(th),sin(th),
0,-sin(th),cos(th),

tx,

ty,

tz};

// Выполнение преобразования
Mat_MuL_VECTDR3D_4X3(&plf &m, &p2);
Прототип функции
void Mat_MuLVECTOR4D_4X4{VECTOR4D_PTR va, MATRIX4X4_PTR mb,
VECTOR4D_PTRvprod);
Назначение
Функция void Mat_MuL_VECTOR4D_4X4() умножает вектор-строку 1x4 va на матрицу 4x4
mb, сохраняя результат в векторе 1x4 vprod. В функции не делается никаких предположений о том или ином виде вектора или матрицы.
Пример использования
VECTOR4D v={miOf10,l},vrt;
// Поворот вокруг оси х и перенос (tx,ty,tz)
ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

363

MATRIX4X4 m - { 1,
О,
О, О,
О, cos(th),sin(th). О,
0,-sin(thJ,cos(th), О,

tx,

ty,

tz, 1};

// Выполнение преобразования
Mat_MuL_VECTOR4D__4X4(&v, &m, &vrt);

Обратите внимание на то, что результат имеет вид vrt = {x',y',z',l), т.е. компонента w
равна 1.0.
Прототип функции
void Mat_MuLVECTOR4Q_4X3(VECTOR4D_PTR va, MATRIX4X4_PTR mb,
VECTOR4D_PTR vprod);

Назначение
Функция void Mat_MuM/ECTOR4D_4X3() очень похожа на функцию Mat_Mul_VECTOR3D_4X3(),
но выполняет умножение четырехмерного, а не трехмерного вектора. Поскольку в матрице
4x3 три столбца, компонент w копируется из va в vprod (другими словами, дополнительный
столбец матрицы 4x3 полагается равным [ 0 0 0

I]'.

Пример использования
РОШ40 рМЮДОДОДЬ р2;
// Поворот вокруг оси х и перенос (tx,ty,tz)
MATRIX4X3 m - { 1,
О,
О,
0, cos(th},sin(th),
О,-sin(th), cos(th),
tx,
tyr
tz};
//Выполнение преобразования
Mat_MuLVECTOR4D_4X3{&pl, &m, &p2);

Прототип функции
int MatJnverse_4X4(MATRIX4X4_PTR m, MATRIX4X4_PTR mi);

Назначение
Функция int Mat_Inverse_4X4() вычисляет матрицу, обратную матрице т, и сохраняет
ее в матрице mi. Если обратная матрица существует, функция возвращает 1; в противном
случае функция возвращает 0, а матрица mi является неопределенной. Функция работает
только с матрицами, последний столбец которых имеет вид [ 0 0 0 1]'.
Пример использования
//Обратите внимание на последний столбец
MATRIX4X4 тА - {1, 2, 9, О,
4,-3, б. О,
1, О, 5Г О,
2,3,4,1};
MATRIX4X4 ml;
// Вычисляем матрицу, обратную А
if{Mat_Inverse_4X4(&mA,&mI))
I

364

ЧАСТЬ », ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

// Обратная матрица существует...
} //if
else
// Обратная матрица не существует...
} //else
Прототип функции
void Print_Mat_4X4(MATRIX4X4_PTR ma, char *name);
Назначение
Функция void Print_Mat_4X4() выводит матрицу в удобочитаемом формате вместе с
именем, переданным в строке name. Вывод осуществляется в файл, открытый при помощи вызова функции Open_Error_File().
Пример использования
MATRIX4X4 m - { 1, 2, 3, 4,
5, 6, 7, 8,

9,10,11,12,
13,14,15,16};
// Открываем файл и осуществляем вывод
Open_Error_File(" error.txt");
Print_Mat_4X4(&m,"Matrix m");
//Закрываем файл
Close_Error_File();

Функции для работы с двумерными и трехмерными
параметрическими прямыми
Когда я начинал работу над созданием математической библиотеки, я вообще не собирался добавлять в нее поддержку параметрических прямых. Почему? Потому что это
все легко кодируется вручную, при помощи вектора v и параметра t. Однако затем я подумал, что часто выполняемые вычисления можно закодировать и облегчить тем самым
работу программиста. Естественным шагом при этом является решение об инкапсуляции
данных, определяющих прямую, в отдельной структуре.
Напомню, как выглядят структуры данных, представляющие параметрические прямые. Вот структура данных для двумерной прямой.
//Двумерная параметрическая прямая///////////////////////
typedef struct PARMLINE2D_TYP
{

POINT2D рО; // Начальная точка
POINT2D pi; // Конечная точка
VECTOR2D v; // Вектор направления
//М=|рО->р1]
} PARMLINE2D, *PARMLINE2D_PTR;
Связь значений между собой показана на рис. 5.2. Обратите внимание, что используемый в структуре вектор не нормализован. Структура данных для трехмерной прямой
выглядит аналогично.
//Трехмерная параметрическая прямая//////////////////////
typedef struct PARMLINE3D_TYP
ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

365

I

POINT3D pO; // Начальная точка
POINT3D pi; // Конечная точка
VECTOR3D v; // Вектор направления
//М-]рО->р1|
} PARMLINE3D, *PARMUNE3D_PTR;

Единственным отличием от двумерного варианта является наличие оси z. А теперь
можно приступать к рассмотрению функций, работающих с этими структурами.
Прототип функции
void Init_ParmJ_ine2D(POINT2D_PTR pjm't
POINT2D_PTR p_term,
PARMLINE2D_PTRp);

Назначение
Функция void Init_Parm_Line2D() инициализирует двумерную параметрическую прямую двумя точками и вычисляет вектор между ними.
Пример использования
POINT2D р! - {1,2}, р2 * {10,20};
PARMLINE2Dp;
// Создание параметрической прямой от точки р! к pZ
Init_ParrrUine2D(&pl, &p2, &p);

Прототип функции
void Compute_Parm_Line2D(PARMUNE2D_PTR p, float t
POINT2D_PTRpt);

Назначение
Функция void Cornpute_Parm_Line2D() вычисляет точку на прямой, соответствующей
значению параметра t, и сохраняет его в переменной pt. При t=0 возвращается начальная точка, при t-1 — конечная.
Пример использования
POINT2D р! - {1,2}, р2 - {10,20}, pt;
PARMLINE2D р;
// Создание параметрической прямой от точки р! к р2
Init_Parm_Line2D(&pl, &p2, &р);
// Вычисление точки, соответствующей значению t=0.5
Compute_Parm_Line2D(&p, 0.5, &pt);

Прототип функции
int Intersect_Parm^Lines2D(PARMLINE2D_PTR pi,
PARMLINE2D^PTR p2,
float *tl, float 42);

Исходный текст функции
int Intersect_Parm_Lines2D{PARMLINE2D_PTR pi,
PARMLINE2D_PTRpZ,
float *tl, float 42)
// Эта функция вычисляет пересечение отрезков двух

366

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

// параметрических прямых и возвращает true, если
// прямые пересекаются. При этом значения tl и t2
// соответствуют точке пересечения. Параметры могут
// выходить за пределы диапазона [0,1], что означает,
// что несмотря на пересечение прямых отрезки не
// пересекаются. Возвращаемое значение 0 означает,
// что прямые не пересекаются, 1 - что отрезки
// пересекаются, 2 - что прямые пересекаются, но вне
// отрезков, и 3 - что прямые совпадают.
// Шаг 1: проверка на параллельность прямых
float det_plp2 = (pl->v.x*p2->v.y- pl->v.y*p2->v.x);
if (fabs(det_plp2) v.x*(pl->pt).y - р2->р0.у) p2->v.y*(pl->pQ.x - р2->р0.х)) / detj>lp2;
42 - (pl->v.x*(pl->p0.y - р2->р0.у) pl->v.y*(pl->pd.x - р2->р0.х)) / det_plp2;
// Проверка пересечения отрезков

if ((*tl>=0) && (««*0) && (Ч2
return{PARM_LINE_INTERSECT_IN_SEGMENT);
else
return(PARM_LINE_INTERSECT_OUT_SEGMENT);
} // Intersect_Parm_Lines2D

Назначение
Функция int Intersect_Parm_lines2D() вычисляет точку пересечения двух параметрических прямых р! и р2, и возвращает значения параметров точки пересечения tl и t2.
Функция может возвращать следующие значения,
// Пересечения нет
«define PARM_LINE_NCLINTERSECT
О
// Пересечение отрезков
«define PARM_LINE_INTERSECT_IN_SEGMENT I
// Пересечение прямых, но не отрезков
«define PARM_LINEJNTERSECT_OUT_SEGMENT 2

В настоящее время функция не рассматривает ситуацию совпадающих прямых из-за
множества возможных при этом ситуаций: частичное перекрытие, включение одного отрезка в другой и т.п.). Вы можете добавить необходимую вам функциональность самостоятельно. См. также демонстрационную программу DEMOII5_1.CPP|EXE на прилагаемом
компакт-диске.
Пример использования
// Данные линии пересекаются

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

367

POINT2D рЗ •= {2,8}, р4 « {7,1};
PARMUNE2D pll, р!2;
//Создание параметрических прямых
Init_Parm_Line2D(&pl, 8,р2, &pll);
Init_Parm_Line2D{S«p3, &р4, &р12);
float tl^O, t2»0; // Переменные для хранения параметров
// Вычисление точки пересечения
int intersection_type Intersect_Parm_Lines2D(&pll, &pl2, &tl, &t2);

Прототип функции
int Intersect_Parm_Lines2D(PARMLINE2D_PTR pi,
PARMLINE2DJ>TRp2,
POINT2D_PTR pt);

Назначение
Функция int Intersect_Parm_Lines2D() вычисляет точку пересечения двух параметрических прямых, но вместо возврата значений параметров tl и t2 она возвращает координаты точки пересечения. Ясно, что перед тем, как использовать полученную точку, следует
проверить, какое целое значение вернула данная функция (возвращаемые ею значения
такие же, как и у предыдущей функции).
Пример использования
// Эти линии пересекаются
POINT2Dpl-{U},p2-{9,8};
POINT2D рЗ - {2,8}, р4 - (7,1);
POINT2D pt; // Переменная для хранения точки пересечения
PARMLINE2Dpll,pL2;
// Создание параметрических прямых
Init_Parm_Line2D(&pl, &p2, Spll);
Init_Parm_Line2D(&p3,, &p4, &р!2);
// Вычисление точки пересечения
int intersect!оn_type Intersect_Parm_Lines2D(&pllr &plZ, &pt);
Программисты на чистом С могут удивиться наличию в одной библиотеке двух функций с одинаковым именем, однако это вполне нормально для C++. Дело в том, что
параметры функций различны и, таким образом, с точки зрения компилятора эти
функции различны. Такое свойство языка C++ называется перегрузкой функций и
сопровождаатся изменением внутреннего представления имени функции с учетом
типов ее параметров, что в результате позволяет иметь функции с одинаковыми
именами, но разными параметрами.

Теперь перейдем к функциям, предназначенным для работы с трехмерными параметрическими прямыми. В данном разделе их немного, поскольку часть их перенесена в раздел, посвященный функциям для работы с трехмерными плоскостями.
Прототип функции
void Init_Parm_Line3D(P01NT3D_PTR pjnit,
POINT3D_PTR p_term,
PARMLINE3D_PTRp);

368

ЧАСТЫ1. ТРЕХМЕРНАЯ МАТЕМАТИКА И ПРЕОБРАЗОВАНИЯ

Назначение
Функция void Init_Parm_Line3DQ инициализирует трехмерную параметрическую прямую двумя точками и вычисляет вектор между ними.
Пример использования
POINT3D р! - {1,2,3}, р2 - {10,20,30};
PARMUNE3D р;
// Создание параметрической прямой от точки р! к р2
Init_Parm_Line3D(&pl, &р2, &р);

Прототип функции
void Compute_Parm_Line3D(PARMlINE3D_PTR p,
floatt,POINT3DJ>TRpt);

Назначение
Функция void Cornpute_ParmJ_ine3D() вычисляет точку на прямой, соответствующую
параметру t, и сохраняет ее значение в переменной pt. При t=0 возвращается начальная
точка, при t= 1 — конечная.
Пример использования
POINT3D р! - {1,2,3}- р2 - {10,20,30}, pt;
PARMUNESDp;
//Создание параметрической прямой отточки р! к р2
Init_Parm_Line3D(&pl, &p2, &р);
// Вычисление точки, соответствующей значению t=0.5
Compute_Parm_Line3D(&pf 0.5, &pt);

Функции для работы с трехмерными плоскостями
Хотя это может выглядеть несколько опрометчиво, я решил добавить в библиотеку
поддержку абстрактных трехмерных плоскостей. В действительности 99% времени мы
будем работать с замкнутыми многоугольниками (лежащими на плоскости), а поэтому,
возможно, эти функции позже потребуется переписать. Тем не менее, возможность определять плоскости и работать с ними нам не повредит на любой стадии работы.
Я решил использовать показанное на рис. 5.20 представление плоскости с помощью
точки и вектора нормали:
n K (*-x 0 ) + n y ( y - y 0 ) + n,(z-z a ) = 0,
где n = {n,,n y> n I } и p0 = {x 0 ,y 0 ,z 0 ).
В этом случае мы храним штоскость в структуре, содержащей вектор нормали и точку
на плоскости; вектор нормали при этом вовсе не обязательно должен быть единичным.
Напомню, как выглядит соответствующая структура.
// Трехмерная

плоскость ///////////////////////////////////

typedef struct PLANE3D_TYP
i
POINT3D pO; // Точка на плоскости
VECTOR30 n; // Нормальный (не обязательно
//единичный) вектор
}PLANE3D,*PLANE3D_PTR;

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

369

Нормаль к плоскости р

Точка на плоскости р

Рис . 5.20. Плоскость, определяемая точкой и нормалью
Многие представленные далее функции тривиальны и призваны всего лишь упростить часто повторяющиеся вычисления либо облегчить представление математических
или геометрических объектов.
Прототип функции
void PLANE3D_Init(PLANE;3D_PTR plane, POINT3D_PTR pO,
VECTOR3D_PTR normal, int normalize);

Назначение
Функция void PLANE3D_Init() инициализирует плоскость при помощи точки и вектора
нормали. Дополнительно функция нормализует последний, делая его длину равной 1.0.
Для этого параметр normalize следует установить равным TRUE. Тикая нормализация нужна при работе с рядом алгоритмов.
Пример использования
VECTOR3D п-{1Д,1};
POINT3Dp={0,0,0};
PLANE3D plane;
// Создание плоскости
PUNE3D_.Init(&pUne, &p, &n, TRUE);

Прототип функции
float Compute_Point_In_Plane3D(POINT3D_PTR pt
PLANE3D_PTR plane);

Исходный текст функции
float Compute^Point_In_Plane3D(POINT3D_PTR pt
PLANE3DJ>TR plane)

i

// Проверка местоположения точки
// относительно плоскости
float hs = plane->n.x*(pt->x - plane->p0.x) +
p(ane->n.y*(pt->y - plane->p0.y) -fplane->n.z*(pt->z - plane->p0.z);

370

ЧАСТЬ II, ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

// Указывает полупространство,
// в котором содержится точка
return (hs);
ompute_PointJn_Plane3D
Назначение
Функция float Compute_Point_InJ4ane3D() обладает достаточно интересной функциональностью. Она вычисляет полупространство, в котором находится переданная ей в качестве параметра точка. Логика данной функции показана на рис. 5.21. Функция возвращает
значение 0.0, если точка лежит на плоскости, положительное число, если точка находится в
положительном полупространстве, и отрицательное— если в отрицательном. См. также
демонстрационную программу ОЕМОШ_2.СРР|ЕХЕна прилагаемом компакт-диске.
Положительное , Отрицательное
полупространство,' полупространство

Точка на плоскости

Рис. 5.21. Разбиение пространства плоскостью на два полупространства
Пример использования
VECTORSDrHl,!,!};
POINT3D р-{0,0,0};
PLAN E3D plane;
// Создание плоскости
PLANE3D_Init(&plane, &p, &n,TRUE);
// Эта точка из положительного полупространства
POINT30 p_test= {50,50,50};
// Проверка местоположения точки
float hs -Compute_PointJn_Plane3D(&p_test &plane);
Прототип функции
int!ntersect_Parm_Line3D_Plane3D(PARMUNE3D_PTRpLine,
PLANE3DJ>TR plane,
uoat*t POINT3D_PTR pt);

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

371

Исходный текст функции
int Intersect_Parm_Line3D_Plane3D(PARMUNE3D_PTR pline,
PLANE3D_PTR plane,
float *tPOINT3DJ>TRpt)
{
// Функция определяет, где переданная параметрическая
// прямая пересекает плоскость. Функция продолжает
// прямую в обе стороны до бесконечности, однако
// отрезок пересекает плоскость тогда и только тогда,
// когда параметр t находится в интервале [0,1].
//Функция возвращает значение 0, если пересечения нет,
// 1,если имеется точка пересечения отрезка и
// плоскости, 2, если точка пересечения находится вне
// отрезка, и 3, если прямая лежит на плоскости.
// Проверка параллельности прямой и плоскости
float plane_dot_line«
VECTOR3D_Dot(&pline->v,&plane->n);
if (fabs(piane_dotjine) pO, plane)
)n.x*pline~>p0.x +
piane->n.y*pline->p0.y +
plane->n.z*pHne->p0.z plane->n.x*plane->p0.x plane->n.y*plane->p0.y plane->n.z*plane->p0.z) / (plane_dot_line);
// Подставляя t в уравнение прямой, находим координаты
//точки пересечения x,y,z
pt->x - pline->p0.x + pline->v.x*(*t);
pt->y = pline->p0.y + plfne->v.y*(*t);
pt->z= pline->p0.z + ptine->v.2*(*t);
// Проверка вхождения t в интервал [ОД]
if (*t>=0.0&& *tx =* sinf_theta * v->x;
q->y =- sinf_theta * v->y;
q->z я sinf_theta * v->z;
q->w = cosf( theta_div_2 );
} //VECTOR3D_Theta_To_.QUAT
Назначение
Функция void VECTOR*D_Theta_To_QUAT() создает кватернион поворота на основании
вектора направления v и угла поворота 0 (рис. 5.22). Функция предназначена для создания кватернионов для расчета поворота точек. Обратите внимание— вектор направления должен быть единичным. В случае передачи в функцию четырехмерного вектора его
компонента w игнорируется.

374

ЧАСТЬ II. ТРЁХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Вектор направления |v| = 1
Кватернион Q
Поворот вокругv
против часовой стрелки
У

Рис. 5,22. Построение кватерниона поворота

Пример использования
// Создание вектора поворота, представляющего собой
//диагональ первого октанта
VECTOR3Dv={l,U};
QUAT qr;
// Нормализация v
VECTOR3D_NormaLize(&v};
float theta = DEGjrO_RAD{100); // 100 градусов
// Создание кватерниона поворота
VECTORS D_Theta_To_QUAT(&q, &v,theta);

Прототип функции
void EulerZYX_To_QUAT(QUAT_PTR q, floattheta.z,
float theta_y, float theta_x);

Исходный текст функции
void EulfirZYX_ToJlUAT(QUAT_PTR q, float theta_z,
float theta_y, float theta^x)
I
// Данная функция инициализирует кватернион,
// основываясь на порядке zyx умножения углов
// поворотов, параллельных осям 2, у, х
// соответственно. Заметим, что прочие 11 способов
// задания поворота дают те же результаты
float cos^z_2 = 0.5*cosf(theta_z);
float cos_y_2 - 0.5*cosf(theta_y);
float cos_x_2 - 0.5*cosf(theta_x);
float sin_z_2 - 0.5*sinf(theta__z);
float sin_y^2 = 0.5*sinf(theta__y);
float sin_x_2 = 0.5*sinf(theta^.x);
ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

375

// Вычисляем кватернион
q->w = cos_z_2*cos_y_2*cos_x_2 +
sin_z_2*sin_y_2*sin_x_2;
q->x • cos_z_2*cos_y_2*sin_x_2 sin_z_2*sin_y_2*cos_x_2;
q->y = cos_z_2*sin_y_2*cos_x_2 +
sin_z_2 *cos_y_2*sin_x_2;
q->z в sin_z_2*cos_y_2*cos_x_2 cos_z_2*sin_y_2*sin_x_2;
} //EuLerZYX_To_QUAT

Назначение
Функция void EulerZYX_To_QUAT() создает кватернион поворота на основании переданных ей углов поворотов Эйлера параллельно осям z, у и х. Эта функция преобразует поворот Эйлера а соответствующий кватернион.
Пример использования
QUAT qzyx;
//Углы поворота
float theta^x - DEG_TO_RAP{20);
float theta^y - DEG_TO_RAd(30);
float theta^z « DEGJXLRAD(45);
// Создание кватерниона поворота
Euter2YX_To_QUAT(&qzyx,theta_z,theta_y,theta_x);

Прототип функции

void QUAT_To_VECTOR3Dj-heta(QUAT^PTR q, VECTOR3D_PTRv,

uoat *theta);
Исходный текст функции

void QUAT_To_VECTOR3D._Theta(QUAT^PTR ч, V£CTOR3D_PTR v,
ftoat "theta)
//Данная функция преобразует единичный
// кватернион в единичный вектор направления
// и угол поворота вокруг него
// Получение угла поворота
*theta - acosf(q->w);
// Предвычисление для повышения эффективности
noatsinf_theta_inv=1.0/sinf(*theta);
// Вычисление вектора
v->x - q->x*sinf_theta_inv;
v->y - q->y*sinf_theta_inv;
v->z - q->z*sinf_thetajnv;
//Умножение на 2
*theta*=2;
} // QUAT_ToJ/ECTOR3D_.Theta

376

ЧАСТЬ!!. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Назначение
Функция void QUAT_Tc_VECTOR3D_Theta() преобразует единичный кватернион в единичный вектор направления и угол поворота вокруг него. Эта функция по сути является
обратной функции VECTOR*D_Theta_ToJlUAT().
Пример использования
QUAT q;
// Считаем, что кватернион q представляет собой
// единичный кватернион поворота
float theta;
VECTORSD v;
// Преобразуем кватернион в вектор и угол
QUAT_ToJ/ECTOR3D_Theta(&q, &v/ &theta);
Прототип функции
void QUAT_Add(QUAT_PTR ql, QUAT^PTR q2, QUAT_PTR qsum);

Назначение
Функция void QUAT_Add() суммирует кватернионы ql и q2 и сохраняет сумму в переменной qsum.
Пример использования
QUAT ql - {1,2,ЗЛ}, q2 - {5,6,7,8}, qsum;
// Сложение кватернионов
QUAT_Add(8,ql, &q2, &qsum);
Прототип функции
void QUAT_Sub(QUAT_PTR ql, QUAT_PTR qZ, QUAT_PTR qdiff);
Назначение
Функция void QUAT_Sub() вычитает кватернион q2 из ql и сохраняет разность в переменной qdiff,
Пример использования
QUAT ql - {1,2,3,4}, q2 » {5,6,7,8}, qdiff;
// Вычитание кватернионов
QUAT_Sub(&ql, &q2,&qdiff);
Прототип функции
void QUAT_Conjugate(QUAT_PTR q, QUAT_PTR qconj);
Назначение
Функция void QUAT_Conjugate() находит кватернион, сопряженный кватерниону q, и
сохраняет его в переменной qconj.
Пример использования
QUAT q - {1,2,3,4}, qconj;
// Вычисляем сопряженный кватернион
QUAT_Conjugate(Sq, &qconj);
Прототип функции
void QUAT_ScaLe(QUAT^PTR q, float scale, QUAT_PTR qs);

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ,

377

Назначение
Функция void QUAT_5cale() масштабирует кватернион q с коэффициентом scale и сохраняет результат в переменной qs.
Пример использования
QUAT q - {1,2,3,4}, qs;
// Масштабирование q с коэффициентом 2
QUAT_Stale(&q, 2, &qs);
Прототип функции
void QUAT^Scale(QUAT__PTR q, float scale);
Назначение
Функция void QUAT__Scale() масштабирует кватернион q с коэффициентом scale, непосредственно изменяя при этом значение переменной q,
Пример использования
QUAT q = {1,2,3,4};
// Масштабирование q с коэффициентом 2
QUA"LScate(&q, 2);
Прототип функции
float QUA"LNorm(QUAT__PTR q);

Назначение
Функция float QUAT_Norm(QUAT_PTR q) возвращает норму (длину) кватерниона q.
Пример использования
QUAT q - {1,2,3,4};
// Чему равна длина q?
float qnorm = QUAT_Norm(&q);
Прототип функции
float QUAT_Norm2(QUAT_PTR q); .
Назначение
Функция float QUAT,_Norm2(QUAT_PTR q) возвращает квадрат нормы кватерниона q. Эта
функция очень полезна там, где мы можем использовать квадрат нормы — например, при
сравнении норм. Преимущество данной функции в более быстром по сравнению с функцией QUAT_Norm() вычислении за счет отсутствия вызова sqrtQ.
Пример использования
QUAT q = {1,2,3,4};
// Чему равен квадрат длины q?
float qnorm2 • QUAT_Norm2(&q);
Прототип функции
void QUAT_Normalize(QUAT_PTR q, QUAT_PTR qn);
Назначение
Функция void QUA"LNormalize() нормализует кватернион q и помешает нормализованный кватернион Б переменную qn.

378

ЧАСТЬ II, ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Не забывайте — все кватернионы поворота должны быть единичными!

Пример использования
QUAT q = {1,2,3,4}, qn;
// Нормализация q
QUAT_Normalize(&q, &qn);

Прототип функции
void QUAT__Normalize(QUAT_PTR q);
Назначение
Функция void QUAT_Normalize() нормализует кватернион q, модифицируя саму переменную q.
Пример использования
QUAT q = {1,2,3,4};
// Нормализация q
QUA"LNoirnalize(&q);

Прототип функции
void QUAT_UnitJnverse(aUAT_PTR q, QUAT_PTR qi);

Назначение
Функция void QUAT_Unit_Inverse() вычисляет кватернион, обратный кватерниону q, и
сохраняет его в переменной qi. Кватернион q должен быть единичным, поскольку функция использует тот факт, что обратным к единичному кватерниону является сопряженный кватернион.
Пример использования
QUAT q-{1,2,3,4}, qi;
// Сначала нормализуем q
QUAT_Normalize(&q);
// Вычисляем обратный кватернион
QUATJJnit_Inverse(&q, &qi);

Прототип функции
void QUAT_Unit_Inverse(QUAT_PTR q);

Назначение
Функция void Ql)AT_Unit_Inverse() вычисляет кватернион, обратный кватерниону q,
сохраняя его в той же переменной q. Кватернион должен быть единичным, поскольку
функция использует тот факт, что обратным единичному кватерниону является сопряженный кватернион.
Пример использования
QUAT q = {1,2,3,4};
// Сначала нормализуем q
QUAOormalize(&q);
// Вычисляем обратный кватернион
QUAT_Unit_Inverse(&q);

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

379

Прототип функции
void QUAT_Inverse(QUAT_PTR q, QUAT_PTR qi);
Назначение
Функция void QUAT_Inverse() вычисляет кватернион, обратный произвольному
(требование единичности отсутствует) кватерниону q, и сохраняет его в переменной qi.
Пример использования
QUATq = {1,2,3,4}, qi;
// Вычисляем обратный кватернион
QUATJnversef&q, &qi);
Прототип функции
void QUAT_Inverse(QUAT_PTR q);
Назначение
Функция void QUAT_Inverse() вычисляет кватернион, обратный произвольному (требование
единичности отсутствует) кватерниону q, и сохраняет его в той же переменной q.
Пример использования
QUATq- {1,2,3,4};

.
// Вычисляем обратный кватернион
QUAT_Inverse(&q);
Прототип функции
void QUAT_Mut(QUAT_PTR ql, QUAT_PTR q2, QUAT.PTR qprod);
Исходный текст функции
void QUAT_Mut(QUAT_PTR ql, QUAT_PTR q2, QUAT.PTR qprod)

:

// Функция перемножает два кватерниона методом "в лоб"
//qprod->w •• ql->w*q2->w - ql->x*q2-:>x ql->y*q2->y - ql->z*q2->z;
//qprod->x = ql->w*q2-:>x + ql->x*q2->w +
//
ql->y*q2->z - ql->z*q2->y;
//qprod->y = ql->w*q2-;>y - ql->x*q2->2 +
//
cjl->y*q2->w - ql->z*q2->x;
//qprod->z - ql->w*q2->z -t- ql->x*q2->y //
ql->y*q2->x -•- ql->z*q2->w;
// Для уменьшения количества умножений
// используется выделение множителей
float prd_0 - (ql->z - ql->y) * (q2->y - q2->z);
float prd_l - (ql->w -*- ql->x) * (q2->w + q2->x);
float prd__2 = (ql->w - ql->x) * (q2->y + q2->z);
float prd_3 = (ql->y + ql->z) * (q2->w - q2->x);
float prd_4 - (ql->z - ql->x) * (q2->x - q2->y);
float prd_5 = (ql->z +• ql->x) * (q2->x + q2->y);
float prd_6 - (ql->w + ql->y) * (q2->w - q2->z);
float prd_7 - (ql->w- ql->y) * (q2->w + q2->2);
float prd_8 - prd_5 + prd._6 + prd_7;
float prd_9 - 0.5 * (prd_4 + prd_8);
// Сборка результата из временных переменных

380

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

qprod->w = prd_0 + prd_9 - prd_5;
qprod->x-= prd_l 4- prd_9 - prdJJ
qprod->y * prd_2 + prd_9 - prd_7
qprod->z - prd_3 + prd_9 - prd_6
} // QUAT_Mul
Использование непосредственного определения произведения кватернионов приводит к 16 умножениям и 12 сложениям. Выполнив несложные алгебраические преобразования, я получил 9 умножений и 27 сложений. Проблема только в том, что при использовании сопроцессора для работы с числами с плавающей точкой преимущество может
быть слишком незначительным или отсутствовать вовсе.
Назначение
Функция void QUAT_Mul() перемножает кватернионы ql*q2 и сохраняет результат в переменной qprod.
Пример использования
QUAT ql={l,2,3,4}, q2={5,6,7,8}, qprod;
//Умножение ql*q2
QUAT_Mul(&ql, &q2, qprod);
Не забывайте, что произведение кватернионов в общем случае некоммутативно, т.е.

Прототип функции
void QUAT_Triple_Product(QUAT_PTR qlf QUAT.PTR Ч2,
QUAT_PTR q3, QUAT_PTR qprod};
Назначение
Функция void QUAT_TripLe_Product() перемножает три кватерниона ql*q2*q3 и сохраняет результат в переменной qprod. Эта функция полезна при повороте точки, т.к. поворот
требует перемножения трех кватернионов: q'vq и qvq*.
Пример использования
// Поворот точки (5,0,0) вокруг оси z на 45 градусов
// Шаг 1: создание кватерниона поворота
VECTOR3Dvz-{OAl};
QUAT qr, // Здесь будет храниться кватернион поворота
qrc; // и сопряженный к нему кватернион
// Создание кватерниона поворота
VECTOR3D_Theta_To_QUAT(&qr, &vz, DEG_TO_RAD(45});
// Вычисление сопряженного кватерниона
QUAT_Conjugate(&qrf Sqrc);
// Создаем точку, которая будет поворачиваться
// (qO = 0; x,y,z - координаты точки)
QUAT qp»{0,5,0,0};
// Выполняем поворот точки р вокруг оси z на 45 градусов
QUAT_Triple_Product(&qr, &qp, &qrc, &qprod);

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

381

// В полученном кватернионе qO=0. Для получения
// координат точки после поворота следует взять
// компоненты х,у и г

Прототип функции
void QUAT_Print(QUAT_PTR q, char *name);

Назначение
Функция void QUAT_Print() выводит кватернион в удобочитаемом формате вместе с
именем, переданным в строке name. Вывод осуществляется в файл, открытый при помощи вызова функции Qpen_Error_Fite().
Пример использования
// Открываем вывод на экран
Open_Error__File("", stdout);
QUAT q-{l,2,3,4};
QUAT_Print(&q);
//Закрываем файл
Close_Error_Fite(};

На прилагаемом компакт-диске имеется демонстрационная программа для работы с кватернионами — DEMOII!5_4.CPP|EXE. Она позволяет вам ввести два кватерниона и точку в трехмерном пространстве и выполнить над ними различные операции, описанные в этом разделе.

Функции для работы с числами с фиксированной точкой
Хотя тема работы с числами с фиксированной точкой раскрыта в предыдущей главе далеко не полно, я, тем не менее, оснастил математическую библиотеку рядом соответствующих функций. Если вы хотите познакомиться с числами с фиксированной точкой более детально — приобретите мою предыдущую книгу1, там этот материал изложен более подробно. Кроме того, современные процессоры работают с числами с плавающей точкой
практически так же быстро, как и с целыми числами, поэтому использование математики
с фиксированной точкой становится не столь уж необходимым. Тем не менее, имеется ряд
алгоритмов, где использование чисел с фиксированной точкой вполне оправдано.
Числа с фиксированной точкой — старый хорошо известный трюк, использовавшийся на медленных процессорах. В нем используется "фиксированная точка", которая искусственно разделяет целое число на целую и дробную части. Я использую формат чисел
с фиксированной точкой 16.16, который означает, что у нас имеется 16 бит в целой,
и 16 бит в дробной части числа. Таким образом, диапазон представления чисел с фиксированной точкой — ±32768, с точностью 2 16.
При работе с числами с плавающей точкой неизбежно возникает вопрос о том, каким
образом они должны быть представлены. Для этих чисел можно использовать обычное
32-битовое целое число (или, если его размер слишком мал — то 64-битовое целое). Соответственно, вот как выглядят определенные в файле T3DLIB4.H типы данных для чисел
с фиксированной точкой.
// Числа с фиксированной точкой ///////////////////////////
typedefint FIXP16;
typedefint*FIXP16_PTR;
' Андре Ламот. Программирование игр для Windows. Советы профессионала, 2-е изд. —
М.: Издательский дом "Вильяме", 2003. — Прим. ред.
382

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

В файле T3DLIB1.H определены несколько констант, связанных с числами с фиксированной точкой и призванных упростить работу программиста.
// Константы, связанные с числами с фиксированной точкой
^define FIXP16_SHIFT 16
«define FIXP16_MAG 65536
«define FIXP16^DP_MASK OxOOOOffff
«define FIXP16_WP_MASK OxffffOOOO
«define FIXP16JWUNDJJP 0x00008000

Создание числа с фиксированной точкой из целого
Для создания числа с фиксированной точкой из целого числа следует выполнить
сдвиг последнего на FIXP16_SHIFT бит влево, что приводит к размещению исходного числа
в целой части числа с фиксированной точкой. Важно только не забывать о возможности
переполнения последнего, т.к. для целой части в нем отводиться в 2 раза меньше битов,
чем в целом числе. Вот как выполняется описанное действие:
FIXP16 fpl - (100 « FIXP16);
Те же действия выполняет макрос INT_TO_HXP16(), входящий в состав математической
библиотеки.

Создание числа с фиксированной точкой из числа с плавающей
точкой
Здесь ситуация немного сложнее, поскольку бинарные представления этих чисел совершенно различны. Поэтому вместо сдвига в данном случае следует использовать умножение на 2}Ь = 65536:
FIXP16 fpl = (int){100.5*65536.0);
Те же действия выполняет макрос FLOAT_TO_FIXP16{), входящий в состав математической
библиотеки. В данной ситуации вы можете также захотеть округлить получаемое число,
так как в процессе преобразования типа в int происходит отбрасывание дробной части
числа. Для этого достаточно перед выполнением преобразования прибавить 0.5:
FIXP16 fpl = (int}(100.5*65536.0 + 0.5);

Обратное преобразование в число с плавающей точкой
Для того чтобы преобразовать число с фиксированной точкой в число с плавающей
точкой, достаточно поделить его на 65536:
float ({float)fp)/65536.0;
(В математической библиотеке это делает макрос FIXP16_TO_FLOAT()).
Теперь вкратце рассмотрим выполнение некоторых операций над числами с фиксированной точкой.

Сложение и вычитание
Для сложения или вычитания двух чисел, представленных в виде чисел с фиксированной точкой, достаточно просто сложить их или вычесть, как обычные целые числа.
FIXP16 fpl = FLOAT_T(LFIX{10.5);
FIXP16 fp2 - FLOAT_TO_FIX(20.7);
FIXPl6fpsum -fpl+fp2;
FIXPl6fpdiff = fpl-fp2;
ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

383

А вот более наглядный пример.
intx=50,y=23;
// Преобразуем х и у в числа с фиксированной точкой
FIXP16fpx =
FIXP16 fsum;
// Складываем их
fsum =- fpx + fpy;
Эти действия упрощаются до
fsum=x*65536 + y*65536-(x+y)*65536;

Как видите, полученный результат действительно представляет собой сумму (х +у) ,
умноженную на 65536 — т.е. приведенную к формату с фиксированной точкой.

Умножение
Выполнить умножение чисел с фиксированной точкой несколько сложнее. Причина в
том, что каждое число оказывается умноженным на масштабирующий множитель 65536.
Таким образом, при непосредственном умножении двух чисел результат оказывается умноженным на 65536J.
// Числа, преобразуемые в формат с фиксированной точкой
int х-50, у=23;
// Преобразование в формат 16.16
FIXP16fpx«x*65536;
FIXP16fpy«y*65536;
Попробуем перемножить полученные числа.
fpx'fpy - (х*б553б) * (у*б5536) - (х*у)*(б553б*б553б);

Теперь вы видите, в чем состоит проблема? Полученный результат оказывается
масштабированным с коэффициентом 655362, а не с требующимся 65536. Решение
простое: надо выполнить обратное масштабирование. Но главная проблема не в этом:
перемножение двух чисел с фиксированной точкой вызывает переполнение 32битового представления. Поэтому реальными решениями являются либо использование 64-битовой математики, либо предварительное масштабирование сомножителей
так, чтобы произведение было масштабировано с корректным коэффициентом 65536.
Вот как это можно сделать:
= (fpx/256)*(fpy/256)
Это приводит к следующему результату:
- ((х*б553б)/25б) * ((у*б553б)/256)
= (х*256)*(у*25б) = (х*у)*б553б

Казалось бы, все в порядке, но данный способ имеет один серьезный недостаток — в процессе деления на 256 мы теряем 8 бит точности как у множимого, так и у
множителя, что неприемлемо: ведь главная цель математики с фиксированной точкой состоит в обеспечении точности при работе с числами с десятичной точкой. В
этой ситуации может выручить ассемблерная вставка с использованием 64-битовой
математики.
384

ЧАСТЬ (I. ТРЁХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Деление
Деление сопряжено с проблемой, обратной умножению: при делении чисел мы теряем десятичную точку, т.е. полученный результат оказывается масштабированным с коэффициентом 1, а не 65536.
// Числа, преобразуемые а формат с фиксированной точкой
intx-50,y-23;
// Преобразование в формат 16.16
FIXP16 fpx « х-65536;
Теперь выполним деление и посмотрим на результат:
fpx/fpy - (х*6553б) / (у*6553б) - (х/у);
Определенно, это не то, что нам надо. Мы вновь оказываемся перед дилеммой — использовать ассемблер для осуществления 64-битового деления или масштабировать числитель и знаменатель таким образом, чтобы в итоге получить корректно масштабированное частное? В последнем случае мы опять сталкиваемся с проблемой потери точности.
Для того чтобы результат деления был корректно масштабирован (с коэффициентом
65536), можно увеличить числитель в 256 раз, а знаменатель в 256 раз уменьшить. Главным недостатком этого способа является то, что по сути при этом мы делим число в формате 8.16 на число в формате 16.8, так что числитель не может превышать 255, а знаменатель — быть меньше 1/28 = 0.0039 . Вот как осуществляется описанный способ деления.
// Числа, преобразуемые в формат с фиксированной точкой
int х«50, у*23;
// Преобразование в формат 16.16
FIXPl6fpx-x*65536;
FIXPl6fpy«y*65536;
// Деление
fpx/fpy- (х*256*б553б)/(у*65536/25б)
- (х'25б*25б/у)*6553б/6553б
- (х/у)*(25б'256) - (х/у)*б5536
Как видите, числа с фиксированной точкой не обязаны быть только в одном формате
16.16 — одни числа в нашей программе могут быть в этом формате, другие — в формате
24.8, а третьи и вовсе 0.32. Главное— корректное масштабирование и отслеживание
возможных переполнений и потерь точности при выполнении математических операций.
Теперь можно приступить к рассмотрению конкретных функций математической
библиотеки.
Прототип функции
FIXP16 FTXP16J4UL(FIXP16 fpl, FIXP16 fp2);
Исходный текст функции
FIXP16 FIXP16_MUL(FIXP16 fpl, FIXP16 fp2)
i

// Функция вычисляет произведение fp_prod - fpl*fp2
// с использование 64-битной математики
FIXP16 fp__prod; // Возвращаемое значение

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

-.asm {

mov eax, fpl
imulfp2
//Умножение fpl*fp2
shrd eax, edx, 16 //Результат в формате 32:32
// размещен в регистрах edx:eax. Приводим его
// к виду 16:16 в регистре еах
} //asm

}//FIXPl6_MUL
Данный код очень прост и понятен. Самое интересное в нем то, что 32-битовые процессоры Intel имеют поддержку 64-битовой математики.
Назначение
Функция FIXP16 FIXP16_MUL() умножает два числа с фиксированной точкой fpl*fp2 с
использованием 64-битовой математики и возвращает полученное произведение. Заметим, что при этом нет потерь точности.
Пример использования
ЯХР16 fpl - FLOATjrO..FIX(10.5);
FIXP16 fp2 - FLOATJO FIX20.7 ;

.

// Выполняем умножение
FIXP16 fpprod - FTXP16_MUL(fpl,fpZ);
Прототип функции
FIXP16 FIXP16J>IV(FIXP16 fpl, FIXP16 fp2);
Исходный текст функции
FIXP16 FIXP16^DIV(FIXP16 fpl, FIXP16 fp2)
//Функция вычисляет частное fpl/fp2 с использованием
// 64-битной математики без потери точности
_asm {

mov eax, fpl // Помещаем делимое в еах
cdq
// Знаковое расширение в edx:eax
shld edx, eax, 16 // Сдвиг 16:16 в edx
sal eax, 16
// Сдвиг еах
idiv fp2
// Выполнение деления
// Результат находится в еах
} //asm

} //FIXP16_DIV
Алгоритм деления немного сложнее. Сначала делимое должно быть знаково расширено
до 64 бит и выполнен сдвиг для корректного масштабирования результата. Этот сдвиг выполняется двумя операциями, поскольку команда shld не выполняет сдвиг в регистре еах.
Более полную информацию об этих несуразностях при реализации 64-битовых сдвигов в
процессорах Intel можно найти в любом руководстве по их ассемблеру.
Назначение
Функция FIXP16 FIXP16 _DIV() выполняет деление fpl/fp2 и возвращает полученный результат. В связи с использованием 64-битовой математики потери точности при этом отсутствуют.
Пример использования
FIXP16 fpl - FLOAT_TO_FIX(10.5);
FIXP16 fp2 - FLOAT_TO_FIX(20.7);

386

ЧАСТЫ!. ТРЕХМЕРНАЯ МАТЕМАТИКА И ПРЕОБРАЗОВАНИЯ

// Выполняем деление
FIXP16 fpdiv - FKP16_DIV(fpl,fp2);
Как видите, для выполнения умножения и деления чисел с фиксированной точкой
используется несколько команд процессора. Следует заметить, что на современных
процессорах сложение и вычитание чисел с фиксированной точкой выполняется
с той же скоростью, что и сложение и вычитание чисел с плавающей точкой, а умножение и деление последних по скорости зачастую превосходит соответствующие
операции над числами с фиксированной точкой.

Прототип функции
void FIXP16_Print(FIXP16 fp);

Назначение
Функция void FIXP16_Print() выводит число с фиксированной точкой, как если бы это
было число с плавающей точкой.
Пример использования
FIXP16 fpl - FLOAT_TO_FIX(10.5);
FIXP16_Print(fpl);

На прилагаемом компакт-диске имеется демонстрационная программа для работы с
числами с фиксированной точкой— DEMOII5_5,CPP|EXE. Она позволяет вам ввести два
числа с плаваюшей точкой, преобразует их в числа с фиксированной точкой, выполняет
над ними все описанные операции и выводит результаты, чтобы вы могли увидеть точность выполняемых действий.

Функции для решения систем уравнений
Следующие две функции предназначены для решения систем линейных уравнений
вида А • X = В . Все, что вам надо передать в функцию в качестве параметра — это матрицу коэффициентов А и матрицу свободных членов В, а также матрицу, в которую следует
поместить решение, если таковое существует. Например, пусть у нас имеется система
уравнений
х - Зу + 7z = -4,
5х + 9у — 2z = 5.

В таком случае матрицы А, В и X выглядят следующим образом:
"3 2 -5"
1 - 3 7 , Х = [х у г]' И В = [6 -4 6]'.
5 9 - 2
Прототип функции
int SoLve_2X2_System(MATRIX2X2_PTR A, MATRIX1X2_PTR X
MATRIX1X2 PTRB);
'

Исходный текст функции

int Solve_2X2_System(MATRIX2X2_PTR A, MATRIX1X2_PTR X
MATRIX1X2_PTR В)
// Решает систему уравнений АХ-=В и вычисляет X«A{-1)*B

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

387

// с использованием правила Крамера
// Шаг 1: вычисление определителя А
float det_A - Mat_Det_2X2(A);
// Проверка на равенство det(a) нулю (если это так,
// решения не существует)
if (fabs(det_A) < EPSILQN_E5)
return (0);
// Шаг 2: Создаем матрицы-числители путем замены
// соответствующих столбцов матрицы А транспонированной
// матрицей В и находим решение системы уравнений
MATRIX2X2 work_mat; // Рабочая матрица
// Поискх
// Копируем А в рабочую матрицу
MAT_COPY,2X2(A, &work_mat);
//Замена столбца х
MAT_COLUMN_SWAP_2X2(&work_mat О, В);
// Вычисление определителя
float det_ABx - Mat_DeC2X2{&work_mat);
//Поискзначения х
X->MOO-det_ABx/det_A;
// Поиск у
// Копируем А в рабочую матрицу
MAT_COPY_2X2(Af &work_mat);
//Замена столбца у
MAT_COLUMN_SWAP_2X2(&work_mat 1, В);
// Вычисление определителя
float det_ABy - Mat_Det_2X2(&work_mat);
// Поиск значения у
Х->М01 - det_ABy/det_A;
// Возврат кода успешного завершения
return(l);
} //Solve_2X2_System

Назначение
Функция int Solve_2X2_System() предназначена для решения системы линейных уравнений
А - Х = В , где А имеет размер 2x2, а матрицы В и Х — размер ]х2. Если решение существует,
оно сохраняется в матрице X и функция возвращает значение 1; в противном случае функция
возвращает значение 0, а матрица X не определена. Функция для решения системы уравнений
использует правило Крамера. Это неплохой пример использования функций для работы
с матрицами, так что присмотритесь к нему повнимательнее. На прилагаемом компакт-диске
имеется демонстрационная программа ОЕМ01Г5_б.СРР|ЕХЕ, которая позволяет вам ввести и решить систему линейных уравнений.
(Следует заметить, что приведенный исходный текст является примером использования
функций, предназначенных для работы с матрицами, но никак не примером Эчхрективного
решения системы линейных уравнений с двумя неизвестными. Гораздо более adpdpeicniBHoe
решение без излишних пересылок в памяти может выглядеть следующим образом.
int Solve_2X2_System(MATRIX2X2_PTR A, MATRIX1X2_PTR X,
MATRIX1X2 J'TR В)

I

float det_A - Mat_Det_2X2(A);
if (fabs(det_A) < EPSILON_E5)

388

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

return (0);
X->MOO = (B->MOO*A->M11 - B->M01*A->M01)/det_A;
X->M01 = (B->M01*A->MOO - B->MOO*A->MlO)/det_A;
return (1);
} //Solve_2X2_System
— Прим, ред.)
Пример использования
В качестве демонстрационного примера приведен код функции mainQ демонстрационной программы DEMOII5_6.CPP.
void main()
[
MATRIX2X2 mA;
MATRIX1X2 mB;
MATRIX1X2 mX;
// Для вывода сообщений используем экран
Open_Error_File("", stdout);
// Ввод матриц Аи В
printf("\nEnter the values for the matrix A (2x2)");
printf("\nin row major form mOQ, mQl, mlO, mil?");
scanf("%f, %f, %f, %f',&mA.MOO,
&mA.M01, &mA.M10, &mA.Mll);
printfC'V1 Enter the values for matrix В (2x1)");
printf{"\mn column major form mOO, mlO?");
scanf("%f, %f", &mB.MOO, &mB.M01);
// Решаем систему уравнений...
if ($olve_2X2_System(&mA, &mX, &mB))
{
// ...и выводим результат
VECTOR2D_Print({VECTOR2D_PTR)&mX,
"Solution matrix mX");

} //if

else
// ...или сообщение об ошибке
printf("\nNo Solution!");
// Закрываем файл вывода
Clqse_Error_File();
.

} //main

Прототип функции
int Sotve_3X3_System(MATRIX3X3_PTR A, MATRIX1X3_PTR X
MATRIX1X3_PTR В);
Назначение
Функция int Solve_3X3_System() предназначена для решения системы линейных уравнений А - Х = В , где А имеет размер 3x3, а матрицы В и X— размер 1x3. Если решение
существует, оно сохраняется в матрице X и функция возвращает значение 1; в противном
случае функция возвращает значение 0, а матрица X не определена.
Пример использования
MATRIX3X3 mA - {1,2,9,4,-3,б, 1,0,5};
MATRIXIXSmB-{1,2,3};
MATRIX1X3 mX;

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

389

//Решаем систему уравнений
if(SolvOX3_System(&mA, &mX, &mB))
{
// Вывод результатов
VECTOR3D_Print((VECTOR3D^PTR)&mX, "Solution matrix mX");

> //if

else
printf("\nNo Solution!");

Работа математического сопроцессора
Старый сопроцессор 80837 и встроенный математический сопроцессор PentiumX (далее
просто FPU — floating point unit), вероятно, можно считать наиболее загадочными составными частями компьютера. Откровенно говоря, найти хорошее руководство по программированию FPU очень трудно — их попросту можно пересчитать по пальцам. Я хочу попытаться попробовать исправить ситуацию — по крайней мере, для читателей этой книги.
Современные процессоры Pentium могут иметь несколько сопроцессоров, однако я
буду говорить об FPU обобщенно, не рассматривая отдельно случай с несколькими
процессорами.

Конечно, ваш компилятор C/C++ отлично справляется со своими обязанностями и
вполне успешно использует FPU при работе с числами с плавающей точкой- Тем не менее, генерируемый компилятором код в критичных участках можно попытаться оптимизировать вручную при помощи ассемблера и 64-битовых инструкций — например, как
было сделано при рассмотрении умножения и деления чисел с фиксированной точкой.
Есть и другой аспект работы с сопроцессором. В моделях Pentium с поддержкой ММХ
регистры FPU представляют собой синонимы регистров ММХ, что означает, что вы не
можете работать с кодом ММХ и кодом FPU без постоянного переключения контекстов.
Работа с ММХ— отдельная тема, которой я не намерен касаться. Если она интересует
вас, вы можете обратиться к неплохому руководству Intel ММХ Programming Guide, которое можно найти в разделе для программистов на Web-узле Intel.
Итак, как же работает FPU? С точки зрения программирования, не имеет значения,
один ли сопроцессор в системе или несколько, внешний он или встроенный — набор команд остается практически один и тот же. Именно эти общие команды нас и интересуют.

ЕШПЮ

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

Архитектура сопроцессора
Сопроцессор представляет собой виртуальную стековую машину, предназначенную
для вычисления математических операций с числами с плавающей точкой. На рис. 5.23
показана абстрактная модель FPU и его взаимоотношения с процессором. Все команды,
передаваемые процессору и предназначены сопроцессору, передаются для выполнения
последнему, никак не затрагивая процессор и его внутренние регистры. Хотя часто процессор и сопроцессор не могут работать одновременно, все же в большинстве случаев
они в состоянии работать параллельно, в особенности в случае архитектуры с использованием U-V-каналов наподобие Pentium.

390

ЧАСТЬ 11. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Процессор

Встроенный
или внешний
сопроцессор

Программный
счетчик

Команда для работы
с плавающей точкой

Рис. 5.23. Математические операции с плавающей тонкой
выполняются сопроцессором

Сопроцессор может выполнять множество математических операций, как стандартных
(таких как сложение, вычитание, умножение и деление), так и более сложных
(логарифмирование, вычисление различных трансцендентных функций и т.п.). Более того,
сопроцессор в состоянии выполнять многие операции быстрее, чем процессор — соответствующие операции с целыми числами, и, кроме того, с более высокой точностью. Даже в случае, когда FPU оказывается более медленным, чем целочисленный процессор, это компенсируется повышенной точностью — что приобретает особое значение в трехмерной графике.
Программирование FPU — очень простая задача. Он имеет свой набор команд, с которыми вы вскоре познакомитесь. Обычно, когда вы хотите выполнить ту или иную команду FPU,
вы просто помешаете ее в ваш код при помощи соответствующей ассемблерной команды. Это
создает определенную сложность, так как.для этого надо либо использовать внешние функции, написанные на ассемблере, либо встраиваемый ассемблер. Лично я предпочитаю последний способ, кодируя небольшие функции полностью на встраиваемом ассемблере (ясно,
что я не делаю этого для объемных высокоуровневых функций).

Стек сопроцессора
Поговорим о внутреннем стеке сопроцессора, поскольку именно на нем строится все
работа. Обычно в стеке FPU имеется восемь элементов и слово состояния (рис. 5.24).
Каждый элемент стека имеет размер 80 битов, из которых 64 бита отведено под десятичное значение, 14 —для показателя степени и один бит — знаковый.
Однако каждый элемент стека может хранить данные разных типов, как показано на
рис. 5.25. Как видите, это могут быть 4-, 8- и 10-байтовые значения с плавающей точкой.
Элементы стека используются для передачи входных данных и получения результата вычислений, и в этом смысле они являются регистрами сопроцессора, т.е. их модно рассматривать и как набор регистров, и как стек.
ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

391

Степень
Десятичное
(15 битов) значение (64 бита)
7978. ..6463

номер бита

10

Г ST(0)
ST{1)

| Управляющие регистры

ST(2)
Стековые
регистры

Управляющее слово

ST(3)

Слово состояния

ST(4)

Слово дескриптора

ST(5)

IP

ST(6)

OP
~

I ST(7)

* •'"•"••- " ...">->

Рис. 5.24. Базовая архитектура FPU
ST{n) I

80-битовый внутренний регистр

Real 10 (10 байтов)

80 битов (long double)

Real 8 (8 байтов)

64 бита (double)

Real 4 (4 байтов)

32 бита (float)

Рис. 5.25. Форматы данных Fflf
Обращение к элементам стека осуществляется с использованием синтаксиса 5Т(о), где
п — номер элемента. Рассмотрим рис. 5.26.
Стек сопроцессора
ST, ST(0)'



Вершина стека

- •

9 1

-Дно стека
ST-

Рис. 5.26. Обращение к элементам стека сопроцессора

Заметим, что стандарт C/C++ не оговаривает размер чисел float, double и long double, так что
точного соответствия размера типу, показанного на рисунке, у конкретного компилятора может и
не быть. Б качестве примера можно привести Walcom C++, у которого sizeof(double) - sizeof(tong
double). — Прим. ред.
392

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА И ПРЕОБРАЗОВАНИЙ

ST эквивалентно вершине стека (TOS, top of stack).
ST(0) эквивалентно вершине стека.
ST(1) эквивалентно второму элементу стека.
ST(7) эквивалентно последнему элементу стека.
При программировании FPU многие команды принимают ноль, один или два операнда. Операндами в большинстве случаев являются константы, память или элементы
стека. Большинство команд помешают результат вычислений на вершину стека, в ST(0).
При работе помогает также знание о том, какие данные после выполнения операции остаются доступны и корректны, а какие уничтожаются.
Теперь, когда вы получили общее представление о сопроцессоре, перейдем к набору
его команд.
Поскольку выпускаются все новые и новые процессоры Pentium, пытаться составить
полный список команд сопроцессора — безнадежная задача. В табл. 5.1 перечислены основные команды сопроцессора, из которых вы, вероятно, будете использовать
в лучшем случае десятую часть.

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

Описание

Команды передачи данных
FBLD

Загрузка BCD-числа (Binary coded decimal, двоично-десятичное число)

FBSTP

Сохраняет BCD-число и удаляет его из стека

FILD

Загружает целое число

FIST

Сохраняет целое число

FISTP

Сохраняет целое число и удаляет его из стека

FLD
FSTP

Сохраняет действительное число и удаляет его из стека

Загружает действительное число

FXCH

Обменивает местами два элемента стека

Арифметические команды

FABS

Вычисляет абсолютное значение

FADD

Суммирует действительные числа

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

393

Продолжение табл. 5.1
Команда

Описание

FIADD

Суммирует целые числа

FADDP

Суммирует действительные числа и удаляет результат из стека

FCHS

Изменяет знак числа

FDIV

Делит действительные числа

FIDIV

Делит целые числа

FDIVP

Делит действительные числа и удаляет результат из стека

FDIVR

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

FIDIVR

Делит целые числа с обратным порядком делимого и делителя

FDIVRP

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

FMUL

Умножает действительные числа

FIMUL

Умножает целые числа

FMULP

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

FPREM

Вычисляет частичный остаток

FPREM1

Вычисляет частичный остаток с использованием формата IEEE

FRNDINT

Округляет операнд до целого числа

FSCALE

Умножает на степень 2

FSUB
FISUB

Вычитает действительные числа
Вычитает целые числа

FSUBP

Вычитает действительные числа и удаляет результат из стека

FSUBR

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

FISUBR

Вычитает целые числа с обратным порядком вычитаемого и уменьшаемого

FSUBRP

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

FSQRT

Вычисляет квадратный корень

FXTRACT

Выделяет показатель степени и значение действительного числа

Трансцендентные функции (все углы — в радианах)
F2XM1

Вычисляет значение (2*х-1)

FCOS

Вычисляет косинус

FPATAN

Вычисляет арктангенс

FPTAN

Вычисляет тангенс

FSIN

Вычисляет синус

FSINC05

Вычисляет синус и косинус

FYL2X

Вычисляет выражение у*1одгх

FYL2XP1

394

Вычисляет выражение у*1одг(х+1)

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

Продолжение табл. 5.1
Команда

Описание

Константы
FLD1

Загружает 1.0

FLDL2E

Загружает 1одге

FLDL2T

Загружает Log210

FLDLG2

Загружает Log102

FLDPI

Загружает я

FLDZ

Загружает О

Операторы сравнения
FCOM
FCOMP

Сравнивает действительные числа
Сравнивает действительные числа и удаляет данные из стека

FCOMPP

Сравнивает действительные числа и дважды удаляет данные из стека

FICOM

Сравнивает целые числа

FICOMP

Сравнивает целые числа и удаляет данные из стека

FTST

Сравнивает вершину стека с нулем

FUCOM

Выполняет неупорядоченное сравнение

FUCOMP

Выполняет неупорядоченное сравнение со снятием со стека

FUCOMPP

Выполняет неупорядоченное сравнение с двумя снятиями со стека

FXAM

Проверяет значение 5Т(0) и помешает результат в регистр условия

Управляющие команды
FCLEX

Очищает все немаскированные исключения с плавающей точкой

FNCLEX

Очищает все исключения

FDECSTP

Уменьшает указатель стека

FFREE

Очищает элемент стека, как если бы он был удален из стека

FINC5TP

Увеличивает указатель стека

FINIT

Инициализирует FPU и проверяет наличие исключений

FNINIT

Инициализирует FPU без проверки наличия исключений

FLDCW

Загружает управляющее слово

FLDENV

Загружает окружение FPU

FNOP

Эквивалент NOP

FRSTOR

Восстанавливает состояние FPU из данной области памяти

FSAVE

Сохраняет состояние FPU в области памяти, проверяя исключения

FNSAVE

Сохраняет состояние FPU в области памяти без проверки исключений

FSTCW

Сохраняет управляющее слово с проверкой исключений

FNSTCW

Сохраняет управляющее слово без проверки исключений

ГЛАВА 5. СОЗДАНИЕ МАТЕМАТИЧЕСКОЙ БИБЛИОТЕКИ

395

Окончание табл. 5.1
Команда

Описание

FSTENV

Сохраняет окружение с проверкой исключений

FNSTENV

Сохраняет окружение без проверки исключений

FST5W

Сохраняет слово состояния с проверкой исключений

FNSTSW

Сохраняет слово состояния без проверки исключений

F5TSW АХ

Сохраняет слово состояния в АХ с проверкой исключений

FNSTSW AX Сохраняет слово состояния в АХ без проверки исключений
WAIT

Приостанавливает процессор до завершения операции сопроцессора

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

Синтаксис операндов

Влечет использование

Классический

Finstruction

ST, ST(I)

С памятью

instruction memory

ST

С регистром

^instruction ST(n), ST
f instruction ST, ST(n)

Нет

С регистром со снятием

^instruction? ST{n), ST

Нет

Все команды сопроцессора начинаются с буквы F. В случае работы с регистром со
снятием со стека команды заканчиваются буквой Р.

Классический формат команд
Классический формат рассматривает стек FPU как обычный классический стек; все
операции обращаются к вершине стека ST(0) и второму элементу стека ST{1). Например,
суммирование выполняется при помощи команды FADD, результатом которой является
сложение ST(0)=ST(0)+ST(1). Таким образом, значение на вершине стека становится равным сумме значений двух верхних элементов стека, как показано на рис. 5.27. В целом
команда использует элементы стека $Т(0) и 5Т(1), если она работает с двумя операндами,
и элемент ST(0), если с одним. При использовании двух операндов обычно верхний операнд снимается со стека, а результат операции помещается на вершину стека.

Формат работы с памятью
Формат команд, работающих с памятью, подобен классическому в том, что стек сопроцессора рассматривается как классический стек. Однако данный формат позволяет
вносить в стек данные из памяти и переносить их из стека в память. Например, можно
загрузить в вершину стека операнд из памяти при помощи команды
FLD память
Снять операнд с вершины стека и перенести его в память можно при помощи команды
FSTP память
396

ЧАСТЬ II. ТРЕХМЕРНАЯ МАТЕМАТИКА и ПРЕОБРАЗОВАНИЯ

До

После FAOO
>F(0}-*- ST(0) + ST(1

Вершина ст etca
TOS

ST