145 35 44MB
English Pages [608] Year 1994
P u bllsher
•
Mitchell Wa ite
Editor-l n-Chief
•
Edito rlai Dlrector Ma naglng Editor content Editor
•
Scott Calamar Joel Fugazzotto
• •
John Crudo
Harry Henderson
Technlcal Revlewers
•
M i ke Radtke and David Bolman
Production D l rector
•
J u l i a n n e Ososke
Design
•
M i cheie Cu neo
Project coordlnator Productlon Illustrations
Kristin Peterson •
the BOOKWORKS
Ben Long
•
Cover Design
•
William Salit
•
•
Ted Mader & Associates
© 1994 by The Walte Group, lnc.®
Publlshed by walte Group PressTM,
200 Tamal Plaza. corte Madera, CA 94925.
Walte Group Press ls dlstrlbuted to bookstores and book wholesalers by Publlshers Group West, Box
8843,
Emeryvllle, CA
94662, 1-800-788-3123
un Callfornia
1-510-658-3453).
All rlghts reserved. No part of thls manual shall be reproduced, stored in a retrieval system. or transmltted by any means, electronlc, mechanlcal, photocopylng, desktop publlshlng, recordlng, or otherwlse, wlthout permlsslon from the publlsher. No patent llablllty ls assumed wlth respect to the use of the Information contalned herein. Whlle everv precautlon has been taken in the preparatlon of thls book, the publlsher and author assume no responslbillty for errors or omlsslons. Netther ls any llablllty assumed for damages resultlng from the use of the Information contalned herein. All terms mentloned ln thls book that are known to be reglstered trademarks, trademarks, or servlce marks are llsted below. ln addltlon, terms suspected of belng trademarks, reglstered trademarks, or servlce marks have been approprlately capltallzed. Walte Group Press cannot attest to the accuracy of thls Information. use of a term ln thls book should not be regarded as affectlng the valldlty of any reglstered trademark, trademark, or servlce mark. The walte Group ls a reglstered trademark of The Walte Group, lnc. Walte Group Press and The walte Group logo are trademarks of The Walte Group, lnc. Borland C++ ls a registered trademark of Borland International, lnc. All other product names are trademarks, reglstered trademarks, or servlce marks of thelr respectlve owners.
Prl nted ln the United states of Amerlca 94 95 96 97
•
10 9 8 7 6 5 4 3 2 1
Library of congress cataloglng-in-P u b l lcation Data Lampton, Chrlstopher
1
Ga rde n s of Imagi nati on : programml ng 3D maze games in Borland C++ Christoper Lampton. p.
cm.
l ncludes b l bllographical references and Index. ISBN: 1-878739-59-X: $34.95 1. Computer games. GV1469.2.L36 794.8'15365--dc30
1994
2. Borland C++.
1. Title
94-7917 CIP
Dedication For Susan W'iener, without whom this and quite a few other books could never have been written. Andfor Lyric, in memoriam.
About the Author Chrisropher Lampron is the author of more than 80 books for readers young and old. These include 20 books on microcomputers and computer programming, i n c l u d i n g introductory b o oks on BAS I C , Pas cal , and assembly lan guage p r o g r a m m i n g , a n d fo u r b o o k s on c o m p u t e r g r a p h i c s a n d a n i m a t i o n programming. He has also written books o n topics as diverse as biotechnology, airline safery, underwater archaeology, sound, astronomy, dinosaurs, the origin of the universe, and predicting the course of epidemics. He holds a degree in broadcast communications from the University of Maryland, College Park, and has worked both as a disk j ockey and as a producer of television commercials for a Maryland TV station. When he is not writing, programming, or indulging his hobby as a fa natic comp uter gam er, he serves as As sociate Sysop (system o perator) of the Game Publishers Forums (GAM PUB) on the CompuServe Information Service. He is also the author of Waite Group Press' Flights of Fantasy and Nanotechnology Playhouse.
Table of contents
INTRODUCTION
. .
. ... .
.
.
. . . . . . . xvii
INSTALLATION
................ xix
CHAPTER ONE
Lost in the Möze..........
CHAPTER TWO
ßösic Gröphic Techniques ....
CHAPTER THREE CHAPTER FOUR CHAPTER FIVE CHAPTER SIX
ßösic Input Techniques
15
..... 61
Mönipulöting ßitmöps . .. . .
105
PolldQOn Mözes.... ..... Texture Möpping. .
I
161
.... .. 193
CHAPTER SEVEN
Spöce: The Finöl Frontier ...
227
CHAPTER EIGHT
Röy Tröcing .... . ....
259
Röy Cösting ....
293
CHAPTER NINE CHAPTER TEN
Heightmöpping ..
CHAPTER ELEVEN
Lightsourcing..
CHAPTER TWELVE
Optimizötion
CHAPTER THIRTEEN APPENDIX A APPENDIX ß INDEX
.
.
. .. .
. .. . .. 357
. ...... 399
.. .. . . .. 459
Putting lt All Tagether
..... 521
.............. .. 551 . . .
.. .
.
...... .
.
563
..... . .. . ... . . 573 .
contents
� INTRODUCTION � INSTA L LATION
.
..
..
.
...
....
.
......
. ..... ...
.
.....
....
..
.
.....
.
....
.
..........
............. ....
� CHAPTE:R ONE: Lost in the Mdze .
.. .
. . ...
. . . ..
....
..
..
..
.
. . ..
.......
..
..
....................
...
..
.
xvu
.....
XIX
.........................
...........................
... . .. .. . .. .
.
.
.
.
......
. 1 .
The Once-and-Future Maze Game . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 . . . . . . ? Face to Face with (Virtual) Reality . . . . . . . . A Maze in Wolfs Clorhing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Types of Mazes . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 2 Wireframe Mazes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 2 Bitmapped Mazes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 3 Polygon Mazes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 3 Ray-Cast Mazes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . .. . . . . . . . . . . . . . . . . 1 3 Animation Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . 1 4 ...
..
..
.....
....... ..... ..... .
� CHAPTE:R TWO ßdsic Grdphic Techniques
... .................
...................................... 1 5
Video Memory: A Brief Tour . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. .. .. . . . . . . . . . . . . 1 8 Offsets and Acidresses . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 8 Manipulating Memory with Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 9 Mode 1 3h 21 Setting the Video Mode . . . . . . . . . . .. . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 Finding Video Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 The VGA Palette . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 A Screen-Clearing Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 B uilding a Wireframe Maze . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 The Carresian Coordinate System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 Plotring Pixels . . . . . . . .. . . .. .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . 33 Drawing a Line . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . .. . . . . . . . . . . 35 Endpoints . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 Horizontal and Verrical Lines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 Lines of Arbitrary Slope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 Bresenham's Algorithm . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .43 At Last . . . the Maze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 One Square at a Time . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 ............. ...................................... ............ ....................................
Contents
� CHAPTER THREE ßdsic Input Techniques
...
.
.
........... .....
.... . .. .. .
..
.
. ......
61
The PC Keyboard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 Two Keys at a Time . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64 The Keyboard lnterrupt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . .. . . . . . . . . . . . . . . . . . . . . . 65 Swiping the lnterrupt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 The initkey() Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .67 The remkey() Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .69 The newkey() Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .69 Using the Keyboard Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 The SCANKEY Program . . . . . .. . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .74 Removing the Keyboard Handler . . . . . . . . . . . . . .. . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 76 The PC Mouse The Mouse Driver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . 77 The initmouse() Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 The readmbutton() Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 The relpos() Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 Using the Mouse Functions . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . 8 1 The MOUSE Program . . . . . . .. . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . .. . . . . .. . . . . . . 82 The PC Joystick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . 84 Analog Joysticks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 Reading the Joystick Buttons . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 85 Determining the Stick Position . . . . . . . . . . . . . . . . . . .. .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 Using the Joystick Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 Calibrating the Joystick . . . . . . . .... ............. . . . . . . .... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 The JOYSTICK Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 The Event Manager . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. .. . . . . . . . . . . . . . . . . . . . . . . . 9 1 Maze Events . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. .. . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . 93 The init_events() Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 The Joystick Calibration Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 The getevent() Function . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . 95 Animaring the Maze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Fixing the Flicker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 00 The GOMAZE Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 0 1 ................................................................................................
� CHAPTER FOUR Mdnipuldting ßitmdps
...........
..
.
. . . . .
. .... ... .
.
.
.
.........
. 1 05 .
What Is a Bitmap? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 1 09 Graphics File Formats . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . 1 09 The 8-Bit Targa Format . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . .. . . . . . . . . . . . . . . . . . . 1 1 0
GARDENS OF I MAG I NAT I O N
8-Bit Targa Loader 000000000000000000000..000ooo0o0....000..o..0000oo..00000000000000000000000000112 The loadTGA() Functiono0000000000000000000000000000000000000000000000000000000000000000000000000115 An 8-Bit Targa Display Program ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo117 The TGASHOW Programoo0000 000 000000 000000 0000000000000 00000000 00 00 000 0 Ooo00000 0000 00 00000000119 The PCX File Format000000000000000000000000000000000000000000000000000000000000000000000000000000000000121 Run-Length Encoding oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo122 The loadPCX() Function 00 000 000000000000 00000000000000000000000000000000000000000 000000 0000000000124 The load_image() Function 0000 000000000 0000000000000000000000000000000000000 0000 000000 0000000000125 The load_palette() Function 000 000000000000000 0000000000000000000000000000000 0000 000000 0000000000127 The PCXSHOW Program oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo128 Building a Bitmapped Maze oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo129 Ray Tracing the Maze 0000 000 000 000 000000000 000000 000 000000000000 000 0000000000 00000000 0000000000000130 Nine Views of the Maze 0000000000000000000000000000000000000000000000000000000000000000000000000000131 Slicing and Dicing the Maze 000000000oo0o0o0ooo000oooo0000oo0oo0ooo0o000o0000000000000000000000 00134 The Maze-Drawing Algorithmoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo134 The Bitmapped drawmaze() Functionooooooooooooooooooooooooooooooo000000000000000000000000140 The draw_slice() Function 0000000000000000000000000000000000000000000000000000000000000000000 0000149 The bitmaze() Programooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo149 Tauring the Mazeoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo157 Refining the Maze Engine000000000000000o00 0o00000oo0o000o0o0000o00000000000000000000000000000 00158 Flying Through the Maze 00000000000000o000000000000000000oo00oooo 0000000000000000000000000000000160 An
0
0
0
0
0
0
� CHAPTER FIVE Polygon Mdzesoooooooooooooooooooooooooooooooooooooooooooooooooooooooooo161 Filled and Unfilled Polygonsoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo164 Filling a Polygon 0000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000167 The polytype Structure 00000000000000000000000000000000000000000000000000000000000000000000000000000169 Polygon Clippingoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo169 A Polygon-Drawing Function 000000000oo00000 0000000000000 0000000000000000000000000000 0000000000171 The Polygon-Drawing Algorithm oooooooooooooooooooooooooooooooooooooooooooooooooooooooooo..o171 Clipping Against the Left Edge of the Viewportoooooooooooooooooooooooooooooooooooooooo173 Calculating the Slope 0 000000 000 00000 000000000000 0000 00000000000000000000000000000000000000000000000174 The Poim-Slope Equationooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo175 The Drawing Begins 0000000000000000000000000000000000oo000o0o0000000oooo00oo000000000000000000000000175 Clipping Against the Right Edgeooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo176 The Error Terms 0000000000000000000000000000000000000000000000000000000000000 0000000000000000 Oo00000000177 The Main Loop 00000000000000000000000000000000000000 0000000000000000000 0000000000000000000000000 000177 Drawing the Lineoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo178 The Final Details 0 000000000000 0 Oo00000000000000000000000000000000000000 0000000000000000 0000000000000000178 The polydraw() Function00 000000 000 0000000 Oo0000000000000000000000000000000000 00000000000000000000180 The POLYDEMO Programoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo182 0
0
0
0
0
0
Contents
The POLYCLIP Program A Polygon Maze A New Maze-D rawing Function The New drawmaze() Function Touring the Polygon Maze
184 185 186 188 190
........................................................................ . .
................. . . . ........................................................................
.... . . .........................................................
.......................................... . . . . ..............
.................................................... . . . . .............. . .
@] CHAPTER SIX Texture Mdpping Two-and-a-Half-Dimensional Texture Mapping The Two-Step Program Bitmap Scaling The Vertical Error Term The Horizontal Error Term Drawing a Pixel The BITSCALE.CPP Program Running the Program Texture Mapping The Ground Rules The Texture-Mapped Polygon Function The polytext() Function A Textured Polygon Demo The TEXTDEMO Program Running the Program A Texture-Mapped Maze Running the Program Going Three-Dimensional
193 196 196 197 199 200 201 202 206 206 207 208 212 215 215 217 218 226 226
................................................ ........
...................... ......................
..................................................... ........................
. . . ...................... . . . . . . . . ................... ......................................
.. . . .... . . . ................................................... . . . .. . . . . .....
.................................................................... . . .
... .................................................. . . . . ...............................
..... . . . . .............................. ....... ...................
. . . ................ . ......................... .... . . . . ..........................
.................. . ......................... ...............................................
. . ............................... . . . ................................................
.................................. ........ . . .......
..................................................... . . . . . . .................
............................. .............. .............................
.....................................................................
....... . . . ..... . . . ................ .... . . ..................................... . .
...................................................... ......... . . . .............
............................... . . .. . . ......... . . . . .............................
... . . . . ..... ............. ............ ........................... . . . . . . . .
@] CHAPTER SEVEN Spdce: The Find I The Third Dimension Three-Dimensional Cartesian Space The Three-Dimensional Illusion Putting Perspective in Perspective Two-and-a-Half Dimensions? Direction Vectors Vector Magnitude Polar Coordinates Polar Direction Vectors The Unit Vector The Unit Circle Rotaring a Point
Frontier
227 231 232 233 234 238 241 244 247 250 253 254 254
....................................
............................. . . . . . .................................................
....................................................... . .
............ . . . ...... . . . .......................................
. . . ...........................................................
..................................................... . . .................
............................... ....................... ................. . . . ................
............................ . . ....................................... . . ..............
....... . . ................................................ .................................
....................... .................................... . . ... . . . ..........
............. . . .................... ........... ..........................................
............ . . . . . . . . ............ ................................... ............... . . ....
.............................................................. . . . . .................. . . . .
GARD E:NS OF IMAG I N ATION
Coordinate Transformations Translating a Point .. . Rotating a Point .. . Three-Dimensional Rotation Let 'Er Rip
.............................................
..............
.
..
..............
.. .. . . . ....
.
...
..
. . ..... . .
...
.
255 256 256 257 258
...........
..............................
.
....................... ..... ..................................................
.
.................................................. .............
............... .....................................................................................
259 � CHAPTER EIGHT Rdy Trdcing . . .. .. Soaking Up Rays . . ....... 262 Looking at the World . 262 A Ray Tracer of Our Own .- .........................................................................264 Low-Resolution Rays ................................................................................268 Defining the World...................................................................................269 A-Tracing We Will Go ... . . 272 Follow That Ray .......................................................................................272 The Heart of the Ray Tracer . . . .. . 276 .... . 280 A Ray-Tracing Function Tracing a Sphere . . .. ... . . .. . 282 The lightsource() Function .......................................................................284 The plot () Function ... ..... 285 The RTDEMO Program . . . 285 Extending the Ray Tracer . .. 291 Ray-Traced Mazes? .. . 291 ...
...........
..
...
....
..
..................................... ....
................................
.................................
.....................................
..............................................
...
..
...
....
................................................................
...................................................
........................................................
..
..
....
....... .
.
..
....................
........................
....
.....
.
. .
.
..
.... .
.
.....
..............
..............................
..............................................
...............................................
...................................
..............
.
..
....................
................... .
..
....................
. ................................................ ......................
� CHAPTER NINE Rdy Cdsting ..............................................................293 Reducing t he Number of Comparisons .. 296 Reducing t he Number of Rays ......................................................................296 Casting Rays .................................................................................................297 Follow That Ray . 298 Wallcasting ....................................................................................................298 The Fine Coordinat e Syst em . 300 Rays Within the Grid .. . . 302 The Basic Ray-Casting Alg orithm . . 303 Rays in All Directions . .. 305 The Wallcasting drawmaze() Function .. . 306 St epping Through the Columns . . .. ... 308 A Touch of Trig . . .. 308 Rotating the Ray.......................................................................................309 Casting the Ray ........................................................................................311 The Slope of the Ray . . . .. . . .. ... .312 ......................
.
................................
..........................................................
................................
............... .....................................................
.........................................................
.
................. .........................
...
..............
.................
............................... .......................................
...........................
....
...
........
....
.......................................
..
.............................................................
.......
...........
.........
.....
......
........................
..
.
.
........
.....
............
......
.......................
Contents
. . Casting in a Loop Calculating the Point of lntersection Drawing the Wall. The drawmaze() Function The WALLDEMO.CPP Program Running WALLDEMO.EXE Ray Casting and Texture Mapping Finding the Bitmap Column Clipping the Column The Error Term Revisited . The Texture-Mapped Ray-Cast drawmaze() Function . Floorcasting . The Similar Triangles Method . Drawing the Ceiling The Floorcasting drawmaze() Function A Complete Ray-Casting Engine The Floorcasting, Wallcasting draw_maze() Function .
313 313 315 318 321 324 326 328 328 330 332 337 340 343 344 347 348
...................... ........ .....................................................
.........................................................
...................................... ................................. . . . . . . . .......
.........................................................................
.............................................................
.......................... .........................................
................................................................
.....................................................................
...............................................................................
....................................... .................................
...
...........................
............................................... ....................................................
............. .....................................................
..................... ...........................................................
.............. ......... ..............................
....................... ............................................
..... .........................
@] CHAPTER TEN Heightmdpping . Block-Aligned Heightmaps . The floorheight[] Array The wall[] and flor[] Arrays Heightcasting . How High the Viewer? Changes in Floor Height . . The Block-Aligned draw_maze() Function The BLOKDEMO Program Heightmapped Ceilings . .. Tiled Heightmaps . Creating Height Tiles . . The Tiled floorheight[] Array . . . . The floorbase[] Array . The Tiled flor[] and wall[] Arrays . . A Tiled Heightmapping Engine The Tiled Heightmapped draw_maze() Function The TILEDEMO.CPP Program . Hybrid Heightmapping Methods Animated Heightmaps ................
..........................
357 360 361 361 363 . 364 . 365 371 378 . .. 381 382 . 383 384 384 385 386 389 . 392 . 396 397
.......................... ......... ......
................................................
............................................................................
........ . . . . . .........................................................
................ ...........................................................................
........................................................................... .
.................................. ....
............. ..
.......... .........
......................... .......................
.....................................................................
...........................................
.....................................................
.....
..........
......
......
...................................
............................... . . ..................
....
.................
....
...................................................................
..
............
.......... .. ... ..................... . ........................
................................................. ............
........................... ........
......... ..................
............... ......................
................................................
..............
............ ................. . . ................................
.
.......................................................................... . . . .....
GARDENS O F I MAG I NATI O N
� CHAPTER ELEVEN Lightsourcing .. . . ... 399 Casting Some Light on Light .......................................................................402 The Inverse Square Law . . .. . 404 The Lightsourcing Formula .. . .405 The MAKELITE.CPP Program................................................................414 Using the Lightsourcing Tables .. .. . 417 The LITEDEMO.CPP Program...............................................................419 Lightsourcing in Three Lines or Less.........................................................422 The Lightsourced draw_maze() Function . 423 Alternate Lightsourcing Effects . . . . . 430 Lightmapping 432 The Lightmapped draw_maze() Function . .434 The MAPDEMO.CPP Program .. ..... . .441 Tiled Lightmaps . ... 445 The Tiled Lightmapping draw_maze() Function . 446 The TLMDEMO.CPP Program . 453 Using Lightmaps.......................................................................................458 .....................
...
......
............... ...............................
............................
......................
........
..............
....
....
..........
........
..
.........
...........
...
..................................
..............
....................
.........................................
... . . .....
.................................
...
......................... ........ ..............................................................
.. .............................................
.....
................
.............
........
.................. .......................
... ....................................................
..................................... .
...........................
............... ....................
� CHAPTER TWE LVE Optimizdtion .......................................................459 The Target Machine ......................................................................................462 A Few Optimization Tricks . .. .462 Translating into Assembly Language . .462 Unrolling the loop . ... 463 Table Look-Ups . . 464 Fixed Point Arithmetic . 465 Where to Optimize? . 466 More About Fixed Point Math . . . 467 Follow That Decimal! . . . 470 Shifting Left and Right . 471 Arithmetic Shifts . 472 Double Precision Shifts ........................................................................472 A Fixed Point Multiplication Function......................................................473 A Fixed Point Division Function ...............................................................475 Fixed Point Trigonometry . . 476 THE MAKETRIG.CPP Module . . 481 Optimized Wall D rawing . . . 484 The drawwall() Function . .. . .. . 488 Optimized Floors and Ceilings . . .. .489 .
. .........
.
...........................................................
................................. ......................
...... ..............
..........
.......
..... .......................................................
.....................................................................
...... .....................
..................................................
.............. ..............................................
.........................
............................................................
.
.........
............................
..................
.
.
.....
......................................
..........................................................
................................. ................................................
..................................................................... ..
...........................................
............... ..
...................................................................
...... ..... .
.........
. .........................
.............................
.....
......
.
.....
. ..
...................
....................
The Optimized draw_maze() Function The Optdemo Program Running OPTDEMO.EXE Optimization Review
500 508 517 517
...... ............ ........ ...............................
................................................................ .................
.......... ............... .......... .. . . . . . . . ..........................
........ ........................ . . . . .. . . . . . . . . . . . . . . . . . .............................
� CHAPTER THIRTEEN Putting lt All Together Adding Objects to the World Hidden Surface Removal Column by Column Drawing The distance[] Array Representing Objects Drawing the Objects The Painter's Algorithm The drawobjecr() Function Adding Game Features Bells and Whisdes The Opening Credits The Automap Tracking the Player Playing the Game
521 524 524 527 527 528 531 535 537 539 541 541 546 548 549
...................................
. . . . . . ........................................ ..........................
.................................. . ... ....................................
......... ............................... . . . ............. ...........
................. . . . . . . . . . . .. . . .. . . ....... . . . . . . . . . . . ............................
. ................ . . ............ ................. .......... ...... ................
. . .................. . . . ........... . . . . . ..........................................
................. . .............................................. ....... . . . . .
.......................................................................
................ ...... . . . . . . .......... . . .... ..... . . . . . ............................
..................... . . ........ . . . . . . .............. . . . . . . . . . . ....... ........ .............
......................................... .......................................
. . . . . . . . . . . . . . . . . . .. . . . . ........... . ................................. . . . . . . . . . . . . ..... . . . . .
. ........... . . . . . . . . . . ... . . . . . . . . . ............ . .... . . . . . . . .. ............ ...........
. . . ........ ... ............ . . . . ............ . . . . . . . . ...... . .. .. . . . ................. .........
� APPENDIX A Credting Gdme Grdphics POVRAY Building a Maze
.... .... ..... . . . . . . ..... . . . . . . . . .
551 554 557
......................................................... . .. ... .......................................
............................... ............................................... . . ....... . .. . .
� APPENDIX ß Assembly Ldngudge 101 Assembly Language Pointers Gerring it into a Register Assembly Language Odd and Ends Passing Parameters to Assembly Language Procedures
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
............... ...................................... . . ...................
563 566 568 569 570
...................... .................... .............. .......................
...............................................................
� INDEX
... . . .................. ............
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
573
GARDE:NS O F I MAG I NATI ON
Acknowledgments A book of this size and complexity is rarely the work of a single individual, even if only one name appears next to the title. More people than I can recall lent a helping hand on this one. Here's a panial list of those to whom I owe a heartfelt thanks: Mitch Waite, my publisher, for letring me rackle another book in the vein of my earlier Flights of Fantasy; John Crudo, my editor, for retaining a sense of humor through crisis after crisis; Scott Calamar, The Waite Group's editorial director, for offering a friendly voice when it began to appear that this book would never be clone; Mark Betz, for generously allowing me to reuse some of the terrific code that he wrote for Flights of Fantasy; and, of course, the denizens of the Game Design section of the CompuServe Gamers Forum, who are always willing to talk about cutting-edge game prograrnming techniques. (You can get there yourself by logging onto the CompuServe Information Service and typing GO GAMERS. Look for game design discussions in section 1 1 .) Although I created many of the graphics images in this book myself, with the aid of the excellent public domain ray tracer P OVRAY and the wonderful commercial art program Fractal Design Painter 2.0., I received additional (and invaluable) graphic assistance from two people: Judith Weller designed several of the graphics tiles used in the chapters on raycasting, including the blue stone wall , the skull door, and the floating skull, using 3D Studio Release 3 from Autodesk Software. Anyone interested in employing Judy's 3D modeling skills can contact her on CompuServe at ID 72662,536. Graphics designer Steven C. Blackmon, of the Digital Image Works, created the model of The Waite Group logo used in chapter 1 3. The image was modeled on and rendered by lmagine, a topnotch (and remarkably inexpensive) 3 D modeler from Impulse Software. Steven can be contacted at CompuServe ID 73774,2504. Finally, I owe special thanks to two people. Game designer Kevin Gliner helped me throughout this project, letring me read the code from his own three dimensional raycasting system (and spending quite a few hours helping me to understand what l 'd read) . Several pieces of Kevin's code have been appropriated for use in this project, with his permission, as described in chapter 1 2. l've altered the code considerably to fit my needs, however, and Kevin should not be blamed for any bugs or clunky code that l 've introduced in the process. lt's only fair to mention that Kevin's raycasting engine runs considerably faster than the example
engine in this book and contains many clever optimizations beyond the ones discussed here. Anyone interested in licensing Kevin's engine can contact him at CompuServe ID 70363,3672. And l'd like to offer thanks once again to Susan Wiener, who helped me keep body and soul tagether during this project. This book is dedicated explicitly to her, and to her recendy departed (and much beloved) canary Lyric, but every other book l've written is dedicated to her in spirit.
Dear Reader: What is a book? Is it perpetually fated to be inky words on a paper page? Or can a book simply be something that inspires-feeding your head with ideas and creativity regardless of the medium? The latter, I believe. That's why I'm always pushing our books to a higher plane; using new technology to reinvent the medium. I wrote my first book in it
as
1973, Projects in Sights, Sounds, and Sensations.
I like to think of
our first multimedia book. In the years since then, I've learned that people want to
experience information, not just passively absorb it-they want interactive MTV in a book. With this in mind, I starred my own publishing company and published Master
C, a
book/disk package that turned the PC into a C language instructor. Then we branched out to computer graphics with Fractal Creations, which included a color poster, 3-D glasses, and a totally rad fractal generator. Ever since, we've included disks and other goodies with most of our books. Virtual Reality Creations is bundled with
3-D Fresnel viewing goggles
and Walkthroughs and Flybys CD comes with a multimedia CD-ROM. We've made complex multimedia accessible for any PC user with Ray Tracing Creations, Multimedia Creations, Making Movies on Your PC, Image Lab, and three books on Fractals. The Waite Group continues to publish innovative multimedia books on cutting-edge topics, and of course the programming books that make up our heritage. Being a programmer myself, I appreciate clear guidance through a tricky OS, so our books come bundled with disks and CDs loaded with code, utilities, and custom controls. By
1994, The Waite Group will have published 135 books. Our next step is to develop
a new type of book, an interactive, multimedia experience involving the reader on many Ievels. With this new book, you'll be trained by a computer-based instructor with infinite patience, run a simulation to visualize the topic, play a game that shows you different aspects of the subject, interact with others on-line, and have instant access to a !arge database on the subject. For traditionalists, there will be a full-color, paper-based book. In the meantime, they've wired the White House for hi-tech; the information super highway has been proposed; and computers, communication, entertainment, and information are becoming inseparable. To travel i n this Digital Age you'll need guidebooks. The Waite Group offers such guidance for the most important sofrware your mind. We hope you enjoy this book. For a color catalog, just fill out and send in the Reader Report Card at the back of the book.
Mirehell Waite Publisher
lntroduction This book is a companion volume of sorrs to my earl1er Waire Group book, Flights of Fantasy. Where Flights of Fantasy concerned rhree-dimensional Bighr simularor programming, Gardens ofImagination rackles a subject that's a bit more down to earth - somerimes even under the earth. Throughout this book, I refer ro the type of game that it's about as "three dimensional maze games," despi te the fact that these games really have no collective name. The maze game encompasses a number of traditional computer game genres, including CRPGs, arcade games, and adventure games. What these games have in common is that each uses three-dimensional graphic techniques to depict the interior of a maze, which may in turn represent the interior of a dungeon, a spaceship, a j ungle, even a castle occupied by Nazi stormtroopers. If you've played Ultima Underworld from Origin Systems, Welfenstein 3D from Apogee Software, Doom from ID Software, Arena from Bethesda Softworks, or any of a host of other maze games to appear over the last few years, you should immediately recognize the type of game that I'm talking about. If not, you have a treat in store. As in Flights of Fantasy, l 've srocked this book fro m cover to cover with computer code demons trating how to dupli cate the effects i n these gam es, including a working computer game developed in the last chapter. This code is intended not as a li brary of routines for the reader to plug into his or her programs (though it's possible to use the code as such) , but as a series of examples to point the reader in the right direction and inspire him or her to produce code that lies even closer to the bleeding edge. Until recently, there was surprisingly li ttle information available in print fo r program mers interested in writing commercial quality compurer games. I hope that this book and Flights ofFantasy have helped to alleviate that situation at least a little.
This book includes a disk containing all the code that appears in these pages. It's probable that changes will be made to that code berween the typesetring of the book and the mastering of the disk. If so, these changes will be documemed in a file on the disk entitled README.TXT. If you have any problern getting this code to do what you want it to, or just want to tell me what you think of the book, you can contact me on CompuServe at ID 767 1 1 , 30 1 . Or log onto the Game Design Section of the CompuServe Gamers Forum (GO GAMERS).
Installation Gardeus of Imagination includes a disk that contains the source code, utilities, and maze discussed in the book. The files are in a compressed format, and should be installed on a hard drive before you use them; you'll need a minimum of 3MB of free hard disk space. To install the files, follow these steps: 1 . Change to root directory of the hard drive on which you wish to install the files. lf the hard drive you wish to install the files on is the C: drive, at the DOS prompt you would type: c: cd\
lf the hard drive is not C:, type the appropriate letter instead. 2. Create a directory for the files on the hard drive called GARDENS by typing: md ga rdens
You may use a directory name other than gardens, if you so prefer. 3 . Change into the newly created di recto ry. To change into the di recrory GARDENS, type: cd ga rdens
4. Insert the Gardeus of Imagination disk into your 3.5-inch floppy drive. l f that
floppy drive is the A: drive, type: a : \ g a rdens -d
(ENTER)
lf it's the B: drive, type b : \ g a rdens -d
(ENTER)
Be sure to include the -d switch after gardens, so that the subdirectories and their files extract properly. Depending on the speed of your PC, the decompression and transfer of the Gardeus of Imagination files may take a couple of minutes.
ou are trapped in a vast maze. It's not that you're a pessimistic kind of person - you always like to look on the bright side of things, even when you need a flashlight to find it - but things are getting pretty grim. You ran out of food half an hour ago. You barely have enough magic left to heal a wart. Your hit points are so low that you can count them on your fingers and toes and still have enough digits left over to play "This Little Piggy. " To make ma tters wo rse, the re's som ething o u t there in the darkn ess, something that's hungry and in a real bad mood. You hold up your Rickering torch and try to get a look at it, but the Harne is so low that you can barely see ten feet in front of you. Of course, that's probably for the best. If you can't see what's out there, maybe it can't see you either. Some fun, huh? Things could be worse. You had the foresight to save your game back in the last town, so even if that thing you hear licking its lips out there in the shadows decides to have you for dinner, you'll only lose 30 minutes of dungeon crawling. And maybe next time yo u'd better crawl in a different dungeon, until you have as much experience as some of those hypermuscular barbarians you've seen hanging around the adventurer's guild in recent weeks. This dungeon is way too tough!
G A R D E: N S O F IMAG I NATI O N
T h e o n ce - a n d - F u t u re M a z e Ga m e Fighring slavering monsters i n a dark, maze-like dungeon may not be anybody's idea of fun, bur when the dungeon is on rhe video display of a computer and rhe monsters are only bits and bytes shuttling through rhe silicon circuitry of a microprocessor, it can be a blast. The sort of computer role-playing game (or CRPG) rhat we've been describing in the last few paragraphs belongs to a !arger genre of computer game, which we're going to refer to in this book as the maze game. CRPGs, however, are only one type of maze game. Other maze games would be better described as arcade games. And still orhers fall into rhe genre known as advenrure games. All of these games, rhough, have one thing in common: They drop the player inro the middle of a maze-like environment, and they depict that environment on rhe video display of the computer using 3D graphics. At a time when the b uzzword "virrual reality" is on every game programmer's lips, these games pur the player right in the rhick of a virtual world. If you l ike computer games, you've probably played more than a few rhat match this description. In rhis book, however, we're going to go beyond relling you how to play such games. We're going to tell you how to create them. If there's anything more fun rhan hacking and slashing your way rhrough a virtual realiry, ir's creating your own virrual world! Maze games are not new. They've been araund almost as long as there have been microcomputers - and maybe a bit longer. The original Crowther and Woods adventure game, which first appeared on mainframes and minicomputers in rhe late 1 960s, fearured a sequence in which the player was trapped in a "maze of twisry lirde passages, all alike," though the maze was only described in words, not illustrated wirh graphics. By rhe late 1 970s, however, crude micromputer maze games had begun to appear. Some of these games rendered rhe interiors of mazes using simple line drawings, in which passageways were depicted by parallel lines extending away from the viewer toward the vanishing point, like something from rhe skerchbook of a Renaissance artist who had j ust discovered the laws of perspective. We'll refer to this type of depiction as a wireframe maze, by analogy to the wireframe graphics used in early microcomputer flight simulators. As crude as the graphics might have been, some of rhese early maze games were quite entertaining - and a couple of them spawned lines of sequels rhat are still appearing today. Perhaps the most successful of rhese early maze games was Ultima, published in 1 9 8 0 by rhen teenaged programmer Richard Garriott for rhe Apple I I . Though most o f the game took place o n a scrolling rwo-dimensional map, the inrrepid player could enter a series of dungeans scattered around rhe Iandscape and feast his or her eyes on state-of-the-art 1 980 wireframe maze graphics. Ir wasn't necessary to enter rhe dungeans to win the game (though in later Ultima 4
CHAPTER O N E:
Lost i n the Mdze
Figure 1 -1 A scene from one of the early Wiza rdry games. The graphics may have been simple, but the games were big, complex, and q u ite popular
games it would be), but they provided an entertairring treat for the player who wanted to experience a crude kind of virtual reality. So successful was this early computer role-playing game that Garriott parlayed its popularity into his own game publishing hause, Origin Systems, which is still producing Ultima sequels today. Not bad for a game that was originally written in Applesoft BASIC. The Wizardry series of games, begun in 1 982 by Sir-Tech Software, carried the wireframe maze concept a step fanher. The first five Wizardry games took place entirely in wireframe mazes, using high resolution color graphics only for the monsters that periodically assaulted the players (see Figure 1 - 1 ) . Like the Ultima games, the Wizardry series is still being produced in the 1 990s and shows no signs of waning in popularity. It would be difficult, if not impossible, to catalog all the maze games that appeared in the subsequent decade. Many of these games, such as Labyrinth and Asylum, were designed for microcomputer systems that have long ago passed on to silicon heaven. (That particular pair of games appeared originally on the Radio Shack TRS-80 Model I in the early 1 980s.) Others, like Slaygon on the Atari ST and Commodore Amiga, came and went so quickly that only rabid aficionados of the genre remernher them at all. Nonetheless, dedicated microcomputer gamers have warm memories of such popular maze-sryle games as Electronic Art's 1 986 The Bard's Tale series, which updated the Wizardry-type dungeon with detailed color graphics and, eventually, cleverly animared monsters, or SSI's "gold box" series of CRPGs, which debuted in the late 1 980s with Pool of Radiance and spawned more sequels and quasi sequels than any other game published up until that time (see Figure 1 -2) . For
GARDENS O F IMAGI NATI ON
Figure 1 -2 One of several dozen three-di mensional mazes from Pool of Radiance, the first of SSI 's h i g h l y successful series of "gold box" CRPGs
microcomputer gamers who owned an ST, an Amiga, or an Apple IIGS during that same period, perhaps the most fondly remernbered of all maze games is Dungeon Master (FTL Games) , which used bitmap images and digitized sound effects to create a dungeon maze so startlingly realistic that players tend to remernher it not as a game that they once played but as a place they actually visited (see Figure 1 -3 ) . Eventually, Dungeon Master was translated from the Atari, et al. , to the IBM PC and its clones, which by the early 1 990s had become the preeminent platforms for microcomputer games; but by that time it had spawned a series of imitations that were nearly as entertaining as the original. �:: ,· t;:F-
:tAJ:OO .,_.
=
./
'
111
t::tJFLH�:: ,.
Hf1L.� !'!--..
/
...-
.,... _ .
/
� ,
· \' -:�·r:·T_....,.:.· .=,:·�':.''T': "'-�·.--:{ ·..-, ;::,�-- · -�··.::: "'"'.·F ·'· :�'-·i · 7· . ·-� , ..
,�-'r
;-
· -- � � -� � ---
i : -· �
-� · �- : ":_ - ! . . . -- .-.-:: >
- ---
·
.
·�
::lo!l ':II
--
-�-�-
- � �-i: - �� · - :Jr ·. I: . · --
-; :-' .
' ]_'_- ..· -I
- �� Z�"Y-:��:::;=i:
Figure 1 -3 The most realistic maze game of the 1 980s, D u ngeon Master is regarded by many fa ns as the best CRPG ever p u b l i shed
CHAPTE:R ONE:
Lost i n the Mdze
Figure 1 -4 The best o f t h e Dungeon Master clones was Eye of the Beholder, p u bl ished by SSI and crafted by the progra m m i n g team at westwood Associates
The best of these Dungeon Master clones was SSI's Eye of the Beholder, from the programming team at Westwood Associates (see Figure 1 -4).
Face to Face with Rea l ity For all its bone-chilling realism, Dungeon Master turned out to be merely a precursor to the super-realistic maze games of the 1 990s. Dungeon Master may have featured superbly detailed maze graphics, and arguably the best mouse driven interface ever applied to a microcomputer game, but the method used to animate the maze bare more than a passing resemblance to that used almost a decade earlier by Richard Garriott in the first Ultima. The player moved araund the dungeon by pressing the arrow keys on the computer's keyboard (or clicking with the mause on equivalent screen icons) . For every press of a key, the player moved a fixed distance forward down a hallway or rotared in place by 90 degrees. All movement was in quantum leaps: lt was not possible to move a shorter distance down the hallway or rotate in place by only 45 degrees. Such coarse movement controls were good enough to allow players to suspend disbelief and project themselves mentally into the dungeon, but they weren't truly realistic. In the real world, movement is smooth and fluid, not discontinuous. (Physics fans may respond that this argument breaks down when applied to panides the size of, say, electrons, but it's essentially true for large-scale objects such as human beings.) By contrast, computer flight simulators such as Microprose's F 19 Stealth Fighter and Speerrum Holobyte's Falcon have lang shown game players that it is
GARDE:NS O F IMAG I N ATION
possible to create a computer game with three-dimensional animation that is smooth and fluid, much more like movement in the real world. Armchair pilots flying the Piper Cherokee Areher in Microsoft's Flight Simulator had nearly total freedom of movement. Computer-simulated airplanes don't move ahead by sudden leaps (except on computers with sluggish CPUs) . They slide through space in a continuous motion. And when they rotate, they do so by fractions of a degree. Of course, this simulated movement takes place high in the sky, with the nearest visible objects a safe distance away. Would it be possible to transfer the fluid animation of flight simulators to the interior dungeon environments of a maze game? Yes, it would be. The first major maze game to utilize the techniques of flight simulator programming in depicting the dank interior of a dungeon was Ultima Underworld, a distant descendant of Richard Garriott's 1 980 CRPG. (A much earlier Atari 800 game called Way Out, from Sirius Software, also used flight simularor techniques to create a remarkably effective maze environment, but only the most fanatic computer gamers - the author of this book, for instance remernher that this game existed.) Published in 1 992 by Origin Systems, Ultima Underworld allowed the player to crawl through the slimy environs of a vast underground complex, all of it depicted with flight simulator-style animared graphics. While screen shots from the game, such as the one in Figure 1 -5 , may not look as realistic as those from Dungeon Master or Eye of the Beholder, rhe smoothness of the animation more than compensated for any shortcomings. The Underworld dungeon may not have looked real, but it felt real, with the kind of pit-of-the-stomach believability rhat computer games very rarely achieve. Ultima Underworld brought the maze game into the age of virtual reality.
F i g u re 1 -5 U ltima Underworld brought the maze game into the a g e of virtual reality
CHAPTE:R O N E
Lost i n t h e Mdze
A M a z e i n Wo lf's C l oth i n g Ultima Underworld may be the most realistic and detailed maze game to come down the pike so far, but only a few months after it was released, another maze game appeared that made even seasoned Underworld players shake their heads in astonishment. Called Wolfeostein 3 0 and markered as shareware by Apogee Games, it featured a maze environment that was in some ways less believable, and certainly far less detailed, than the one in the Origin game (see Figure 1 -6) . But what it lacked in realism, it more than made up for in sheer velocity. Wolf 30, as it is popularly known, was by all odds the fastest maze game ever published. Like Underworld, it allowed the player to move smoothly and fluidly through a maze of rooms and passages, but at a far more hectic pace. Wolf 30 was pure action. Tossing aside most of the role-playing features of earlier maze games, Wolf 30 was a compulsively playable arcade game. lt was also the first of a new genre of maze game. The programmers at Id Software, who had developed Wolf 30 for Apogee, markered the animation engine from that game to other programmers who wanted to produce their own Wolf 3D-style games. One of the first of these, Blake Stone: Aliens of Gold (written by Jam Software), was released by Apogee in late 1 993 (see Figure 1 -7) . In the meantime, the programmers at Id proceeded to develop a more advanced version of the Wolf 30 animation engine, with more features than the original. This engine became the basis for a game called Doom (see Figure 1 -8), published in December 1 993, which took place in a far more realistic maze environment than the one in Wolf 3 D . Doom introduced lightsourced 30 graphics and heightmapping to the Wolf 3D-style maze game. Translated from computerese,
Figure 1 -6 The wildly popular Wolfenstein 3D introduced a new type of maze game that com bined elements of CRPG and arcade game with smooth three-di mensional a n i mation
G A R D E N S OF I MAG I NATION
F i g u re 1 -7 Blake Stone: Aliens o f Gold uses t h e a n i mation e n g i n e developed for Wolfenstein 3 D
that means that the maze in Doom is illuminated by changing and moving light sources and that the floor rises and falls. Stairs lead from one dungeon level to the next. Walls fade in and out of view as they move closer to and farther away from the p layer's lantern. Lights blink on and off. (Much of this was also true of Ultima Underworld, but Wolf 3D used a different - and potentially faster style of animation than Underworld, as we'll see later in this book.) Inspired by the success of Wolf 3D, more "smooth-scrolling arcade games" (as they are sometimes called) appeared in 1 993, including Shadowcaster from Origin Systems, which also used the Id animation engine. Bethesda Softworks
Fig u re 1 -8 Although w ritten by the same program mers who produced Wolfenstein 3D, Doom features even more soph isticated and i n n ovative maze graphics tech n i q ues
CHAPTER O N E
Lost i n t h e Mdze
Figure 1 -9 Term inator: Rampage is a maze game based on the popular series of fi lms
published a maze game called Terminator: Rampage (based on the popular film series) ; in 1 993 (see Figure 1 -9) and The Elder Scrolls: Arena in early 1 994 (Figure 1 - 1 0) . Westwood Associates, now with their own label separate from SSI, released a Dungeon Master-style CRPG called Lands of Lore in the summer of 1 993. As the mid- 1 990s neared, it was dear that the maze game was the hottest thing going in the world of computer games. This left a lot of computer game programmers scratching their heads and asking the inevitable question: Just how do you write one of these games, anyway?
Figure 1 -1 0 The Eider Scrolls: Arena applies Wolf 3D-style an imation techniq ues to the CRPG game
GARDENS O F I MAG I N AT I O N
Types of M a zes The short answer to that question is that there are several different ways to write a maze game. In fact, it could be argued that there are as many different ways of writing a m aze game as there are programmers writing them. However, we can break the vast majority of maze games down into four basic types, each of which is programmed in a different manner. In this book, we'll refer to those types as wireframe mazes, bitmapped mazes, polygon mazes, and ray-cast mazes . (I t's also possible to divide maze games into genres such as CRPGs and arcade games. In this book, however, we'll be talking mostly about the technical details of implementing such games, not the higher-level details of gam e design. This should not be taken to mean that game design is not important, j ust that it deserves a great deal more space than I would be able to give it in this book.) Each of the maze games mentioned earlier in this chapter falls into one of these four categories:
� �
Wi refra me mazes: The early U ltima a nd Wizardry games Bitmap ped mazes: The Ba rd's Tale series, oungeon Master, the 551 "gold box" games. Eye of the Beholder, the later Wizardry games, Lands of Lore
�
Polygon mazes: U l tima U nderworld 1 a nd 2
�
Ray-cast mazes: Welfenstein 3D, ooom, Blake 5tone: Aliens of Gold
Although some of these types of maze game may look superficially similar to the player, from the programmer's point of view each requires a very different approach.
Wi refra me M a z e s Wireframe mazes are the siruplest type o f maze to program and require the least CPU power and memory resources to animate, which is why this approach was used in most of the early microcomputer maze games. All the programmer needs in order to create this type of maze is a fast line-drawing rotttine and the patience to figure out where to put the lines on the display. The programming techniques required for creating a wireframe maze are simple and straightforward, and no advanced mathematics are required. We'll discuss wireframe maze techniques in some detail in the next chapter.
CHAPTER O N E:
Lost i n the Mdze
B it m a p p e d M a z e s Bitmapped mazes aren't much more difficult to program than wirefrarne mazes. In fact, it could be argued that they're a little easier, since no line-drawing routine is required. Bitmapped maze graphics are pieced tagether from predrawn images of walls, floors, ceilings, and bits of set decoration. The only catch is that a competent artist is required to draw these images (though in some instances a ray-tracing program will suffice). Once drawn, gerring the images on the video display is a piece of cake, relatively speaking. We'll discuss bitmapped mazes in more detail in chapter 4.
Po lyg o n Ma zes Polygon mazes are the most difficult type to program. The techniques used to a n i mate such mazes are e s s e n t i a l l y the same as those u s e d t o a n i m ate microcomputer flight simulators, and they require at least a passing knowledge of trigonometry and geometry on the part of the prograrnmer. The resulrs, however, can be worth ir. Polygon mazes are potentially more realistic than rhe mazes created using other merhods, but they require rhe programmer to overcome a series of difficult technical problems, including hidden surface removal, polygon clipping, rhree-dimensional database design, and texrure mapping. lronically, the im ages in a polygon maze program are o ften less realistic than those in a birmapped maze game, but the greater realism of continous, flighr simulator-style movement more than makes up for this lack of realism. We'll talk about polygon mazes at some length in chaprers 5 and 6.
Ray- Cast Mazes Ray-casr maze techniques are surprisingly easy to master and can produce effecrs almost as vivid and realistic as those found in polygon maze games. Ray-casting rechniques use simple principles of oprics to project a derailed image of the floor, walls, and ceiling of a maze onro rhe video display of a compurer. These techniques resemble an exrremely simplified version of the ray-tracing techniques that are used to produce highly realistic animared images for movies and TV (We'll discuss ray tracing at greater lengrh in chapter 8.) The only drawback of ray casting as a method of drawing mazes is that the ways in which the player is allowed to move through the maze must be limited somewhar and the types of objects rhat the player can encounter musr be somewhat reduced relative to a polygon maze game. However, in a cleverly designed maze game, most players will never notice these limitations. Player movement in a ray-casr maze game can
G A R D E N S O F IMAG I N ATION
be every bit as fluid and realistic as in a polygon maze game, and in many cases the animation is even smoother. This combination of programmer ease and high velocity animation makes ray casting the ideal method for creating arcade-style maze games on current generation microcomputers. For that reason, we'll spend t h e last five chap ters of this book discussing ray-casting techn iques and developing a ray-casting animation engine that can be used to produce both arcade-style maze games and CRPGs. We'll even develop a game that uses this engme.
A n i m ati o n P ro g ra m m i n g In the chapters that follow, we'll recapitulate the evolution of the maze game, exploring the whole gamut of techniques used to render more and more realistic mazes on the microcomputer video display. Before we can talk about maze game programming, however, we'll need to say a few words about computer graphics and a n i mation i n general . In the next chapter, we'll discuss the basics of producing images on the video display of an I BM-compatible microcomputer then we'l l use those techniques to create the rudiments of a wireframe maze game, similar in visual spirit to the early Wizardry games.
rogramming the video display of a microcomputer isn't difficult. Most computer program ming languages come with either a builr-in set of commands or a Standard library of functions for purring information on the display. The problern is, these commands and functions aren't necessarily up to the demands rhat a game programmer is likely to place on them. There are libraries of functions available that have been optimized for game programming needs, and many game programmers find these libraries indispensable, bur they will cost you money above and beyond the small fortune that you've al ready laid out to buy your compiler or interpreter. In rhis book we'll develop our own set of graphics functions in Borland C++ and assembly language for outputring graphics to the video display of an IBM compatible microcomputer. The Borland compiler comes with a graphics library called the BGI (for Borland Graphics Interface) , but the functions included in it aren't fast enough or versatile eno ugh to handle the tasks that we're going to throw at rhem. If you have a favorite library of graphics functions that you'd prefer to use, feel free to substitute them where necessary in your own code. However, writing graphics code from scratch isn't a lot more difficult than learning to use a prewritten library of functions - and the graphics code that you write yourself is guaranteed to do exacdy what you want it to do. After you've finished debugging it, anyway.
G A R D E N S O F I MAGI NATION
Vi d e o M e m o ry: A B ri ef To u r The major concept that you need to understand in order to program graphics on an I B M-compatible microcomputer (or j ust about any other type of micro c o m p u t er, for that matter) is the concept of video memory. I nside your microcomputer, usually in one of the slots attached to the motherboard, is a set of electronic circuits that store an encoded version of the image currently on the video display. Change the contents of these circuits and you change the image. Programming graphics is as simple - and as complicated - as that. Up until now, you may not have delved into the esoteric rules of programming a computer on the level of bits and bytes. But it's time to lose your innocence. Programming your computer's memory directly - especially the video memory - may sound intimidating, but it's simple once you learn the rules.
Offsets a n d A d d resses Every type of information that's stored inside your computer - programs and databases, graphics and musical scores - is stored in memory. Yet memory is nothing more than a series of electronic circuits, each of which can store an electrical representation of a binary number - that is, a number made up of the digits 0 and 1 . We don't have to worry precisely how these numbers are represented electrically or even how the binary numbering system works, because most programming languages will do the work of translating other forms of information into the internal form required by the computer and not bother us with the details. However, it helps to understand the essential layout of the computer's memory if you're going to put images on the screen. Each m emory circuit can h o l d a n u m b e r i n the range 0 to 2 5 5 . ( I 'm representing these numbers using the decimal numbering system, because that's the only system that all readers of this book are guaranteed to be familiar with, but the memory circuits store them in binary.) There are commands in the C++ language, as in most other programming languages, that will place a number in a memory circuit or retrieve a number that's already stored in a circuit. Because it's important that data be retrieved from the same memory locations in which it has been stored, each location has an identifying number, called an address, that can be used to distinguish it from other locations. It's traditional to write these addresses using the hexadecimal numbering system. Hexadecimal has strong mathematical ties to binary, which makes it easy to translate from one system to the other; however, it takes fewer digits to represent a number in hexadecimal than in binary, making it by far the less unwieldy of the two systems. Hexadecimal numbers use 1 6 different digits, the numerals 0 through 9
CHAPTER TWO
ßdsic G rdp hic Techniques
plus the letters A through F. The number 65535 in hexadecimal, for instance, is FFFF. In C++ or C, this would be represented as Oxffff, with the "Ox" identifYing the number as hexadecimal. The addresses used to identify memory locations in I B M - compatible microcomputers are made up of two parts: a segment and an offset. The reasons for this are complex and have to do with the nature of the IBM hardware; we're not going to go into what they are. Each of these numbers, the segment and the offset, can fall anywhere in the range 0000 to ffff hexadecimal - that's 0 to 65535 in decimal. The address is written by separating the segment and offset with a colon, like thi s : 1 2 34: 5 6 7 8 . Such a segment-offset pair can also be translated into a linear address by multiplying the segment by 16 and adding it to the offset, though you'll rarely, if ever, need to do this. It's important to bear in mind, though, that two segment-offset pairs that translate into the same linear address represent the same location in the computer's memory, even though the segment and offset numbers may be different. Thus 0000 :00 1 0 and 000 1 :0000 represent the same memory location, because they both translate to the linear address 000 1 0 hexadecimal (or 1 6, in decimal).
Ma n i p u l ati ng M e m o ry with Poi nte rs A C++ programmer can use several different methods to put a numeric value into a memory location. Every time the value of a variable is changed, for instance, one or more numbers are placed in memory locations that have been assigned to that variable by the C++ compiler. To interact with video memory, however, the programmer needs to be able to change the value of specific memory locations rather than locations chosen arbitrarily by the compiler. The most direct way to change the value of a specific memory location is to use a C++ poi nter. A pointer is a variable that represents the address of a location in the computer's memory. In most versions of C++ written for IBM-compatible microcomputers, pointers come in two varieties: near pointers and far pointers. The primary difference between the two is that the programmer can control precisely which memory location a far pointer represents, but can only control the offset of the memory location represented by a near pointer. The segment portion of the address p o i n ted to by a near pointer is determined by the operaring system at the time the program is loaded. Since we need to control both the segment and offset portions of the address in order to interact properly with video memory, we'll be using far pointers exclusively in this book. When declaring a pointer, the programmer must tell the compiler what sort of values it will be pointing to in the computer's memory - int, char, jloat, and so on. In programming video memory, we'll need to point to byte-sized values, so
GARDENS O F IMAG INATION
we'll use pointers to values of type char. A far pointer to values of type char is declared like this: char
far
* c h a r po i n t e r ;
This declares a far pointer called charpointer, which points t o values o f type char. To set charpointer equal to a specific memory address, we use the MK_FP macro, which is defined in the Borland header file DOS.H. In order to use it, you'll need to include DOS.H at the top of your program file. The M K_FP macro - the name stands for "make far pointer" - is used like this: c h a r po i n t e r = ( c h a r
far * )
MK_F P ( Ox7456, O x 0890 ) ;
This assigns the address 74 56:0 890 to the pointer charpointer. The typecast in parentheses in front of the MK_FP macro converts the pointer, which is a far pointer to type void, into a far pointer to type char. After all this work, we're finally ready to place a value in the memory location pointed to by the pointer. To do so we must dereference the pointer, which is clone using the * or dereferencing operator, like this: * c h a rpoi n t e r
=
1 79 ;
This assigns a value of 1 79 to the memory location 7456:0890, assuming that the value of charpointer has not been changed since the previous assignment Statement. To retrieve this value, we can put the dereferenced pointer on the other side of the assignment operator: char
t h i s va r
=
* c h a r po i n t e r ;
This assigns the value stored a t the address pointed to by charpointer to the char variable thisvar. (You'll notice that we've declared the char variable thisvar in the assignment Statement itself This is allowed in C++ at the time of a variable's first use, though it's important to note that the variable will only be valid in the block where it is declared. For instance, if this Statement is placed inside a loop, the variable thisvar will no Ionger be valid when the loop terminates. In technical Jingo, it will "go out of scope. ") If nothing occurs to change the value at that location between these two assignment Statements, thisvar will be set equal to a value of 1 79. The dereferenced pointer *charpointer can be used anywhere wirhin a program that a char variable could be used. To print the value stored at the location pointed to by charpointer as a signed decimal integer, for instance, you could use the printfstatement: p r i n t f < " % i \ n " , * c ha r p o i n t e r ) ;
CHAPTER TWO
ßdsic G rd phic Techniq u es
M ode 1 3 h So what memory locations do you need to change in order to program video memory? And what values do you need to change them to? That depends on several things. Obviously, it depends on what sort of image you want to place on the display. Bur it also depends on what video mode the computer is currendy in. The programs in this book are designed to run on the IBM VGA video adapter, variations on which can be found in the vast majority of current PCs. The VGA adapter can be operared in 24 different "official" modes and quite a few more unofficial ones. Some of these are text modes, which means that the numeric values represent text characters. The remaining modes are graphics modes. When the VGA adapter is placed in a graphics mode, the values placed in video memory represent the colors of the tiny dots on the video display, known as pixels, which make up all graphic images. In this book we'll use the VGA adapter in the "official" graphics mode known as mode 1 3h. (The "h" aft:er the 1 3 indicates that the number is in hexadecimal. In decimal this mode is known as mode 1 9 , but the hexadecimal identifier is more commonly used than the decimal one.) In this mode all graphics are constructed out of a matrix of 320 pixels horizontally and 200 pixels vertically. Each of these pixels can be in any of 256 different colors (which can, in turn, be chosen from a list of more than 25 6,000 different colors) . Mode 1 3h is the most common mode for programming animared video games on the PC. There are a number of reasons for this, the main one being that it is the only Standard VGA mode that supports 2 5 6 simul taneous colors on the display. Some VGA adapters offer additional 25 6-color modes - quite a few even offer modes in which 1 6 million different colors can be simultaneously present on the display - but none of these modes is standardized. They are programmed differendy on different brands of adapters, and on many adapters they are simply not available. Furrhermore, most of these modes feature higher resolutions numbers of pixels - than mode 1 3h. Although this improves the appearance of the graphics, it also tends to slow down the speed of the animation, since a much larger amount of data must be moved around in the computer's memory in order to draw and animate an image. Games that use the higher-resolurion 256-color modes are already appearing on the market, but for the moment they are still outnumbered by lower-resolurion games. A year or two from now, one of these modes may be the Standard mode for video game development, but at present it is mode 1 3h that has that honor. Forrunately, most of the techniques that we'll discuss in rhis book for animaring images on the mode 1 3h display can be used in higher-resolution modes with only minor changes to the program code.
GARDENS OF I MAG I NATION
Setti n g the Video M o d e To make our job easier, the code for turning mode 1 3h (or any other "official" VGA video mode) on and off is built into the video card itself We can use this code in a p rogram by going through the built-in ROM BI OS, the set of program rourines that are installed in the computer at the factory. However, many of the ROM BIOS routines - including the ones for setting the video mode - are best accessed through assembly language modules rather than through high-level programming languages such as C++, so in this book we're going to create a small library of assembly language routines for communicating with the ROM BIOS (as well as with video memory itself) and put them in a file on the accompanying disk called SCREEN.AS M . We'll then l i n k this file di rectly with our C++ programs, so that we can call these rourines from our C++ code as though they were C + + functions. This file w i l l be automati cally as sembled by Tu rbo Assembler (TAS M), a urility program that comes as part of the full Borland C++ 3. 1 package, when its name is included in a Borland PRJ file. (For those readers who lack the full Borland C++ package with TASM, the already assembled version of this code is also included on the accompanying disk under the file name SCREEN.OBJ . Substitute this name for SCREEN.ASM in the project files or make files for all programs in this book that reference it. TASM may be purchased separately for readers using 4.0.) There isn't space in this book for a full-scale primer on assembly language, though we'll return to the subj ect at somewhat greater length in chapter 1 2, when we discuss program optimization. (A brief overview of assembly language programming is provided in Appendix B .) And you don't really need a knowledge of assembly language to use these functions (though it's helpful to know an assembler if you want to revise these functions or write some of your own) . So that you won't be totally in the dark about how these functions work, 1'11 explain how some of the assembly language instructions and conventions work as they arise, but it's not necessary that you grasp every little detail of this code. When we write an assembly language routine that can be called as a C++ function, we must begin it with a PROC Statement: _s e t mode
PROC
This tells the assembler (the program that translates this assembly language code into a form that can be linked with a C++ program) that we're about to create an assembly language procedure called _setmode. (When we write an assembly language procedure that is to be called by name from a C/C++ program, the name of that procedure must begin with an underscore. However, when we call it from C++, we must drop the underscore and refer to it by the name setmodeQ, withour the underline.) We can then use the assembly language ARG directive to
C HAPTE:R TWO
ßdsic G rdphic Techniques
teil the assernhier that we'll be passing a single parameter to this procedure when we call it: ARG
mode : W ORD
This is equivalent to declaring a function in C++ with the function header setmode(int mode). Now we can reference the labe! mode in the text of the procedure, and the assernhier will translate that reference into code referring to the value that we pass from C++. However, for this to work correctly, we must first include a piece of boilerplate code to take care of the technical details: push mov
bp bp,sp
Don't worry about what this does. Just remernher that, without it, the ARG directive won't work properly. We then must move the value of the mode parameter into AX, which 1s a special memory location wirhin the computer's CPU known as a register: mov
a x , [mod e J
Translated into something resembling English, this instruction means "move the value in the memory location that we have labeled mode and place it in the special memory location known as AX." Now that the video mode is in location AX, we must place a 0 in the location known as AH: mov
ah,O
Memory location AH is actually part of memory location AX, though you don't really need to know that in order to understand what this code does. Basically, we are telling the ROM BIOS that it should execute video function 0 (which is the function that sets the VGA mode) to pur the video display into the mode stored in AX, which will be equal to the parameter we passed from C++. Then we execute this instruction: i nt
1 0h
This calls the ROM BIOS itself to do the actual work of changing the video mode. All that's left to do is clean up the mess we made with the instructions that set up the computer's memory for the ARG directive: pop
bp
Finally, we must execute a RET instruction to teil the CPU that it's time to return control from this function back to the program that called it: ret
And we must teil the assernhier that the assembly language procedure 1s finished, with the ENDP directive:
GARDENS OF I MAGI NATION
_s e t mo d e
ENDP
The complete SETMODE procedure appears in Listing 2- 1 .
Listing 2-1 The S ETM O D E p roced u re ; s e tmode ( i n t mod e ) ; S e t VGA a d a p t e r t o B I O S mod e MODE s e t mode ARG push mov mov mov i nt pop ret s e tmode
-
-
PROC mode : WO R D bp bp,sp a x ,mode a h ,O 1 0h bp
; S a v e BP
; S e t u p s t a c k po i n t e r ; AX = v i deo mode f u n c t i o n number ; AH ; C a l l v i deo B I OS ; R e s t o r e BP =
E N DP
The semicolons in several of the program lines indicate that what follows is a comment to be ignored by the assembler. Because assembly language code tends to be rather obscure, it's a good idea to embed such comments wherever possible, to clarifY the programmer's intent. To put the VGA adapter into mode 1 3h, you can call this function from C++ like this: s e t mode ( Ox 1 3 ) ;
F i n d i n g Vi deo M e m o ry Once you've put the VGA adapter into mode 1 3h, finding video memory is a piece of cake. lt always begins at address AOOO: OOOO (which is equivalent to a linear address of AOOOO hexadecimal) and continues for a total of 64,000 (or F 8 0 0 hexadecimal) addresses thereafter. The last address in mode 1 3h video memory is AOOO:F7FF. (The remaining 1 , 535 bytes of the 64-kilobyte segment video memory are unused, since a segment is 6 5 , 5 36 bytes long and there are only 64,000 pixels on the mode 1 3h display.) Changing the value stored in any of the addresses in this range will change the color of the pixel in the corresponding position on the video display. How can you teil which pixel corresponds to which memory address? In mode 1 3h the pixels on the display are arranged in 200 horizontal rows, each containing 320
CHAPTE:R TWO
Fig ure 2-1
ßdsic G rdphic Tech n i q u es
Each add ress in video memorv corresponds
to a pixel on the video display
pixels. The first location in video memory - that is, the location designated by the address AOOO:OOOO - corresponds to the first pixel in the first row of pixels at the top of the display, which is to say the pixel in the upper-left corner of the screen. The second location - the one designated by address AOOO:OOO 1 corresponds to the pixel immediately to the right of that pixel. And so on for the first row of the display, with the location designated by address AOOO: O 1 3 F corresponding to the pixel in the upper-right corner of the display. The next 320 memory locations correspond to the 320 pixels in the row immediately below this one, so that address AOOO:O 1 40 corresponds to the pixel directly below the one in the upper-left corner. Each succeeding 320 addresses in video memory corresponds to another row of pixels on the display, moving from the top row sequentially to the bottom one. The last address in video memory, AOOO: F7FF, corresponds to the pixel in the lower-right corner of the display (see Figure 2- 1 ) . With this information we can write a short program to paint the mode 1 3h video display entirely white. Such a program appears in Listing 2-2 .
GARDE:NS O F I MAG I NATION
II I I W H I T EOUT V e r s i o n 1 . 0 II II
F i l l s mode 1 3 h v i deo memory w i t h a
II
f i l l i ng
c o n s t a n t va l u e o f O x O f ,
t h e di s p l a y w i t h w h i t e p i x e l s
II I I W r i t t e n b y C h r i s t o ph e r L a m p t o n f o r Ga r d e n s o f
II
# i n c l ud e
I m a g i na t i on
( Wa i t e G ro u p P r e s s )
< s t d i o . h>
# i n c l ud e
< d o s . h>
# i n c l ud e
< c o n i o . h>
# i n c l ud e
" s creen . h "
vo i d m a i n ( vo i d ) { II
S a v e p r ev i ou s v i deo mode :
i n t o l dmode=* ( i n t * ) MK_F P ( O x 4 0 , 0 x 4 9 ) ; II
C r e a t e po i n t e r t o vi deo memo r y :
c h a r f a r * s c r e e n= ( c h a r * ) MK_F P ( O x a 0 00 , 0 ) ; I I S e t mode 1 3 h : s e tmode ( O x 1 3 ) ; II for
Fi l l
v i deo memory w i t h a
( u n s i g n ed i n t
I I Wa i t wh i l e
for user
i =O ;
c o n s t a n t va l u e :
i xd i f f )
{
II
in
x d i r e c t i on
I I Move o f f s e t II
if
in
y d i m ens i o n
so . . .
pi x e l
to n e x t
in y di r .
} } } II
e l se {
II i nt
II
l e n g t h=yd i f f + 1 ;
II for
< i nt
i =O ;
i < l e ng t h ;
i ++ ) {
II II
s c reen [ o f f s e t J = c o l o r ;
II
I f d i f f er e n c e i s b i gger
in
y d i m e n s i on . . . p r epa re
to c o u n t o f f
in
y d i r e c t i on Loop t h rough poi n t s i n y d i r e c t i on Set
t h e n e x t p i xe l
in
the continued on nextpage
GARDE:NS OF IMAGINATION continutdfrom prroious pagt
II II II ( e r ro r_t e rm>O )
to n e x t p i x e l
i n y d i r e c t i on
I I C h e c k to s e e i f move
e r ro r_t e rm+=x d i f f ; if
L i n e t o C O LO R
I I Move o f f s e t
o f f s e t +=y_un i t ;
{
requ i red i n x d i r e c t i on
II If
so . . .
e r r o r_t e rm-=yd i f f ;
II
. . . r e s e t e r ro r t e rm
o f f s e t+=x_un i t ;
II
. . . an d move o f f s e t
II
pixel
to n e x t
i n x di r .
} } } }
This function divides neady into two parts, one for lines with slopes less than (where xdiff is greater than ydijf) , and one for lines with slopes greater than 1 (where ydijf is greater than xdijf) . (Lines with a slope of precisely 1 can be handled by either part.) The variable x_unit is set to either positive or negative 1 , depending on whether the line is moving in the positive or negative direction (that is, from lower to higher x or vice versa) . Similarly, the variable y_unit is set to positive or negative 320. These variables are then added to the pixel positions to i n crem e n t them fro m o n e p o s i t i o n to the next in the corresponding dimensions. The variable error_term is initialized to 0 and either ydijf(for lines with slope less than 1 ) or xdijf(for lines with slope greater than 1 ) is added to it repeatedly, until it exceeds the value of either xdiff or ydijf(whichever is not being added to i t fo r the current slope ) . Then the pixel is i ncre menred in the appropriate dimension, ydijf (or xdijf) is subtracted from error_term and the process begins again, until the end of the line is reached. A forO loop is used to increment the pixel coordinate in either the x or y dimension from one end of the line to the other. 1
At La st . . . the M a z e We'll use this implementation o f Bresenham's algorithm to draw the "wires" in our wireframe maze. To create a wireframe maze, we must do two things: Design a data structure for storing the maze in the computer's memory, and design an algorithm for drawing the maze on the screen. The first of these is the easier problern to solve. If we restriet ourselves to mazes that can be depicted on a piece of graph paper using ho rizontal and vertical lines that correspond to the graph lines, then we can store a description of a maze as a two-dimensional array of bytes ( that is, as a rwo-dimensional char array). Consider, by way of example, the maze in Figure 2- 1 1 , which has been drawn on a piece of graph paper. Each square on the graph paper represents a single position in the maze. Squares that have been shaded are wall squares - that
CHAPTE:R TWO
ßdsic G rd p h i c Tech n i q u es
Figure 2-11 A maze drawn on a sheet of graph paper
is, impassable squares in which solid cement extends from the floor to the ceiling. An adventurer wandering through this maze would not be able to enter these squares. Unshaded squares are empty squares, in which an adventurer can travel. Every square in this maze can be given a coordinate position, similar to the way in which we give coordinate positions to pixels on ehe video display. If we place the origin of the maze coordinate system at the upper-left corner, then the square in that corner would have x,y coordinates of 0 , 0 . The square to i ts immediate right would have x,y coordinates of 1 , 0. The square immediately below it would have x,y coordinates of 0, 1 . And so forth. We can store such a maze in the computer as a 16 by 16 array of char, like this: c h a r ma z e [ 1 6 J [ 1 6 J ={ {1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 } , {1 ,0,0,0,0,0,0,0,0,0,0,0,0,0 , 0 , 1 } , { 1 ,0 , 1 ,0 , 1 , 1 , 1 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 0, 1 } , { 1 , 0 , 1 , 0 , 1 , 0 , 1 , 0, 1 , 1 , 1 , 0 , 1 , 1 , 0 , 1 } , { 1 ,0 , 1 , 0 , 1 , 0 , 1 , 0 , 0 , 0 , 1 , 0 , 0 , 1 , 0 , 1 } , { 1 , 0 , 1 , 1 , 1 ,0, 1 , 0 , 1 , 0 , 1 , 1 , 1 , 1 ,0 , 1 } , {1 ,0,0,0,0,0,0,0, 1 , 0,0,0,0,0,0, 1 } , {1 ,0,1 , 1 , 1 ,0, 1 , 0 , 1 , 1 , 1 , 0, 1 , 1 ,0,1 } , { 1 , 0 , 1 ,0,0,0 , 1 , 1 , 0,0, 1 , 0 , 1 , 1 , 0 , 1 } , { 1 , 0 , 1 ,0 , 1 , 0 , 1 , 1 , 1 , 1 , 1 , 0 , 1 , 1 , 0 , 1 } , {1 , 0 , 1 ,0,1 ,0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,0, 1 } , {1 ,0,1 , 1 ,1 ,0,1 , 0 , 1 , 0 , 1 , 1 , 1 , 1 ,0, 1 } , { 1 , 0 , 0 , 0 , 1 , 0 , 1 , 0, 1 , 0 , 0 , 0 , 0 , 1 , 0 , 1 } , { 1 ,0,1 , 1 , 1 , 0 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ,0 , 1 } , { 1 ,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 1 } , { 1 , 1 , 1 ,1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 } };
G A R D E N S O F I MAG I N ATION
The elements of this array that are set to 1 represent the shaded squares in the maze, while the elements that are set to 0 represent open squares. (lt may occur to you that we could represent this maze using only a bit per square rather than a full byte, but the byte representation will prove useful in the more sophisticated mazes that we will develop later in this book.) To locate our position wirhin the maze, it would be useful to have a data structure for storing x,y coordinates. We'll define such a structure like this: s t ru c t
xy {
i n t x , y;
};
Now, how do we go about drawing the maze on the display?
o n e S q u a re a t a Ti m e lmagine yourself standing in the middle o f a maze, looking into the distance. What would you see? Probably, you'd see something like the wireframe maze representation in Figure 2- 1 2 (albeit with the addition of such real world frills as colors and textures) . A lang hallway stretches out in front of you, interrupted periodically by doors opening to both sides. To draw such a representation on the video display, we need to locate the viewer's position and orientation wirhin the maze, then work our way down the
F i g u re 2-1 2 A wireframe representation of a maze as seen by someone standing in the m id d l e of it
C HAPTER TWO
ßdsic G rdp h i c Tech n i q u �s
hallway along which he or she is currendy looking. Where we find walls, we should draw walls. Where we find openings in the walls, we should draw openmgs. First, we need a variable to store the viewer's current position. We'll use the xy structure that we defined a moment ago to declare a variable called pos: s t ru c t
xy pos={ 1 , 3 } ;
This indicates that the viewer i s standing at coordinate position 1 ,3 wirhin the maze, one square from the left edge and three squares from the right edge. We'll also need a way to define the viewer's orientation - that is, whether the viewer is facing in the positive x direction (toward higher x coordinate positions in the maze), the negative x direction (toward lower x coordinates in the maze) , the positive y direction, or the negative y direction. We'll arbitrarily refer to the posi tive x direction as direction 0, the positive y direction as direction 1 , the negative x direction as direction 2, and the negative y direction as direction 3 (see Figure 2- 1 3) . That way, we can Store the direction as an integer variable: i n t d i r e c t i on=1 ;
Bur how will we teil the computer what rhese directions mean? We'll create an array of type xy which will hold the increments for each of these directions that is, an x value of 1 for the positive x direction, an x value of - 1 for the negative x direction, and so forth. We'll call this array increment and we'll declare it like this: s t ru c t xy
i n c reme n t [ 4 J ={ { - 1 , 0 } , { 0 , 1 } , { 1 , 0 } , { 0 , - 1 } } ;
There is one element o f this array for each o f the four positions i n which the viewer can face. Restricting the viewer's orientation to these four positions
1 (North)
3 (South)
Figure 2-1 3 The four compass directions
GARDE:NS OF I MAGI NAT I O N
simplifies the problern of drawing the maze, since we only have to draw it from these four positions. (Most of the early maze games worked this way.) Later, we'll learn how to draw a maze from any arbitrary orientation. For reasons that we'll see in a moment, we'll also need a pair of arrays to give us the increments along the x and y axes for paths through the maze that move to the left and right of the viewer's current orientation. We'll call these arrays left and right: s t r u c t xy L e f t [ 4 J = { { Q , - 1 } , { - 1 , 0 } , { Q , 1 } , { 1 , 0 } } ; s t r u c t xy r i g h t [ 4 J = { { Q, 1 } , { 1 , 0 } , {0,-1 } , {-1 , 0 } } ;
We could draw the image of the maze to fill the entire display, but we'll rarely, if ever, need to do that i n practice. Usually, we'll be drawing the maze in a window filling part of the display and placing other information in the remaining space. So let's draw a small box on the display to delineate the window in which the maze will be drawn: v o i d d r a wbox ( c h a r * s c r e e n ) { L i n e d r a w ( 82 , 1 9 , 2 94, 1 9 , 1 5 , s c r e e n ) ; L i n e d r a w ( 294 , 1 9 , 2 94, 1 1 9 , 1 5 , s c r een ) ; L i n e d r a w ( 294, 1 1 9 , 8 2 , 1 1 9 , 1 5 , s c r e e n ) ; L i n e d r a w ( 82 , 1 1 9 , 82 , 1 9 , 1 5 , s c r e e n ) ; }
This short function, called drawbox(), uses the linedraw() function that we developed earlier to draw a box on the display with its upper-left corner at coordinates 82, 1 9 and its lower-right corner at coordinates 294, 1 1 9 (see Figure 2- 1 4) .
Figure 2-14 The box drawn on the d isplav by the DRAWBOXO function
CHAPTE:R TWO
ßdsic G rdphic Techniques
To draw the maze itself, we need to know how many squares the viewer is able to see into the distance. We'll arbitrarily choose a value of 4, which we'll store in the integer variable visibility: i n t vi s i b i l i ty=4;
Now we'll need to draw each of the four squares visible along the viewer's line of sight. We'll step through all four squares using a forO loop: f o r ( i n t di st=O;
d i s t prompt and nothing appeared on the display. ) Our collection of low-level keyboard handler routines will consist of three assembly language functions: one to install the new keyboard handler, one to remove it, and the keyboard handler itself. We'll call these functions initkeyQ,
CHAPTER THRE:E:
ßdsic Input Tech niq ues
assembly language. (Technically, newkey() isn't actually a function, since we'll never call it direcdy from CIC++. Ir will be called automatically when a keyboard interrupt is generared - that is, when the user presses a key.) We'll place rhese rhree functions in an assembly language module called IO.ASM, along with the other low-level in pur functions rhat we'll develop in this chapter.
The i n itkeyo F u n ctio n The BIOS routine rhat installs a new inrerrupt handler is one of a !arge set of routines in the PC ROM known as rhe INT 2 1 H routines, after rhe assembly language instruction that is used to call rhem. Specifically, it is function 25h, otherwise known as Set Inrerrupt Vector. Before we can call rhis function, however, we must call function 35h, also known as Ger lnrerrupt Vector, to learn rhe number of the old keyboard handler rourine - so that we can restore it when the program terminates. And to store the address of the old function, we'll need to set aside 16 bits of memory for the segment and 16 bits for the offset. The last of rhose tasks is rhe easiest. In assembly language, we can allocate 1 6bit data storage with rhe DW (Define Word) directive, like this: i ntofs
dw 0
i n t s eg
dw 0
These two DW di rectives allocate a pair of 1 6-bit storage locations (equivalent to int variables in C/C++) called intofi and intseg. We'll use rhe first to store the offset of the old inrerrupt vector and rhe second to hold the segment. We'll also need a memory location for storing information about which keys are being pressed. This location will need to be accessible to both our CIC++ routines and the keyboard handler, so we'll define it in C++ as an array and pass its address to the initkey() function, which will store rhis address where it can be accessed by the newkey() handler. We'll define storage for the address of rhis array the same way we assigned storage for rhe address of rhe old inrerrupt handler: bufseg
dw 0
bufofs
dw 0
The first of these will hold rhe segment of the array, the second the offset. The actual form that this array will take will be explained in a moment, when we discuss the keyboard handler function. Next, we' ll call I N T 2 1 h function 3 5 h using rhe same p rocedure we demonstrated for calling DOS and BIOS functions in the last chapter: mov
ah ,35h
;
Ca l l
mov
a l , 09h
;
.
i nt
21 h
;
. . . i n t e r r u p t 09h
.
•
INT 2 1 h
to get
F u n c t i on 3 5 h
c u r rent address of
GARDENS O F I MAGI NATI O N
The 3 5 h placed in the AH register is the number of the function that will give us the address of the old interrupt handler; the 09h in AL is the number of the function that the handler handles. On return from this call to DOS, the BX and ES registers will contain the offset and segment of the old interrupt handler, respectively. We can move these values directly into the storage locations we've set aside for them: mov
i nt s e g , e s
;
mov
i ntofs,bx
;
S a ve s egm ent . . •
and offset
To install a new interrupt handler, we must place the segment and address of the new handler in the DS and DX registers, then call INT 2 l h function 25h: mov
dx , s e g _n e w k e y
;
Put
mov
ds,dx
;
. . . i n t e r r up t
t h e s eg m e n t of new
mov
d x , o f f s e t _ne w key ;
P l us offset
mov
ah,25h
;
Ca l l
mov
a l , 09 h
;
. . . to i n s ta l l
i nt
21 h
hand l e r
in DS
i n DX
I NT 2 1 h
F un c t i on 2 5 h n e w h a nd l e r
Finally, we'll take the address of the buffer in which key data will be passed back to our CIC++ program and store it in the locations we set aside for it earlier. The address will have been passed to the initkeyO function in a 32-bit parameter called buffer, so we'll move the segment portion and address portion of that parameter into storage: Les
d x , bu f f e r
mov
bufseg,es
mov
bufof s,dx
;
S a v e a d d r e s s o f n e w s c a n c o d e buf f e r
And then we'll return to C++. The complete initkeyO function is in Listing 3- 1 .
Listin g 3-1 The i n itkeyo f u n ction _i n i t ke y
PROC
; I n i t i a l i z e keyboa rd i n t e r ru p t h a nd l e r ARG
bu f f e r : D W O R D
push
bp
mov
bp,sp
;
Po i n t e r t o s c a n code buf f e r
;
S a v e E S a nd D S
; S a v e B P r eg i s t e r
r eg i s t e r s
push
es
push
ds
mov
a h ,35h
;
Ca L L
mov
a l , 09h
;
. . . to g e t c u r r e n t a d d r e s s of
i nt
21 h
;
. . . i n t e r r u p t 09h
mov
i n t seg,es
;
S a v e s eg m e n t
mov
i n t o f s , bx
;
. . . a nd o f f s e t
mov
d x , seg _ne w key
;
Put
I NT 2 1 h
F un c t i on 3 5 h
t h e segment of new
CHAPTER THREE ;
ßdsic I nput Techniq ues
. . . i n t e r ru p t h a n d l e r i n O S
mov
ds,dx
mov
d x , o f f s e t _n ewkey ;
P l us o f f s e t
mov
ah,25h
;
Ca l l
mov
a l , 09 h
;
. . . t o i n s t a l l new h a n d l e r
;
S a v e a d d r e s s o f new s c a n code b u f f e r
;
Restore
i nt
21 h
Les
d x , bu f f e r
mov
bu f s e g , e s
mov
buf o f s , d x
pop
ds
pop
es
pop
bp
INT 21 h
i n OX Fu n c t i on 2 5 h
r eg i s t e r s
ret _i n i t key
ENOP
The rem keyo F u n ctio n The remkeyQ function, which removes the new keyboard handler and reinstalls the old one, is extremely simple. Ir loads the offset and segment of the old handler into DX and DS and calls INT 2 l h function 25h again. Aside from the usual set-up and return instructions, that's all it does. The complete function appears in Listing 3-2.
Listing 3-2 The rem kevo fu n ction _remkey PROC r eg i s t e r
push
ds
;
Save OS
mov
dx , i n t seg
;
G e t s e gment a n d o f f s e t o f
mov
ds,dx
;
. . . o l d i nt errupt
mov
dx , i n t o f s
hand l e r
mov
ah,25h
; Ca L L I N T 2 1 h F u n c t i on 2 5 h
mov
a l , 09 h
;
i nt
21 h
pop
ds
. . . to restore old hand l e r
; Restore O S reg i s t e r
ret _remkey E N O P
Th e n ewkeyo F u n ction The most important o f the three low-level keyboard functions is the newkeyQ function. This is the actual interrupt handler that we installed and removed with the last two functions. lt's written pretty much like any other assembly language procedure, except that ( 1 ) we can't pass any parameters to it (because we'll never acrually call it), (2) we must end it with an IRET (lnterrupt RETurn) instruction, and (3) we must be careful to save the value of every CPU register that we use in
G A R D E N S OF I MAG I NAT I O N
the course of the procedure. The reason for this last requirement is that an interrupt handler such as this will be executed at unexpected times, while the computer is performing other tasks. We must be careful not to mess up the values stored in the registers lest we interfere with whatever task is in progress when the user presses a key. We must leave all registers precisely the way we find them. O nce we've finished saving the registers, however, we can get down to the serious business of handling the interrupt. First, we need to get the address of the buffer that we passed from C++ earlier and put it into a register (or, rather, pair of registers) . This buffer is a 1 28-byte array of type char in which each position corresponds to one of the 1 0 1 scan codes that can be returned from the PC 1 0 1 key keyboard. (See Figure 3- 1 for a chart showing which scan codes represent which keys.) Of course, since the keyboard has 1 0 1 keys and the array has 1 28 elements, that leaves a few unused spaces in the buffer. But we'll leave those extra 2 7 entries in place for future keyboard expansion (and so that we won't accidentally trash memory beyond the end of the array should the keyboard processor accidentally send us nonsense data) . The design of the PC keyboard is such that it can never have more than 1 28 scan codes, so we should be safe into the foreseeable future. We'll put the segment and offsec of the buffer into registers ES and Dl, where we can use them as a pointer to the starr of the array: mov
a x , bu f s e g
mov
es,ax
mov
d i , bu f o f s
;
Po i n t E S : D I a t
s c a n code buf f e r
Next, we'll get the scan code for the last key pressed from the keyboard processor. This is done by inputring data through data port 60h. (See Appendix B on machine language programming for more detail on inputring data through ports.) We'll input the data to register AL: in
a l , 60 h
;
Get
the
L a t e s t s c a n code
In a moment we're going to use the 1 6-bit value in the AX._ register as an index into the scan code buffer, based on the value that we just input to the AL register. Since AL is already part of the AX._ register - see Appendix B for details - the job of setring up the index is half done, but we'll need to put a 0 in the other half of the register, like this: mov
ah,O
;
Z e ro h i g h b y t e o f AX
The scan code is now in the AL register. This code will be either a make code or a break code - that is, it will eieher indicate that the key corresponding to the code has j ust been pressed or that it has just been released. How can we tell the difference? A break code is equal to the scan code for the key plus 1 28, while a make code is equal simply to the scan code. Since no scan code on the PC keyboard is allowed to have a value greater than 1 27 (and at present none have
CHAPTER THREE
fu Esc
! or @ or # or s or % or A or & or * or or or or + or Bksp Tab
1 2 3 4 5 6 7 8 9 10 =
Q w E R T
y u I
0 p { }
or [ or ] Enter Ctrl
ßdsic Input Tec h n iq u es
Code
fu
Code
fu
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
A s
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
Cops lock Fl F2 F3 F4 F5 F6 F7 FB F9 Fl O Fl l F1 2 Numlock Serail lock Horne or 7 Up or 8 PgUp or 9 Gray left or 4 Center or 5 Right or 6 Gray + End or 1 Down or 2 PgDn or 3 Ins or 0 Del or .
58 59 60 61 62 63 64 65 66 67 68 1 33 1 34 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
D F G H
J K l or ; or ' or Left Shift I or \
II
-
z X c V
B N
M or , or . ? or I Right Shift Prt Sc or *
Ah Spacebar
Figure 3-1 The scan codes for the 1 01 -key keyboard
values greater than 1 08), this means that make codes have values in the range 0 to 1 27, and break codes have values in the range 1 28 to 255. The next thing that our keyboard handler must do, then, is to check to see which kind of code it has received. We'll use the CMP (compare) instruction to compare the value in AL with 1 28 : cmp
a l , 1 28
; W a s c ode a " m a k e "
or a
"brea k " ?
If it's a break code (that is, i f a key has been released) , this instruction will set rhe CPU bo rrow flag (see Appendix B ) . We can make a j ump i nstruction
GARDENS OF I MAGI NAT I O N
contingent on this flag and branch to the routine for handling break codes if the flag is set: j nb
break
;
I f b r e a k , s k i p a head
I f this j ump isn't taken, then it's a rnake code - that is, a key has been pressed. We'll rnove the value in AX to BP, where we can use it as an index into the buffer. And we'll store a 1 in the buffer element corresponding to this scan code, to indicate that the key is currendy pressed: mov
bp,ax
;
mov
[ by t e p t r e s : d i +bp J , 1
; A n d f l a g k e y a s p r e s sed
j mp
e n d key
E l s e u s e code as
i nd e x i n to buf f e r
That last instruction skips over the instructions that handle break codes. These instructions are j ust like those that handle the make code, except that they first subtract 1 28 frorn the scan code (by ANDing the value with 1 27; see Appendix B) and then move a 0 into the appropriate buffer position rather than a 1 : b r ea k :
; I f b r e a k c od e , remove h i g h b i t
and
a l , 1 27
mov
bp,ax
mov
[ by t e pt r e s : d i +bp J , O ;
;
U s e code as
i nd e x
F l ag
re l ea s ed
key a s
i n t o buf f e r
The next fo ur instructions, which are perforrned whether or not the code was a rnake or a break, change the value of a single binary digit of the value received through input port 6 1 h, then send this value back out through that port. This rather odd operation "clears" the interrupt request - that is, it teils the 8042 processor in the keyboard that we won't be needing further copies of the rnost recently received scan code. (lt's possible to write keyboard handler routines that "chain" to the regular B I O S keyboard handler by not clearing the interrupt request and passing control to the regular handler once the custorn handler does its thing. This, for instance, is the rnethod that terrni nate-and-stay-resident utilities - TSRs - use to watch for the pressing of certain key cornbinations while not disturbing the activity of the regular keyboard handler.) Here's how the clearing operation is perforrned: endkey : in
a l ,61 h
mov
ah,al
or
a l , 80h
out
61h,al
;
C l ea r t h e keyboa rd
i nt e r rupt
We rnust also send a rnessage to the PC's general interrupt handling circuitry, which up until now has been refraining frorn sending any additional interrupts to the C P U while we perforrned o u r interrupt processing, telling it that the cornputer is ready to process other interrupts. This is clone by sending a value of 20h through outpur port 20h:
CHAPTE:R THRE:E: mov
a l ,20h
out
20 h , a l
; A l l ow a dd i t i o na l
ßdsic I nput Tec h n i q u es
i n t e r rupts
The rest of the interrupt handler consists of restoring the value of all saved registers and executing an IRET instruction. The complete text of the keyboard interrupt handler appears in Listing 3-3.
_ne w k ey
PROC
pu s h
ax
pu s h
di
FAR ;
Save a l l
regi sters used
;
Po i n t E S : D I a t
scan code buffer
s c a n code
pu s h
bp
push
es
mov
a x , bu f s e g
mov
es,ax
mov
d i , bu f o f s
in
a l , 60h
;
Get
mov
ah,O
;
Z e ro h i g h b y t e of AX
cmp
a l , 1 28
; Wa s code a
j nb
brea k
;
If break,
;
E l s e use code a s
mov
bp,ax
mov
[ byt e p t r
j mp
end key
es : d i +bp J , 1
the
l a test
"ma k e " or a
" b r ea k " ?
s k i p ahead i ndex
i nto buffer
; A n d f l a g key a s p r e s sed
brea k : and
a l , 1 27
;
mov
bp,ax
; U s e code a s
i nd e x
mov
[ b y t e p t r es : d i +b p J , O
;
F l ag k e y a s
re l ea s ed
;
C l e a r t h e keyboa rd i n t e r r upt
If
b r ea k code,
remove h i g h b i t i n t o buf f e r
endkey : in
a l,61 h
mov
ah,a l
or
a l , 80 h
out
61 h , a l
mov
a l ,20h
out
20 h , a l
pop
es
pop
bp
pop
di
pop
ax
; A l l o w a d d i t i on a l ;
Restore
i nt e r rupts
re g i s t e r s
i ret _n ewkey
ENDP
U s i n g the Keyboa rd F u n cti o n s These three low-level keyboard functions initkeyQ, remkeyQ, and newkeyO must always be used together. And they must be used with considerable caution. When our custom keyboard handler snaps into place, it will be the only means we -
-
G A R D E N S OF I MAG I NATION
have of communicating with our program. Even the familiar (CTRL}-�-(DEL) combination will cease to work. If you're debugging a program under Turbo Debugger or the built-in debugger that comes with the Borland C++ IDE, you'll no l anger be able to terminate the p rogram by hitring (CTRLHilll . So i t 's imperative that you put some kind of escape clause into your program before the first time you run it, even if you're working wirhin a familiar resring and debugging environment. For instance, you can add code to your program that will watch for the keyboard handler to report that the (Esc) key (scan code 1 ) has been pressed. Here's how you can incorporate the keyboard handler into a program. Link the I O .ASM file to your code and include the IO.H header file at the top of any modules that reference the initkeyO or remkeyO functions. Put a call to initkeyO near the beginning of your code, like this: i n i t ke y ( s c a nb u f f e r ) ;
where scanbuffer is a 1 28-byte array of type char with non-local scope - that is, it has been declared externally to any functions. From that point until you invoke the remkeyO functions, the scan codes of all keys pressed on the keyboard will be instandy (and rather mysteriously) flagged in the scanbuffer array. For instance, if the (ENTER) key (which has a scan code of 28) is pressed, element 28 of this array will be set to 1 for as long as it is held down. It will be reset to 0 when the key is released. Your program can test these elements to see which keys are currendy being pressed.
T h e SCAN KEY P ro g ra m Now let's test our keyboard handler to see if it works as expected. The short program in Listing 3-4 (available on the disk as SCANKEY.EXE) will repeatedly print the word "ENTER" on the display as long as the (ENTER) key is being held down and the word "SHIFT" as long as the (SHIFT) key is held down. If both keys are pressed simultaneously, it will print both words alternately. When the (Esc) key is pressed, it terminates. It does all of this simply by installing the keyboard handler and watehing for elements 1 , 28, and 42 (scan codes for (Esc), (ENTER), and (SHIFT) ) to be set to 1 . (Actually, it watches for them to be set to any non-zero value.) The SCANKEY output looks like this: SHI FT S H I FT SH I FT S H I FT S H I FT SHI FT SHI FT
CHAPTE:R THREE
ßdsic I nput Tec hn iq ues
S H I FT S H I FT S H I FT ENTER ENTER ENT E R ENTER ENTER ENTER EN T E R ENTER ENTER ENTER ENTER ENTER ENTER
Listing 3-4 The SCAN K EY. C P P p rogra m I I S CANKEY . C P P II I I Demon s t ra t e s a l t e r n a t e keyboa rd h a nd l e r II I I W r i t t en b y C h r i s t o p h e r La m p t on I I f o r Ga rdens of
I ma g i na t i on
# i n c l ude
< s t d i o . h>
# i n c l ude
"io.h"
c h a r f a r s c a n bu f f e r [ 1 2 8 J ;
( Wa i t e G r oup P r e s s )
I I Buf f e r f o r 1 2 8 s c a n codes
vo i d ma i n ( vo i d )
{
II
I n s t a l l a l t e r n a t e keyboa rd d r i ve r :
i n i t key ( s c a n bu f f e r ) ; I I Loop un t i l E S C ( s c a n code w h i l e ( ! scanbuf f e r [ 1 J )
{
II
W a t c h f o r S H I F T key :
if
( s c a n bu f f e r [ 42 J )
1)
i s p r e s sed :
pri n t f < "SHI FT\n" ) ;
I I W a t c h f o r E N T E R key : if
( s canbuf f e r [ 2 8 J )
p r i n t f < " ENTER \ n " ) ;
} II
}
Remove a l t e rn a t e keyboa rd h a nd l e r :
remkey ( ) ;
GARDE:NS O F I MAG I N AT I O N
R e m ov i n g the Keyboa rd H a n d l e r Once you're finished wirh rhe keyboard handler, remove it by calling the remkeyO function, like this: remkey ( ) ;
Fo rget ro do rhat, and rhe computer will appear to lock up when your program ends. Of course, you may not notice this if you're running under Turbo Deb ugger or the Borland I D E at the time, since both of rhese programs are aggressive about re-i nsralling rheir own keyboard handlers when a program terminates. Bur most users of your program will be running it from the DOS p rompt, where applications are expected to observe an honor sysrem about reinsralling the old keyboard handler when rheir work is clone. Later in this chapter, we'll show you how to incorporate these low-level keyboard handling rourines into a higher-level inrerface, in rhis case an event management module for a wireframe maze program. For the moment, however, we're going to turn ro another input device, the PC mause.
T h e PC M o u se For microcomputer users, rhe input device known as the mause has always been more closely idenrified wirh rhe Apple Macintosh computer than with the IBM PC and its clones. That's because the mause has been Standard equipment with all Macinroshes since the first Mac rolled off rhe assembly line more than a decade ago. lt's pretty much impossible to use a Macinrosh wirhaut one. PC users, by contrast, have come to the ma use in dribs and drabs. Unril recendy, most PC software used the keyboard as the primary input device, wirh mause support added as an afterrhought (if at all). Bur rhe increasing popularity o f rhe M i c rosoft Win dows operaring sysrem has changed rhat. Like the Macintosh operaring sysrem, Windows is expressly designed to be used wirh a mause. And, as Windows has found its way onto more and more computer sysrems, so have mice. Now it's a rare PC-compatible computer rhat doesn't have one attached to it. Game designers have raken note of this phenomenon. More and more, rhe mause is being made available as an optional control device on PC-based games. Many PC games are now being designed with the mause as a primary input device, rhough keyboard control is still made available for those users wirhaut a rodenr attached to rheir sysrem unit. Dungeon Master, rhe seminal maze game menrioned in chapter 1 , is quite difficult to control without a mouse. This is due in part to its heritage on rhe Atari ST, a computer that (like the Macintosh) comes with a mause as Standard equipment, but games inspired by Dungeon
CHAPTE:R T H R E E
ß d s i c I n put Techniques
Master - SSI's Eye of rhe Beholder series, for instance - also tend to have a strong mouse orientation. Alrhough a maze game can be designed wirhout mouse support, it is in creasingly essential rhat rhe maze game programmer have a background in mouse-programming skills.
The M o u se Drive r You'll be happy to know rhat we don't have to design out own low-level handler for rhe mouse the way we did for rhe keyboard. Thar's because a low-level mouse handler is supplied with every mouse sold. It's called rhe mouse driver and is generally loaded in to rhe computer's memory when rhe CONFIG. SYS and AUTOEXEC. BAT files are executed at power-up time. (Some mouse drivers are boored from CONFIG. SYS and orhers from AUTOEXEC. BAT, depending on how rhe driver itself has been implemenred.) To receive information from rhe mouse, we have only to call rhis driver and rell it what we want to know. There's a Standard interface rhat programs can use to talk to the mouse driver, so this is actually a very simple process. (Some early mouse drivers, which did not follow rhe Standard for such drivers set by the Microsoft Mouse, may not respond to rhe standardized set of mouse driver commands, but rhese are now fairly rare and can generally be ignored. However, you should specif}r that your programs require a Microsoft-compatible mouse lest some user with a prehistoric driver complain rhat yout program won't work on his or her machine.) Like rhe inpur rourines in rhe BIOS, rhe mouse driver is best accessed from assembly language, since inrerfacing to the mouse driver from C++ is awkward at best. So we'll add a few short mouse routines to out IO .ASM file, which can then be called as functions from a C/C++ program. Although rhese routines will barely scratch the sutface of rhe rricks that the mouse can perform - rhey'll only call three of rhe many functions available from most mouse drivers - rhey will nonetheless provide us with enough information to allow us to use the mouse in our programs. The mouse driver functions that we will be calling are listed in Table 3- 1 . These functions are designed to be called from an assembly language program using the INT 33H instruction. The number of the desired function must be placed in rhe AX register before rhe call. Additional information is exchanged in the other registers listed in the table. The first of these functions, function 0 , simply initializes rhe mouse driver. (Ir also rells us how many buttons rhe mouse has, since some PC mice have two buttons and others have rhree .) Function 3 teils us whether or not any of the three mouse burtons is being pressed. (Ir also returns a pair of numbers representing the cutrent position of the mouse, but we're going to ignore these in favor of rhe information returned by the next function.) Function OBh rells us how far the mouse has moved across rhe user's
G A R D E N S O F I MAGI NATION
desktop since the last time the function was called and in what directions it has moved. This relative position is measured in a unit called, for obvious reasons, the mickey, which is equivalent to 1 1400th of an inch. (For older mice, the mickey is equal to 1 /200th of an inch.) To access these functions, we'll create three assembly language mous e functions: initmouseO t o initialize the mouse interface, readmbuttonO t o detect whether the mouse button has been pressed, and relposO to read the position of the mouse relative to the last position reported.
Ta ble 3-1 I NT 3 3 H F u n c t i on OOH R e s e t mouse d r i ve r IN: AX :
OOOOH
OUT : AX :
I n i t i a l i z a t i on s t a t u s
FFFFH:
S u c c e s s f u l l y i n s t a l l ed
OOOO H :
I n s t a l l a t i on f a i l e d
BX :
Number o f mouse b u t t an s
F u n c t i on 03H G e t p o i n t e r po s i t i on a nd b u t t o n s t a t u s IN: AX :
0003H
OUT : BX :
B u t ton s t a t u s
L e f t mouse b u t t on < 1 i f p r e s s e d > 1 : R i g h t mou s e b u t t on ( 1 i f p r e s s e d ) B i t 2 : Cen t e r mou s e b u t t on ( 1 i f p r e s sed ) Bi t
0:
Bi t CX :
X coord i na t e
DX :
Y coordi n a t e
F u n c t i on OBH Get
r e l a t i ve mouse
pos i t i on
IN: AX :
OOOBH
OUT : CX :
Re l a t i ve h o r i z on t a l
DX :
R e l a t i ve v e r t i ca l d i s t a n c e
di stance
( i n m i c keys )
( i n m i c keys >
Ta ble 3-1 The mouse driver f u n ctions called by the routines in this chapter
CHAPTER THREE
ßdsic I nput Techniq ues
The i n itm o u se o F u n ction The initmouseO function mostly j ust loads a 0 into the AX register and calls the mouse driver. While it may not be obvious from looking at the function itself, it also returns an error code as the integer value of the function, since the mouse driver leaves this code in the AX register and the contents of this register are automatically passed back to CIC++ as the result of the function call. A 0 value means that an error has occurred. Listing 3-5 contains the text of the initmouseO function.
Listing 3-5 The i n itmo u seo fu n ction _i n i tmouse ;
PROC
C a l l mouse d r i ve r
i n i t i a l i z a t i on
rout i n e
mov
ax,O
;
Request
i nt
33h
;
Ca l l mo u s e d r i v e r
;
R e t u r n w i t h e r ro r code i n AX r e g i s t e r
ret _i n i tmouse
f un c t i on z e ro ( i n i t i a l i z e )
ENDP
The rea d m b utto n o F u n cti o n The function that returns information about the mouse buttans isn't much more complicated. The readmbuttonO function calls mouse driver function 3, which reports on the absolute position of the mouse and the status of the buttons. We're not interested in the absolute position of the mouse, so this value is ignored. A single byte value describing the current status of the mouse buttans is returned from function 3 in the BX registers, so we move that value into the AX register where it is returned to CIC++ as the integer value of this function. The first three individual bits - binary digits - in this 1 6-bit binary number tell us whether the equivalent mouse buttans are pressed or not. If a button is pressed, the equivalent bit is set to 1 . If not, the bit is reset to 0. The first (or rightmost) bit of the number represents the status of the left mouse button, the second bit represents the status of the right mouse button, and the third bit represents the status of the center mouse button. We'll show you more about how to read these bits in a moment. The text of the readmbuttonO function appears in Listing 3-6.
Listing 3-6 The rea d m button o f u n ction _readmbu t t on ;
PROC
Read mou se b u t t o n
continued on next page
GARDENS OF I MAG I N AT I O N continuedftom previous pttge
mov i nt mov
; R e q u e s t f u n c t i on 3 ( r ead b u t t o n s ) ; Ca l l mou se d r i v e r ; P u t r e s u l t i n f u n c t i on r e t u r n ; re g i s t e r
ax,3 33h a x , bx
ret _r e a d mb u t t o n
ENDP
The third button isn't present o n all P C mice, so you shouldn't assume that the user has one. Some games assign an inessential function to this button, often one that can also be accessed from the keyboard, but the majority of games ignore it completely.
The re l po s o F u n ction Finally, w e want t o know how far the mouse has moved since the last time we checked up on its whereabouts. The relposO function obtains that information by calling m ouse driver function Obh. The mouse can move in two directions, roughly corresponding to the x and y axes of a Cartesian coordinate system, so the mouse driver will return to us two values representing the relative position of the mouse in each direction, as measured in mickeys. Negative values represent movement to the left or up while positive values represent movements to the right or down. (Note that this corresponds to the way in which Cartesian coordinates on the video display traditionally grow smaller to the left and up and !arger to the right and down. See Figure 3-2. )
� / �i����� Figure 3-2
Negative mickeys represent
movement up a n d left; positive mickeys represent movement down a n d right
CHAPTE:R T H R EE
ßdsic Input Tech n i q u es
Since the relpos() function will need to return both of these values to the CIC++ routine that is calling it, we'll need to pass the function pointers to a pair of integer variables in which it can return these values. We'll da this using the ARG directive, as described in the last chapter (and in the appendix an assembly language) . After calling the mause driver, the relpos() function will retrieve the two values from the CX and DX registers, place them in the variables pointed to by these pointers, and return ro the caller. The text of the relpos() function appears in Listing 3-7.
_r e l po s ; Get
PROC
c h a nges
i n mou se po s i t i o n r e l a t i ve t o
ARG
x : DWOR D , y : DWORD
push
bp
mov
bp,sp
mov
a x , OOObh
; ;
Reques t
Last
ca l l
f u n c t i o n Obh
( r e l a t i ve mou se pos i t i o n )
i nt
33h
;
Ca L L m o u s e d r i ve r
Les
bx,x
;
Po i n t e s : bx a t
mov
[e s : bx J , c x
;
. . . and s t o r e re l a t i v e po s i t i on
X
paramet er
Les
bx,y
;
Po i n t e s : bx a t y pa r a m e t e r
mov
[ e s : bx J , d x
;
. . . a nd s t o r e r e l a t i ve po s i t i on
pop
bp
ret _re l pos
ENDP
Why would we be interested in the relative position of the mouse, as opposed to its absolute position? In this book, we won't be using the mause to guide a pointer on the display, the way that some programs da. Instead, we'll be using it much the way we use the j oystick, to indicate directions of motion. Thus all we'll really need to know is the direction in which the mause is moving, which can easily be determined from its relative position. More about this in a moment.
U s i n g the Mouse Fu nct i o n s Ta use these three mause functions i n a program, link IO .ASM to your code and include the IO.H file in all modules that reference them. Initialize the mause during the initialization of the program by calling initmouse(), like this: if
( ! i n i t mou s e ( ) )
ex i t ( 1 ) ;
You'll notice that this instruction exits the program with an error code if the
initmouse() function returns a 0 value. (You'll recall that this means an error
G A R D E N S O F IMAG INATION
occurred in initializing the mouse.) Alternatively, if the mouse isn't crucial to your game, you could set a flag indicating that the mouse isn't available for use, then continue with the program, like this: if
( i n i t m o u s e ( ) ) mou s e_i n p u t = FA L S E ;
To find out if the left or right mouse buttons are currently being pressed, call the readmbuttonO function, like this: i nt
bu t t on_s t a t u s = r eadmbu t t on ( )
l f the left mouse button is being pressed, the first bit - that is, the rightmost binary digit - of the integer variable button_status will be set to 1 . You can test for this by performing a bitwise AND between button_status and the constant number 1 , then testing the result to see if it's non-zero, like this: if
( bu t t on_s t a t u s & 1 ) p r i n t f < "The
l e f t mouse b u t t o n i s be i ng p r e s s ed . \ n " ) ;
I f the right mause button is being pressed, the second bit of button_status will be set to 1 . You can similarly test for this by performing a bitwise AND between b utto n_status a n d t h e c o n s t a n t n u m b e r 2 . The p r ed e fi n e d c o n s t a n ts LMBUTTON and RMBUTTON , equal to 1 and 2 respectively, are provided in the I O . H file for this purpose. Finally, to determine if the mouse has moved since you last checked, you can call the relpos() function, passing pointers to a pair of integer variables to it as parameters, like this: i n t x re l , yr e l ; r e l po s < &x r e l , & y r e l ) ;
O n return from this function, xrel will contain the number of mickeys that the mouse has moved in the x direction since the last call to this function, with n egative values represe n t i n g m oveme n t s to the left and posi tive val ues representing movements to the right. Similarly, yrel will contain the number of mickeys that the mouse has moved in the y direction since the last call to this function, with negative values representing upward movements and positive values representing downward movements.
T h e M O U S E P ro g ra m To demonstrate the mause functions, Listing 3-8 contains a short program called MOUSE.CPP, available on the disk as MOUSE.EXE, which prints the x and y coordinates of the mouse and the status of the mouse buttans on the text display, updating them continuously as you move the mause and press the buttons. (Pressing the right mause button also terminates the program, so you'll only see the pressing of this button reported briefly before the program ends.) It does this
C HAPTE:R T H R E E
ßdsic In put Techniques
by assuming the initial position of the mouse to be at coordinates 0,0, then calling relposO on every pass through a loop and adding the relative changes in the mouse position to these coordinates. At the same time, it calls readmbuttonQ to check for the pressing of the mouse button . The gotoxyO library function, prototyped in the CONI O . H header file, is used to format this information neatly in the middle of the text screen. (The clrscrO function, prototyped in the same file, is used to dear the display while the program is initialized.) The MOUSE outpur looks like this: Mouse
X
Mouse
Y=
Left
442 -279
button
Ri ght
not
button
pressed
not
pressed
Listing 3-8 The M O U S E . C PP prog ra m II
MOU S E . C P P
II I I Demon s t r a t e s m o u s e i n t e r f a c e f u n c t i o n s II I I W r i t t en b y C h r i s t op h e r Lampton II
f o r Ga rdens of
# i n c l ude
I ma g i n a t i o n ( Wa i t e Gr oup P r e s s )
< s t d i o . h>
# i n c l ude
< s t d l i b . h>
# i n c l ude
< c o n i o . h>
# i n c l ude
" i o . h"
vo i d ma i n ( vo i d ) { i n t bstatus,xre l , yre l ; i n t mousex=O,mous ey=O ; II
II
Mouse x , y po s i t i on s
C l ea r t e x t d i s p l a y :
c l rs c r O ; II
I n i t i a l i z e m o u s e d r i ve r :
if
( ! i n i t mous e ( ) )
ex i t ( 1 ) ;
I I Read mouse b u t t o n s t a t u s : b s t a t u s = r eadmbu t t o n ( ) ; I I Loop un t i l
r i g h t mouse but to n i s p r e s s e d :
w h i l e ( ! ( b s t a t u s & RMBUTTON ) ) II
Read
{
re l a t i v e mou se po s i t i on :
continued on next pagt
GARDENS O F IMAG I NAT I O N continuedfrom previous page
r e l po s < &x r e l , &y r e l ) ; II
Upd a t e m o u s e po s i t i o n va r i a b l e s :
m o u s e x +=x r e l ; m o u s ey+=y r e l ; I I W r i t e mouse po s i t i on s o n d i s p l a y : g o t ox y ( 20, 1 0 ) ; p r i n t f ( "Mo u s e X=%6d" , m o u s e x ) ; g o t oxy ( 2 0 , 1 1 ) ; p r i n t f < " Mo u s e Y=%6d " , m o u s e y ) ; I I R e a d mo u s e b u t t on s t a t u s : b s t a t u s = r e a dmbu t t on ( ) ; I I W r i t e b u t t on s t a t u s on d i s p l a y g o t o x y ( 20 , 1 2 ) ; if
( b s t a t u s & LMBUTTON )
e l s e p r i n t f < " Left
p r i n t f < " Le f t b u t t o n p r e s s ed
b u t ton not
");
p re s s ed " ) ;
g o t o x y ( 20, 1 3 ) ; if
( b s t a t u s & RMBUTTON )
e l se print f < "R i ght
p r i n t f < " R i g h t b u t t o n p r e s sed
");
but ton not pressed " ) ;
} }
T h e PC J oystick Finally, we come to rhe joystick. lt's hard to rhink of the joystick as being a Strange and obscure input device - joysticks for home gameplay have been around at least si nce the Atari 2600 game machine was released back in the 1 970s - yet it's probably the hardest input device to find solid information about. This may be because it's only used for game input and not for "serious" p urposes (tho ugh flight sim ulator fans, who have been known to take their hobby very seriously, may di sagree with rhis s u mmation) . H igh -mind ed programming texts usually don't conrain information on such frivolous devices as rhe j oystick. There are a couple of joystick functions in the PC's BIOS, but they aren't especially useful. As with the keyboard, we'll write our own assembly language joystick routines rhat interact directly with rhe joystick hardware. We'll pur these ftmctions into I O .ASM, along with our growing array of assembly language 110 functions for orher PC input devices.
A n a l og J oysti cks The PC joystick i s a n analog joysrick. This means that i t can give us information that rhe joysricks attached to certain other computers, such as the Atari ST and
C HAPTE:R TH RE:E:
ßdsic Input Techn i q ues
Commodore Amiga, cannot. Not only can it tell us whether the user is pressing the joystick buttans and pushing the stick up, down, right, or left - but it can tell us how for the user is pushing the joystick. Alas, this makes the joystick rather difficult to program. On an assembly language level, we can receive information about the joystick by inputring data through port 020 1 h, with a series of instructions such as this: mov out in
d x , 0201 h dx , a l a l , dx
; Put gamepo r t a d d r e s s i n DX ; Send ra ndom data t h r ough po r t ; G e t va l i d d a t a f rom po r t
The first instruction establishes 020 1 h as the number of the porr we wish to send data through. The second instruction ourputs random data through the port, which signals to the joystick circuitry that we wish to receive information about the stick position. And the third instruction receives data from the joystick and places it in regisrer AL. The value that we receive from the joystick with these instructions is known as the gameport byre. The individual bits in the gameport byre give us information abour the position of the joystick - in fact, they can give us simultaneaus information about two joysticks - but they do not yield this information easily. They also give us information about whether the joystick buttans are being pressed. This information is more easily decoded, so we'll talk about it first.
Rea d i n g the J oystick B u tto ns Two joysticks can be attached t o the P C gameport a t one time; we'll call these joystick A and joystick B. Each joystick has two buttons, which we'll call button 1 and button 2. This means that we can read the status of up to four different buttans in the gameport byre. Bits 4 and 5 of the gameport byre represent the status of buttans 1 and 2 on joystick A. Bits 6 and 7 of the gameport byre represent the Status of buttans 1 and 2 on joystick B. When one of these bits is set to 0, the equivalent button is being pressed. When one of these bits is set to 1 , the equivalent button is not being pressed. This is not exacdy intuitive. One would expect it to work the other way around, with a 1 bit representing a button that's being pressed and a 0 bit representing a button that isn 't being pressed. So we'll flip the b its in the gameport byre before we send this information back to the calling program. We can do this with the assembly language NOT instruction, which changes Os to 1 s and 1 s to Os. A short assembly language routine for reading the gameport byre, flipping the appropriate bits, and passing the resulting value back to a calling CIC++ program is shown in Listing 3-9. The calling program must provide a parameter for this function, telling it which joystick buttans the function should read. This parameter consists of a binary "mask" in which those bits are set to 1
GARDE:NS O F IMAGI NATI ON
that correspond to the bits in the gameport byte that the program is interested in reading. We'll show you how to use this function in a moment. Note that the gameport is referred to in this function by the constant GAMEPORT, which is defined elsewhere in the IO.ASM module as equal to 020 1 h.
Listing 3-9 The readj b utto n o f u n ction _re a d j b u t t o n
PROC
; R e a d j oy s t i c k b u t t o n s p e c i f i ed by BMAS K ARG
bma s k : WO R D
push bp mov
bp,sp
mov
d x , GAMEPORT
mov
ah,O
out
dx, a l
in
a l , dx
not
al
mov
bx , bm a s k
a nd
a l,bl
pop
bp
; ; ; ; ; ; ;
Po i n t D X a t
j oy s t i c k po r t
Z e ro h i g h by t e o f r e t u r n va l u e Reques t d a t a
f rom port
G e t va l ue f rom j oys t i c k F l i p but ton bi t s M a s k out a l l And
L ea v e
but
resu l t
r e q u e s t e d bu t t ons i n AX
ret _re a d j b u t t o n
ENDP
Dete rm i n i n g the Sti c k Positi o n Reading the position of the stick itself is a great deal more difficult. The position of j oystick A on the x axis - that is, in the left and right direction - is represented by bit 0 of the gameport byte while the position of joystick A on the y axis - that is, in the up and down direction - is represented by bit 1 of the gameport byte. Similarly, the position of joystick B on the x axis is represented by bit 2 of the gameport byte, while the position of joystick B on the y axis is represented by bit 3 of the gameport byte. How can a single bit in the gameport byte represent the position of a joystick? With great difficulty. When we first write random data to port 020 1 h and read the value of the gameport byte, the bits representing the various stick axes will always be set to 1 . So we must continue reading the gameport byte from port 0 2 0 1 h in a loop, until the bit corresponding to the stick axis that we are interested in is reset to 0. The amount of time that it takes for this to happen will tell us what position the stick is in on that axis. If it resets quickly, the stick is pulled to the left or up (depending on which axis we are measuring) . If it resets slowly, the stick is pulled to the right or down. If it resets in an intermediate amount of time, the stick is somewhere in the middle. Thus we must time the
CHAPTE:R T H R E E
ßdsic I n put Tech niques
number of loops required for the appropriate bit to reset in order to determine the stick position. The easiest way to do this is with the aid of two 80x86 assembly language instructions: TEST and LOOPNE. The TEST instruction tests the value of a specific bit in a specific register, while the LOOPNE instruction loops back to an earlier instruction as long as the result of the last operation (which in this case will be the TEST operation) wasn't 0. LOOPNE also subtracts 1 from the value in the CX register every time it loops (and stops looping if that value reaches 0). If we test the bit of the gameport byte that we are interested in with TEST, we can use the LOOPNE instruction to keep looping unril the bit resets to 0. And if we begin looping with a value of 0 in the CX register (which will roll over to FFFFh the first time LOOPNE executes, prevenring the loop from terminating immediately) , we can prevent the loop from taking forever to terminate - in case something is seriously wrong with the joystick hardware - while keeping a count of the number of times the loop has executed. Of course, since the value in CX is decremenred rather than incremenred every time the loop is executed, we'll need to subtract the value in CX from 0 to determine the actual number of times the loop repeated before the gameport byte was reset to 0. An assembly language function that does all of these things is in Listing 3- 1 0.
Listing 3-1 0 The readstickO f u n ction _rea d s t i c k
PROC
; R e a d c u r r ent po s i t i o n of ARG
j o y s t i c k o n a x i s spe c i f i ed b y BMASK
bma s k : WORD
p u s h bp mov
bp, sp
cli
; ;
mov
a h , by t e p t r bma s k
mov
a l ,O
mov mov out
Turn off
i n t e r r u p t s , wh i c h cou l d
e f f e c t t i m i ng
;
Get b i tma s k i n t o a h .
d x , GAMEPORT
;
Point
cx,O
;
P repa re t o
dx , a l
; S e t j oy s t i c k b i t s
a l , dx
;
R e a d j oy s t i c k b i t s
test a l ,ah
;
Is
DX a t
j o ys t i c k po rt L oop 6 5 , 536 t i me s to 1
L oop2 : in
; L oopne
L oop2
; ;
sti
; ;
mov
reque s t ed b i t
sti l l
( i n bi tma s k )
1?
I f so ( a nd ma x i mum count
isn ' t
done ) , t r y a g a i n Count
is
f i n i shed,
so reena b l e
i n t e r rupts
ax,O continu.ed on next page
GARDENS OF IMAGINATION continuedfrom previous page
sub a x , cx pop bp ret _r e a d s t i c k ENDP
; S u b t r a c t C X f rom z e ro, t o g e t count
U s i n g the J oysti c k F u n cti o n s The j oystick functions are both simpler and more complicated to use than the earlier input functions in this chapter. No initialization is required before they can be used. And only rwo types of data are returned by these functions: stick position data and button data. Both the readjbutton() and readstick() functions are called with a s ingle parameter - a bi tmask that teils the functions which bits to read in the gameport. So that you don't have to worry about which binary digits are which, we've provided appropriate masks as constants in the IO.H file. These constant masks are JOY_X and J OY_Y, for reading the x and y axes of j oystick A, and J B UTTON 1 and JBUTTON2 for reading buttons 1 and 2 of joystick A. (You'll have to come up with your own masks for reading joystick B.) For i nstance, to read the x axis position of j oystick A, call the readstick() function like this: i n t s t i c k 1 = r e a d s t i c k ( J OY_X ) ;
This will set the integer variable stickl to the number of times the assembly language loop executed when timing the bit 0 of the gameport, which gives you a method of calculating the position of joystick A. (We'll show you how to use this value in a moment.) Similarly, to read the y axis position of joystick A, call the readstick() function like this: i n t s t i c k 1 = r e a d s t i c k ( J OY_Y ) ;
To read the status of button 1 o n j oystick A, call the readjbutton() function like this: i n t b u t t o n 1 =rea d j b u t ton ( J BUTTON 1 ) ;
If the value rerurned by the function is non-zero, then burron 1 is being pressed. If the value is 0, then it isn't being pressed. Similarly, to read the status of button 2 on joystick A, call the readjbutton() function like this: i n t b u t ton2=rea d j b u t t o n ( J BUTTON2 ) ;
If you need to know whether one or the other of the buttons is being pressed, but don't care which one, you can read them both at the same time like this: i n t b u t ton= read j bu t t on ( J BUTTON 1 + J BUTTON2 ) ;
CHAPTER THREE
ßdsic I n put Techniques
If the value returned by the function is non-zero, then one or both of the buttans is being pressed. If the value is 0, then neither is being pressed. Note that you can't use this trick with the readstick() function to read the value of the stick on both axes simultaneously. Well, you can, but the value returned would be meaningless. (Actually, it would be the greater of the x and y axis values, but there would be no way for you to tell which one it was.)
Ca l i b rati ng the J oystick The hardesr part of using the joystick functions i s figuring out what the value returned by the readstick() function means. Recall that this value is rhe number of times that the assembly language loop that read the stick value had to loop before the appropriate bit in the gameport reset to 0. But what does this tell you about the physical position of the stick? Thar's difficult to say, because on different computers it will mean different things. The faster the computer, the more rimes the timing loop will execute before the bit resets to 0. So a number that means the joystick is being pushed to the right on a slow machine may mean the joystick is being pushed to the left on a fast machine. How do you determine what these numbers mean for the machine the program is actually running on? The answer is that you must calibrate rhe joystick before rhe game can begin. You must prompt the user to move the joystick to the extreme upper left and perform some action, such as pushing a joystick button, that will signal when the stick is in position. When this signal is received, you must call the readstick() function to find out what number is returned for that position. You must then repeat this operation for rhe center and lower right positions, noting the numbers for each. Once you have these numbers, you can use them for determining the joystick positions while the game is actually being played.
The JOYST I C K P ro g ra m Listing 3-1 1 demonstrates how to use the joystick functions in a program. The logic of the JOYSTICK.CPP program is similar to that of the MOUSE.CPP program earlier in rhis chapter, though the need to calibrate the joystick makes this listing a bit more complicated. To keep things as simple as possible, we've calibrated only one set of reference values for the joystick - the ones for the center position . The program then loops repeatedly, measuring the joystick position on both axes and announcing if it's to the left or right of, or exactly in, this central position. It also notes whether the buttans are pressed. When the second button is pressed, the program terminates. While running this program
G A R D E N S O F I MAG INATION
(which is available on the disk as JOYSTICK.EXE) , you'll note that it's extremely difficult to get the j oystick precisely in the center. In practice, we'll need to represent the center position as a range of values around this reference position, so that slight joystick slippage to either side won't be taken as a command from the user. The output looks like this: X a x i s po s i t i on : Left of
cent e r
Y a x i s po s i t i on : Above c e n t e r But ton 1
not pressed
But ton 2 not pressed
Listing 3-11 The J OYSTI C K . C P P prog ra m II II II II II II
J OY ST I C K . C P P Demon s t r a t e s
j oy s t i c k i n t e r f a c e f un c t i o n s
W r i t t e n by C h r i s t o p h e r L a m p t o n for
Ga rdens o f
I ma g i n a t i on
# i n c l ud e
< s t d i o . h>
# i n c l ud e
# i n c l ud e
< c o n i o . h>
# i n c l ud e
"io.h"
( Wa i t e G ro u p P r e s s )
v o i d m a i n ( vo i d ) { i n t x s t i c k , ys t i c k ; i nt xcent ,ycent ; i nt
status 1 ,status2;
I I C l e a r t he text d i s p l a y : c l rs c r O ;
I I C a l i b r a t e t h e u s e r ' s j oys t i c k : p r i n t f ( " \ n \ n C en t e r your
j oy s t i c k a nd p r e s s b u t t o n " ) ;
p r i n t f < "one . \ n " ) ; whi le
( ! r e ad j bu t t on ( J BUTTON1 ) ) ;
x c e n t = r e a d s t i c k ( J OY_X ) ; y c e n t = r e a d s t i c k ( J OY_Y ) ; whi le
( r e a d j b u t t on ( J BUTTON 1 ) ) ;
I I S e t up d i s p l a y : c l rs c r ( ) ;
II II II II II II
Loop un t i l
j oys t i c k
but ton pressed Get
x c o o rd i n a t e
G e t y coor d i n a t e Loop un t i l r e l ea s ed
bu t t on
CHAPTER THRE:E
ßdsic I n put Techniques
g o t oxy ( 20 , 1 0 > ; pr i n t f < " X a x i s po s i t i o n : " ) ; goto x y ( 2 0, 1 2 ) ; pr i n t f < " Y a x i s po s i t i o n : " ) ; II
Read but to n 2 s t a t u s :
s t a t u s 2 = r e a d j but ton ( J BUTTON2 > ; I I Loop un t i l
r i g h t m o u s e bu t t on i s p r e s s e d :
whi le( ! status2) II
{
Read X a nd Y a x i s po s i t i o ns :
x s t i c k = r e a d s t i c k ( J OY_X > ; y s t i c k= r e a d s t i c k ( J OY_Y > ; I I Wri te
j o ys t i c k po s i t i o n s on di sp l a y :
gotoxy ( 2 0, 1 1 ) ; if
( x s t i c k< x c e n t )
printf x c e n t )
pri ntf < "
R i ght of
if
( x s t i c k= = x c e n t )
cente r " ) ;
");
C e n t e red
pri ntf < "
g o t oxy ( 2 0, 1 3 ) ; if
( ys t i c kycen t ) p r i n t f < "
Be l ow c e n t e r " ) ;
if
( ys t i c k==ycen t )
print f ; I I W r i t e but ton s t a t u s on d i sp l a y gotoxy ( 20 , 1 4 > ; if
( status1 )
pri n t f < "Button 1
e l s e pr i n t f < " B u t ton 1
not
pressed
p r e s s ed" ) ;
");
g o t oxy ( 2 0, 1 5 ) ; if
( s t a t us2 )
pr i n t f < " Bu t ton 2 p r e s s ed
e l s e p r i n t f < " B u t t o n 2 not
");
p r e s sed" ) ;
} }
The Eve nt M a n a g e r The low-level input functions that we have been developing in this chapter can be called directly from a game program to determine what sort of input commands are coming from the user through the input device of choice, be it the keyboard, joystick, or mouse. l t would make our lives as programmers a lot easier, though, if there were some sorr of protecrive layer between rhe game program and rhe raw data input by these low-level functions, a protective layer that would translate this raw input into a form more appropriate for our game.
GARDE:NS OF I MAGI NATI O N
That's the job of an event manager. An event manager is a routine or set of routines that monitors the input from the low-level functions and passes on only the information needed by the rest of the program, in a form that the program can easily use. Unlike our low-level input functions, which can be used in pretty m uch the same fo rm by j us t about any type of comp uter program from a spreadsheet to a flight simulator, the event manager will be custom-designed for the type of program with which we intend to use it. In this chapter we want to write an animared maze exploration program based on the m aze-drawing routi nes presented in the last chapter. It would be appropriate for such a program to receive input from the user concerning the type of movement that the user wishes to make thro ugh the maze. For instance, if the user wishes to move forward down a corridor of the maze, he or she could press the (!) key, push the j oystick forward, or move the mouse forward on the desktop. If the user wishes to rotate to the right, he or she could press the 8) key, push the joystick to the right, or move the mouse to the right. And so forth. The event manager for such a program could watch for the appropriate actions to be performed, then translate those actions into a set of "movement events" which could be passed back to the game program. If the user pushes the joystick fo rward, for instance, a "forward movement" event could be returned to the program. Similarly, if the user presses the right arrow key, a "rotate right" event could be returned to the program. The program need not know whether these events were generared by the joystick, the mouse, or the keyboard (or by some even more exotic input device that we've added to the event manager at the last moment, such as a touch screen or data glove) . The program only needs to know that the user has requested forward movement, so that it can try to provide what the user wants. The program can then be written without any specific concern for the nature of the input devices that are generaring these events.
M a z e Events Before we can write the event manager, then, we need to decide on the set of events that will be required by a simple maze exploration program. The event manager we develop in this chapter will recognize five types of event, which we'll call go_forward, go_back, go_left, go_right, and quit_game. To pass these events from the event manager to the calling program, we'll use an event structure that we'll define like this: II
S t r u c t u r e f o r pa s s i n g even t s
s t r u c t e v e n t_s t r u c t
to
c a l l i ng p r og ram :
{
i n t g o_f o rw a r d , go_ba c k , g o_l e f t , g o_r i g h t , q u i t_9ame; };
CHAPTER THRE:E:
ßdsic Input Tech n i q u es
We'll place this definition in the file EVNTM GRl . H . The " 1 " in the file name indicates that this will not be the last event manager that we create in this book. The event structure will be declared by the calling program and passed to the event manager as a parameter. The event manager will then change the value of the fields in the structure to indicate what events have occurred and pass it back to the calling program . When the event manager sets a field in the structure to a non-zero value, it indicates that the event has occurred (or is still occurring, in the case of continuous events such as keys being pressed or sticks being pushed) . Because more than one field can be set to a non-zero value at the same time, the event manager can pass m ultiple eve nts back to the calling program . For example, this structure could be used to indicate that the user wants to move forward and turn right simultaneously.
The i n it_eve ntso F u n cti o n The event manager will be initialized by calling the init_events() function, shown in Listing 3- 1 2 . This does nothing more than initialize the low-level keyboard and mouse handlers. (The j oystick handler doesn't require any initialization, although the user's joystick will need to be calibrated.) The address of a 1 28-byte array of type char called keybu.lfer (defined elsewhere in the event manager module) is passed to the keyboard handler.
voi d i n i t_even t s ( ) I I I n i t i a l i z e event m a n a g e r { I I I n s t a l l a l t e r n a t e keyboard d r i ve r i n i t ke y ( keybu f f e r ) ; I I I n i t i a l i z e t h e mou se d r i ve r i n i tmouse O ; }
The J oysti ck Ca l i b rati o n F u n cti o n s There are three functions fo r cal ibrating the j oystick: one that determ i nes the center positions in both axes (the setcenter() function) , one that determines the farthest up and left positions (the setmin() function) , and one that determines the fanhest down and right positions (the setmax() function). The values for these positions are stored in a set of variables that are global to the event manager module, so they can be used later to determine whether the joystick is pointing to
G A R D E:NS OF I MAG I NATION
the left o r right, up or down. These fu nctions m ust be called during the initial ization of the game, after the user has been instructed to move the joystick into the appropriate positions and press j oystick button 1 . The text of the functions appears in Listings 3- 1 3, 3- 1 4, and 3- 1 5 .
Listing 3 -1 3 The setcentero fu n ction vo i d s e t c e n t e r ( ) II
Set
center
j o ys t i c k c o o r d i na t e s
{ whi le
( ! rea d j bu t t o n ( J BUTTON1 ) ) ;
I I Loop un t i l II
j o ys t i c k
b u t t on p r e s s e d
x c e n t = r e a d s t i c k ( J OY_X ) ;
I I G e t x coordi nate
y c e n t = r e a d s t i c k ( J O Y_Y ) ;
I I G e t y coordi nate
whi le
I I Loop un t i l bu t t on
( r e a d j b u t t on ( J BUTTON 1 ) ) ;
II
r e l ea s ed
}
Listing 3-1 4 The set m i n O f u n ction vo i d s e t m i n O I I S e t m i n i mum j oy s t i c k c o o r d i n a t e s { whi l e
( ! re a d j bu t ton ( J BU T T O N 1 ) ) ;
I I Loop un t i l II
j oy s t i c k
but ton p r e s s ed
x m i n = r e a d s t i c k ( J OY_X ) ;
I I G e t x coo r d i n a t e
ymi n = r e a d s t i c k ( J O Y_Y ) ;
I I G e t y coo r d i n a t e
whi le
I I L o o p un t i l
( r e a d j but t on ( J B UTTO N 1 ) ) ;
II
bu t t on
r e l e a s ed
}
vo i d s e t ma x ( ) II
S e t ma x i mum j oy s t i c k c o o r d i na t e s
{ whi le
( ! r e ad j b u t ton ( J BU T T O N 1 ) ) ;
II II
Loop un t i l
j oys t i c k
b u t ton p r e s s e d
xma x = r e a d s t i c k ( J O Y_X ) ;
I I G e t x c oo r d i n a t e
yma x = r e a d s t i c k ( J O Y_Y ) ;
I I G e t y coor d i n a t e
CHAPTE:R THREE wh i l e
( r ea d j bu t t o n ( J BUTTON 1 ) ) ;
II Loop un t i l II
ßdsic I nput Tech niq ues
bu t t on
r e l ea s ed
}
The geteve nto F u ncti o n The main body o f the event manager is the geteventO function. This function takes two parameters: an integer bitmask specifying which types of events are desired, and an event structure in which the current events (as it were) will be returned. The bitmask consists of the constant values KEYBOARD_EVENTS, MOUSE_EVENTS, and J OYSTICK_EVENTS (defined in EVNTMGR l .H) added tagether as desired. For instance, to obtain keyboard events and joystick events, you would call the event manager like this: g e t eve nt ( KEYBOARD_EVENTS+ J OY S T I C K_E V E N T S , &eve n t s ) ;
where &events is a pointer to a variable of type event_structure. The geteventO function begins its work by clearing any previous events out of the event_structure variable that it is to return events in: II
C l e a r a ny eve n t s
in
structure :
event s->go_f o r wa rd=O; even t s ->go_b a c k=O; even t s ->go_L e f t =O ; even t s ->go_r i g h t=O; event s->q u i t_game=O;
If joystick events have been requested, it then calls the low-level j oystick functions to determine if any events have occurred. If they have, it sets the appropriate fields in the event structure: II
If
j oy s t i c k even t s
reque s t ed . . . .
i f ( event_ma s k & J OY S T I C K_EVENTS ) if
( r e a d s t i c k ( J OY_Y ) < ( x c e n t -4 ) )
if
( r e a d s t i c k ( J OY_Y ) > ( x c en t + 1 Q ) )
if
< r e a d s t i c k ( J OY_X ) < ( x c e n t -4 ) )
if
( r ead s t i c k ( J O Y_X ) > ( x c en t + 1 Q ) )
if
( read j bu t t on ( J BUTTO N 1 ) )
{ even t s->go_f o rwa rd=1 ; event s->go_ba c k=1 ; even t s->go_L e f t = 1 ; even t s ->go_r i g h t = 1 ;
even t s -> q u i t_game =1 ;
}
If mouse events have been requested, then the event manager calls the low level mouse functions to determine if any events have occurred. If they have, it sets the appropriate fields in the event structure: II
I f mouse eve n t s
if
( e vent_ma s k & MOUSE_EVEN T S ) r e l po s < &x , &y ) ;
r e q u e s t ed . . . . {
I I R e a d r e l a t i ve mou s e po s i t i on
GARDE:NS OF I MAG I NATI O N if
( r e a d s t i c k ( J OY_Y ) < ( x c e n t -4 ) )
if
( r e a d s t i c k ( J O Y_Y ) > ( x c e n t + 1 0 ) )
if
( r e a d s t i c k ( J O Y_X ) < ( x c e n t -4 ) )
if
( r e a d s t i c k ( J O Y_X ) > ( x c e n t + 1 0 ) )
if
( r e a d j b u t ton ( J BUTTON 1 ) )
eve n t s ->go_f o r wa rd=1 ; e v e n t s ->go_ba c k =1 ; eve n t s ->go_L e f t = 1 ; eve n t s ->go_r i g h t = 1 ;
event s->q u i t�ame =1 ;
}
If mouse evenrs have been requested, then the event manager calls the low level mouse functions to determine if any evenrs have occurred. If they have, it sets the appropriate fields in the event structure: II
I f m o u s e eve n t s
if
( e ven t_ma s k & MOU S E_E V E N T S ) r e l po s < &x , &y ) ;
r eq u e s t e d . . . . II
Read
if
( y 5 ) e v e n t s->go_b a c k= 1 ;
even t s ->go_fo r wa rd =1 ;
if
( x 2 0 ) eve n t s ->go_r i g h t = 1 ;
event s->go_L e f t = 1 ;
i n t b= rea dmb u t t o n ( ) ; if
{
r e l a t i v e m o u s e po s i t i on
( b&MBUTTON 1 )
II
R e a d m o u s e but t o n
eve n t s->q u i t�ame= 1 ;
}
Finally, if keyboard events have been requested, the event manager checks the
keybuffer array to determine if any evenrs have occurred. If they have, it sets the appropriate fields in the event structure: II
If
if
( e vent_ma s k & KEYBOA R D_E V E NTS )
keyboa rd eve n t s
reque s t ed . . . . {
if
( k eybu f f e r [ F O R W A R D KE Y J )
eve n t s ->go_f o r wa rd=1 ;
if
( keybuf f e r [ BA C KKEY J )
e v e n t s->go_ba c k = 1 ;
if
( k eybuf f e r [ L E FTKEY J )
e v e n t s->go_ L e f t = 1 ;
if
( k eybuf f e r [ R I G H T K E Y J )
if
( keybuf f e r [QU I T K E Y J )
event s->go_r i g h t = 1 ; e v e n t s->q u i t�ame=1 ;
}
The cons tants used here for the keyboard scan codes are defined in the EVNTM G R l . H fi l e . F O RWAR D KEY is the scan code fo r the (!) key, BACKKEY is the scan code for the (I) key, LEFTKEY is the scan code for the CB key, RIGHTKEY is the scan code for the G key, and QUITKEY is the scan code for the (ESC) key. However, the definitions of these constants can be changed in the header file so that any set of five keys can be used to trigger these events. For instance, the scan code fo r the • 8 • key on the keypad could be assigned to FORWARD KEY, and so forth. By using variables in place of these constants, a game program could be designed in such a way that the player could redefine at any time the keys used to trigger the evenrs, perhaps from a special configuration screen. The complete text of the getevent() function appears in Listing 3- 1 6 .
C HAPTER THREE
ßdsic I n put Techniq ues
e v e n t s ->go_r i g h t =O; event s->q u i t_ g a me=O;
I I I f j oys t i c k eve n t s r e q u e s t e d . . . . i f ( event_ma s k & J OY S T I C K_EVENTS ) if
}
( r e a d s t i c k ( J O Y_Y ) < ( x c e n t -4 ) )
if
( r e a d s t i c k ( J O Y_Y ) > ( x c e n t + 1 0 ) )
if
( r e a d s t i c k < J O Y_X ) < ( x c e n t -4 ) )
if
( r e a d s t i c k ( J O Y_X ) > ( x c e n t + 1 0 ) )
if
( r e a d j but t on ( J BUTTO N 1 ) )
e v e n t s->go_ba c k= 1 ; e v e n t s->go_L e f t = 1 ; e v e n t s->go_r i g h t =1 ;
e v e n t s->qu i t_game= 1 ;
I I I f mouse even t s r e qu e s t ed . . . . i f ( event_ma s k & M O U S E_E V E NTS ) re l po s < & x , &y ) ; if
( y 5 )
{
II Read re l a t i ve mouse po s i t i on
e v e n t s->go_f o r wa rd=1 ; e v e n t s->go_ba c k=1 ;
if
( x20 )
e v e n t s->go_L e f t = 1 ; eve n t s ->go_r i g h t = 1 ;
i n t b=readmbut t on ( ) ; if }
{ e v e n t s ->go_f o r wa rd=1 ;
( b&MBUTTON 1 )
I I Read mouse bu t t on
e v e n t s->qu i t_ g a me= 1 ;
I I I f keyboa rd eve n t s r e q u e s t ed . . . . i f ( event_ma s k & KEYBOARD_EVE N T S ) if
( keybu f f e r [ FORWA R D KEY J )
{
event s->go_f o rwa rd=1 ;
if
( keybu f f e r [ BA C KKE Y J )
e v e n t s->go_ba c k=1 ;
if
( k eybu f f e r [ L E F TKE Y J )
eve n t s ->go_L e f t = 1 ;
if
( keybu f f e r [ R I GHTKE Y J )
if
( k eybu f f e r [ Q U I T KE Y J )
e v e n t s->go_r i g h t = 1 ; eve n t s ->qu i t_ g a me= 1 ;
} }
A n i m ati n g t h e M a z e We now have almost all the tools we need for an animared tour of the maze we created a still picture of in the last chapter. You might think that the animation itself would require a great deal of additional code, but that's not true. Animation is simply a matter of drawing a sequence of successive images on the computer display, erasing one image, and then drawing the next image on top of it. In the last chapter we developed the code that draws the image; in this chapter we developed the code that allows the user to interact with that image. Now all we have to do is put them together and we have a game. Well, almost. . . . We'll reuse much of the same code we used in our maze-drawing program in the last chapter. We'll add some instructions to that code, to initialize the event manager and calibrate the joystick: I I I n i t i a l i z e event manage r : i n i t_eve nt s ( ) ;
GARDENS OF I MAG I NATION II
Ca l i brate the user ' s if
j oy s t i c k :
( W H I C H_EV E N T S & J O Y S T I C K_EVEN T S ) p r i n t f < " \ n C e n t e r you r
{
j o y s t i c k and p r e s s b u t t o n " ) ;
p r i n t f ( " on e . \ n " ) ; setcenter( ) ;
I I C a l i b r a t e t h e c e n t e r p o s i t i on
p r i n t f < " Move y o u r
j oy s t i c k
to t h e uppe r
L e f t hand " ) ;
p r i n t f ( " co r n e r a n d p r e s s b u t t on one . \ n " ) ; setmi n ( ) ;
I I C a l i b r a t e t h e m i n i mum po s i t i on
p r i n t f < " Move y o u r
j o ys t i c k t o t h e
L ow e r r i g h t h a nd
"
);
p r i n t f ( " c o r n e r a n d p r e s s b u t t on o n e . \ n " ) ; s e t ma x ( ) ;
II
C a l i b r a t e t h e m a x i mum po s i t i on
}
We'll need a method for timing the frames of the animation, so that they don't Bash by too quickly on faster machines (or too slowly on slower machines) . We'll do this by calling the CIC++ library function clockO, defined in the header file TIME. H . This function returns the number of ticks that have transpired since the p rogram has Started runni ng. How long is a tick? That may vary from computer to computer, so the CIC++ compiler maintains a special macro called CLK_TCK that can be used to obtain the number of clock ticks in a second. To determine how many ticks we wish to wait between animation frames, we'll establish a constant called FRAMES_PER_SECOND, which we'll initially define as being equal to 6: # d e f i ne
F RAM E S P E R_S E C O N D 6
Then we'll divide CLK_TCK by frames per second to obtain the number of ticks per frame: II
S e t num b e r o f
t i c k s per
t i c k s_ p e r_f rame = C L K_T C K I
f rame : F RA M E S_P E R_S E C ON D ;
So that we'll know j ust how much time the first frame o f the animation takes, we'll record the current tick count in the variable last_frame. This variable must be of type clock_t, a special data type defined in the TIME. H header file (which is usually j ust a long integer) : II
I n i t i a l i z e t h e f ra me t i m e r :
c l o c k_t
L a s t f rame=c l o c k ( ) ;
Since our animation loop will continue until a quit_game event occurs, we need to be sure that the quit_game field in the events structure - the structure of event_type that we'll be using to pass events from the event manager - is set to 0, so that the animation loop will execute at least once: I I Make s u r e we get a t II
i n t o t h e ma z e :
e v e n t s . q u i t_ g a me=O;
L ea s t one
f r ame
CHAPTER THREE
ßdSiC I n put Techniques
Then we start executing an animation loop that will continue execuring as long as no quit_ game events occur: I I L e t ' s go f o r a wa l k i n t h e ma ze : w h i l e ( ! even t s . qu i t_ g a me )
{
We then call the drawmazeO function that we developed in the last chapter to draw our initial position in the maze (which is defined by the same set of variables established in the last chapter) : I I D r a w t h e ma ze
i n s c r e e n buf f e r :
d r a wma z e ( s c r ee n_bu f f e r ) ;
So that the image of the maze is not whisked too quickly from the display, we must pause until a number of clock ticks has passed equal to the ticks_per_frame value that we established a few instructions ago: II
P a u s e un t i l
whi l e
t i me f o r n e x t
f r ame :
( ( c l o c k ( ) - L a s t f r ame ) < t i c k s_ p e r_f r ame ) ;
We must then reset the value of the lastframe variable to the current tick count: II
S t a r t t i m i n g a no t h e r f rame :
L a s t f rame=c l o c k ( ) ;
If any movement events have occurred, we'll need to change our position within the maze. So we call the event manager to check for input: II
C h e c k for
i np u t e v e n t s :
ge t even t ( WH I C H_EV ENTS , &eve n t s ) ;
Should a go_forward event occur, we'll want to move to the next position in the maze along our current heading. You'll recall from the last chapter that our position within the maze is contained in the variable pos, which is of type xy (that is, it consists of an x field and a y field) and our heading is contained in the integer variable direction. The forward increments along that heading are contained in the array element increment[direction], which is also of type xy. By a d d i n g t h e v a l u e o f incremen t[directio n}. x to o u r x p o s i t i o n a n d increment[direction}.y to our y position, we can determine our next position along the current heading. However, we'll first assign this position to a temporary variable of type xy called newpos, so that we can test to see if the maze square at this position is empty before we change our actual position. If the next square is not empty, we can't move into it, because there's a wall in the way: II
Do we w a n t if
to move f o r w a rd?
( even t s . go_f o r w a r d )
{
newpos . x=pos . x+ i n c reme n t [ d i r e c t i o n J . x ; newpos . y=pos . y+ i n c reme n t [ d i r e c t i on J . y ;
GARDENS O F I MAG I NATION if
( ! ma z e [ newpos . x ] [ ne w p o s . y ] )
{
p o s . x=newpos . x ; p o s . y=n ewpos . y ; } }
I f a go_back event has occurred, we'll want to do the same thing in the direction opposite to our current heading. However, we'll make these instructions part of an else, so we can't move both forward and backward should the player be pressing both keys simultaneously: II
. . . or d o we w a n t else
if
to move ba c k w a r d ?
< e vent s . g o_ba c k )
{
n e w po s . x=pos . x- i n c r em e n t [ d i r e c t i on J . x ; n e w po s . y=pos . y- i n c r em e n t [ d i r e c t i o n J . y ; if
( ! m a z e [ n e w p o s . x ] [ ne w p o s . y ] )
{
p o s . x=n ewpos . x ; p o s . y=n ewpos . y ; } }
We'll also want to check to see if a go_right or go_left event has occurred. If so, we'll rotate our heading clockwise or counterclockwise, respectively: II
Do we w a n t
if
( eve n t s . g o_ L e f t )
to turn
Left?
{
- - d i r e c t i on ; if
( d i r e c t i on3 )
d i r e c t i on = O ;
} }
And that's the end of the animation loop. Everything is now set up for the next frame of the animation, which will be drawn by the call to the drawmazeO function that we placed at the top of the loop. When the loop terminates, we'll need to deinstall the event manager: I I T e rm i na t e e v e n t m a n a g e r : e n d_even t s ( ) ;
F i x i n g the F l i c k e r What sort o f animation will this loop produce? Alas, not quite the type we want. The program GOMAZE l .EXE on the disk is written exacdy as we have described
CHAPTER THREE
ßdsic Input Techniq ues
here. Run it and you'll see immediately what's missing. You can move through the maze by pressing the keys on the keyboard, but the animation of the maze is marred by an irritating flicker. What causes this flicker? You're watehing the image of the maze being drawn - and erased - six times a second. It makes sense that such an image would flicker, because it's constandy vanishing and reappearing. How can we fix this flicker? By drawing and erasing the maze in an offscreen buffer - and only moving the image onto the screen once it's complete. That way, one complete image will rapidly replace another complete image on the video display, with no flicker in sight. To accomplish the task of moving the image from the buffer to the display, we'll need a fast assembly language function to move a reetangular segment of memory into video RAM Why not move the whole contents of the buffer into video RAM? Because it takes too long. We only need to move that section of the screen that actually contains the maze. We'll use the putwindowO function, developed in the last chapter, for that task. .
The GO MAZ E P ro g ra m The final version of the mainO function of the GOMAZE.CPP program appears in Listing 3- 1 7.
Listing 3-1 7 The m a i n O f u n ction of the GOMAZ E . C P P prog ra m vo i d ma i n ( vo i d ) { i nt scan; event_s t r u c t even t s ; st r u c t x y newpo s ; II
I n i t i a l i ze event manage r :
i n i t_even t s ( ) ; I I Ca l i b r a t e t h e us e r ' s if
j oy s t i c k :
( W H I C H_E V E N TS & J OY S T I C K_EV E N T S ) p r i n t f < " \ n C e n t e r your
{
j oy s t i c k a nd p r e s s b u t t o n " ) ;
p r i n t f < "one . \ n " ) ; s e t cen t e r ( ) ; I I Ca l i b r a t e t h e c e n t e r po s i t i on p r i n t f < " Move your
j oy s t i c k
to t h e u p p e r
L e f t h a nd " ) ;
p r i n t f < " co r n e r a nd p r e s s b u t t o n one . \ n " ) ; setmi n ( ) ;
I I C a l i b ra t e t h e m i n i mum po s i t i o n
p r i n t f ( " Move your j oy s t i c k
to t h e
L ow e r
r i g h t h and " ) ;
p r i n t f ( " co r n e r a nd p r e s s bu t t on one . \ n " ) ; continued on next page
GARD ENS O F I MAGI N ATION continuedfrom previou.s page
s e tma x ( ) ;
II
C a l i b r a t e t h e m a x i mum posi t i o n
} I I C r e a t e po i n t e r
to v i deo memo r y :
c h a r f a r * s c r e e n= ( c h a r f a r * ) MK_F P ( Ox a 000, 0 ) ; I I C r e a t e o f f s c r e e n v i deo b u f f e r : char II
f a r *s c r ee n_b u f f e r=new u n s i gned
c h a r [64000 ] ;
S a v e pre v i o u s vi deo mod e :
i n t o l dmode=* ( i n t * ) M K_F P ( Ox 4 0 , 0 x 49 ) ; II
P u t d i sp l a y i n mode 1 3 h :
s e t mode ( Ox1 3 ) ; II
C l e a r di s p l a y :
c l s ( s c re e n_bu f f e r ) ; II
D ra w w i ndow o n d i s p l a y :
d ra wbox ( s c r e en_bu f f e r ) ; I I S e t numbe r o f t i c k s p e r f r ame : t i c ks_pe r_f r a m e = C LK_T C K I II
F RA M E S_PE R_S E C O N D ;
I n i t i a l i z e t h e f ra m e t i me r :
c l o c k_t
L a s t f rame=c loc k ( ) ;
II
Ma ke s u r e w e g e t a t
II
i nt o t he maze :
L ea s t
one f r ame
e v e n t s . q u i t_ g a m e = O ; II
L e t ' s go f o r a w a l k i n t h e m a z e :
w h i l e ( ! e v e nt s . q u i t_ game ) II
{
D r a w t h e ma z e i n s c r e e n buf f e r :
d ra wma z e ( s c r e e n_bu f f e r ) ; I I Move s c r e e n b u f f e r t o s c r een : p u t w i ndow ( 0 , 0 , 3 2 0 , 2 00 , s c r e e n_bu f f e r ) ; II
Pause unt i l
wh i l e
t i me f o r n e x t
f rame :
( ( c l o c k ( ) - L a s t f r ame ) < t i c k s_ p e r_f rame ) ;
I I S t a r t t i m i ng a n o t h e r f r ame : L a s t f r ame=c l o c k ( ) ; I I Check for
i nput event s :
g e t eve n t ( W H I C H_EVENTS , &even t s ) ; I I Move v i ewe r a c c o r d i ng t o i nput eve n t s : I I D o w e want if
t o move f o r w a r d ?
( event s . g o_f o r w a r d )
{
CHAPTER THREE newpos . x=pos . x+ i n c rement [ d i r e c t i o n J . x ; newpos . y=pos . y+ i n c rement [ d i r e c t i o n J . y ; if
( ! ma z e [ n ewpos . x ] [ n ewpos . y ] )
{
pos . x=newpos . x ; pos . y=newpo s . y ; } } II
. .
e l se
.
o r do we w a n t
if
to move
( eve n t s . go_ba c k )
ba c kw a r d ?
{
newpos . x=pos . x- i n c r e m e n t [ d i r e c t i o n J . x ; newpos . y=pos . y- i n c remen t [ d i r e c t i o n J . y ; if
( ! ma z e [ newpos . x J [ newpos . y J )
{
pos . x=newpos . x ; pos . y=newpos . y ; } } II
Do we w a n t
if
( eve n t s . go_L e f t )
to t u r n
Left?
{
--d i r e c t i o n ; if
( d i r e c t i on3 )
d i r e c t i on=O;
} } II
T e r m i na t e event ma n a g e r :
end_even t s ( ) ; I I Re l ea s e memo ry de l e t e s c r e e n_bu f f e r II
R e s t o r e o l d v i deo mode :
s e t mode ( o l dmode ) ; }
ßdsic I n put Tec h n i q u es
et's be honest. The wireframe maze animation that we created in the last two chapters, while it ill ustrated principles of graphics output and device input that could be used in a state-of-the-art game program, was not exacdy of commercial quality itself To the best of my knowledge, the last major commercial game that featured wireframe maze graphics was Wizardry V, published in 1 9 89. And that game looked like something from the distant past even at the time of its publication. More recent installments in the Wizardry series have made quantum leaps beyond the simple wireframe mazes of the first five entries. As entertaining as the early Wizardry games were (and still are) , no serious game programmer can get away with this sort of simplistic maze animation in the 1 990s. So, in this chapter, we're going to make a quantum leap of our own. We'll rhrow the wireframe maze graphics aside and build the core of a bitmapped maze engine similar to those used in such up-to-the-minute games as Eye of the Beholder 1 , 2, & 3 and Wizardry VII: Crusaders of the Dark Savant. This engine will use many of the programming principles that we've discussed in the last two chapters, but what you'll see on the screen will no longer be white lines on a black screen. lnstead, you'll see a realistic, full-color animared maze that could have leaped right out of one of the games on the current software bestseUer list. As a bonus, we'll show you how to generate new graphics for this maze engine using software that can be obtained quite inexpensively or even (if you play your cards right) for free.
GARDE:NS OF I MAG I NATION
These b itmapped maze graphics will b e constructed out of predesigned graphic bitmaps, which (as it happens) we'll be creating using the ray-tracing program POVRAY. You can use other methods for constructing bitmapped maze graphics - so-called "paint" programs like Electronic Ans' Deluxe Paint are an excellent way to create bitmaps, if you have the requisite artistic talent (or can afford to hire the services of someone who does) . Nevertheless, a ray tracer is a great way to throw tagether quick-and-di rty graphics of surprising beauty and realism, especially if you are less than deft at painting pixels yourself. Before we can use the bitmaps created by POVRAY to create a maze, though, we must have some method of storing these bitmaps on disk and loading them into the computer. So in the first ponion of this chapter, we'll concentrate not on creating bitmaps, but on storing and retrieving them from disk.
What ls a B itma p ? We saw in chapter 2 how computers like the IBM PC and its clones store the current video image in memory as a sequence of byte values representing either characters or pixels on the display. In mode 1 3h, each byte in video memory represents the color of a corresponding pixel on the screen. Because these bytes of data, like all numbers held in the electronic circuitry of a computer, are made up of in divid ual bin ary digits, or bits, it's common to refer to this method of representing pixels as a bitmap. In the case of mode 1 3h, it might seem more appropriate to use the term "bytemap," but the term bitmap goes back to the days when it was possible to describe the state of pixels on the typical video display using only 1 or 2 bits wirhin a byte. This is still the case with the PC's rwo- and four-color graphics modes, but as computer graphics move into the era of 25 6-color images and beyond, bitmaps made of individual bits are becoming increasingly less common. Now 4 or 8 or even 24 bits per pixel may be required. Bitmaps aren't always stored in video memory. In the last chapter, we stored the bitmap representing the next frame of animation in an offscreen buffer in video RAM until we were ready to move it onto the video display. For Ionger term storage, it's quite possible to store bitmapped images in a disk file on a hard drive or CD ROM . In fact, most computer games store their graphics in precisely this way. If you've ever wondered why the typical computer game takes up several megabytes of space on your disk, it's probably because the game includes a vast library of 2 56-color bitmapped images stored in the same directory with the game, or in one of the subdirectories off of that directory. (Digitized sound files are also a popular method of burning up large amounts of disk storage, especially in the case of CD ROM-based games, which typically have about half a gigabyte
C HAPTER FO U R
M d n i pu ldting ßitmdps
of storage available to devote to this and other types of data.) When the game needs to display these graphics, the contents of the files are read from the disk and copied into video memory, often by way of one or more off-screen buffers. What form do 256-color graphics take when stored on a disk? In some cases they are stored in exactly the same way that they are normally represented in video memory, as a sequence of byte values representing the pixels on the display, perhaps with a list of 256 color descriptors (each consisting of 3 bytes of color data, as described in chapter 2) appended to the end of the file so that the image's palette can be reconstructed when it's displayed. This is not, however, a particularly efficient way of storing an image. Quire often, rhe graphics are stored in compressed format, with as much redundant information as possible squeezed out of them. If you think the games on your hard drive are taking up a lot of space now, you should see how much space they'd take up if their graphics weren't compressed! In this chapter we'll be constructing images of a maze out of predrawn bitmaps. We'll need to store those bitmaps on the disk before we can use them. lt's important that we give some thought about how we're going to do this, so let's consider some possible formats in which we could store the files.
G ra p h i cs F i l e F o rmats There are probably an infinite number of different ways in which graphics bitmaps can be stored in a disk file. Fortunately, we don't have to reinvent the wheel. There are a number of Standard file formats available for us to use. Let's look at a few of them and see what sort of strengths and weaknesses they have. The PCX format was developed in the mid- 1 980s for a program called PC Paintbrush, published by ZSoft. (The format is also referred to as PC Paintbrush format or ZSoft format.) This is probably the single most widely supported graphics file format for the PC. Although the data compression algorithm used to squeeze the redundancy out of the files isn't the most efficient, it's very easy (and fast) to decompress. PCX files are identified by a file extension of .PCX, as in IMAGE.PCX. The Graphics Interchange File (GIF) format was developed by the CompuServe Information Service as a means of viewing graphics while logged on to the service via modern. Although files in this format were originally intended primarily for on-line viewing, the GIF (pronounced "jiff") format has also become quite popular as a means of distriburing graphics images from computer to computer. The compression scheme is more effective than the one used in PCX files, but it's also harder (and somewhat slower) to decode. GIF files are identified by an extension of .GIF, as in IMAGE.GIF.
GARDENS OF IMAG I NATI O N
The Targa file format (TGA) is named after a line of graphics add-on boards sold by the Targa company. Because the most popular Targa boards are those that offer 24-bit color (that is, bitmaps that represent each pixel with 3 bytes of data and therefore can contain up to 1 6 million different colors at one time) , most Targa files contain 24-bit images. These images are stored in so-called direct color format, in which each pixel is represented by a 3-byte color descriptor, with 8-bit values for the red, blue, and green intensities of the color. These files contain no palette, since any palette information would be redundant, the color information being stored in the bitmap itself. However, the Targa format also allows for the storage of 8-bit (25 6-color) images with an attached palette. Targa files are often uncompressed. This format is quite popular, in part because of the simplicity of the uncompressed Targa format and in part because it's the most widely known method of storing 24-bit images (though uncompressed 24-bit image files can be q u i t e l ar ge ) . Targa fi l es are i d e n t i fi e d by an exte n s i o n o f . TGA, as i n IMAGE.TGA. A large nurober of other formats are available, such as the Windows BMP (short for bitmap) format. If you'd like a detailed survey of the most common o n e s , you m ight want to take a l o o k at the book Graphics File Formats (Windcrest/McGraw Hill, 1 9 92) by David C. Kay and John R. Levine. No graphics programmer should be without at least one reference to the Standard PC file formats, and this is a particularly useful one. In this chapter we'll use files in two different formats, the uncompressed 8-bit TGA format and the PCX format. I 've chosen the latter because it's supported by a wide variety of graphics utilities and it provides an easily decoded compression scheme. I 've chosen the former because it is simple to implement and happens to work weil with the graphics utilities that I used to create the maze bitmaps that we'll use in the maze program that we develop in this chapter. Let's look at these two formats in turn.
T h e 8 - B it Ta rga F o rmat The 2 56-color, 8-bit Targa file format is actually rather rare, having been eclipsed long ago by the 24-bit version. However, in creating the graphics for the maze program in this book, I used the public domain Persistence of Vision ray tracer (better known simply as POV or POVRAY) and the p ublic domain Pielab graphics conversion utility, both of which work well with Targa files. POVRAY stores the images it creates in the 24-bit Targa format. Pielab is easily used to downscale these 1 6 million color images to 256-color images, and will store the converted images as 8-bit Targa files, which can be viewed on PCs without 24-bit graphic adapters . (lt will also store the converted images as GIF files, but the GIF
C HAPTE:R FOU R
M d n i pu ldting ßitmd ps
compression scheme is more complex to decode.) I decided that it would make my job a great deal easier if I included routines in the bitmapped maze program to support the 8-bit Targa format. Like almost all types of graphics files, Targa files begin with a header containing information about the image or images that the file contains. (Some graphics file formats allow more than one image to be stored in a fil e, though none of the files on the disk that accompanies this book are stored that way.) We'll need some of the information in this header, so the first thing we'll want to do is read it off the disk. The easiest and most elegant way to do this is to declare a data structure with precisely the same fields as the header and read the header from the file using the CIC++ read() function. The definition for the Targa header appears in Listing 4- 1 .
Listing 4-1 The tga_header stru ctu re st ruct
t g a_h e a d e r {
BYTE I D_l eng t h ; BYTE c o l o r_ma p_t ype; BYTE
i ma ge_t y p e ;
WORD f i r s t_co l o r_map_e n t r y ; W O R D c o l o r_ma p_l en g t h ; BYTE c o l o r_ma p_e n t ry_s i z e ; WORD i ma g e_x_o r i g i n ; WORD i ma g e_y_o r i g i n ; WORD i ma g e_w i d t h ; WORD i ma g e_h e i g h t ; BYTE b i t s_p e r_p i xe l ; BYTE i ma g e_d e s c r i p t o r_b i t s ;
II II II II II II II II II II II II
Leng t h o f O=d i r e c t
ID fi e ld c o l o r , e l s e pa l e t t e
Type o f Ta r g a
i ma g e
F i r s t pa l e t t e c o l o r H o w l o n g i s pa l e t t e ? H o w l o n g i s c o l o r de s c r i p t o r ? Uppe r
left
corner X
Uppe r
left
corner Y
W i dt h
i n pixels
Hei ght
in pixels
How many b i t s de s c r i be p i x e l ? D e s c r i p t i on o f i ma g e
};
The data types BYTE and WORD are aliases for unsigned char and unsigned int, which l've defined along with this header in the file TARGA.H in an attempt to make the code less cluttered. This header is followed in turn by an image ID, which is an optional string of characters identif}ring the image - a name, if you will. The length of this string is defined by the ID_length field in the header. Not all Targa files contain an image ID, so this byte will often be equal to 0. Nonetheless, we'll have to check for this when loading a Targa file, so that we can handle the image ID if it's there. The image ID is followed by the color map, which is roughly equivalent to the palette that we discussed in chapter 2. As with the image ID, not all Targa files contain a color map. In fact, since the popular Targa 24-bit format uses a direct color bitmap rather than a palette-mapped structure, the color map is rather rare in
GARDENS OF I MAG I NATION
Targa files. Nonetheless, we'll be using color-mapped Targa files in a couple of the programs that we create in this chapter, so we'll need to deal with the color map. Although the color map can take several forms, we'll deal with 24-bit color maps, idemified by a value of 24 in the color_map_entry_size field of the header. These color maps are similar but not idemical to VGA 256-color palettes, so we'll need to do some converting when we load the color map. More about that in a moment. Following the color map is the bitmapped image itself. Like the color maps, this bitmap can take several different forms. In this chapter we'll deal exclusively with 8 -bit images, identified by a value of 8 in the bits_per_pixel field of the header. The bitmap for an 8-bit uncompressed Targa image is identical to the bitmap used in VGA mode 1 3h: a sequence of bytes representing pixels in which each byte corresponds to one of the 256 color descriptors in the color map. We can read the image data direcdy into an array, where we can store it prior to putting the bitmap (or portians of the bitmap) on the display. That's pretty much all there is to an 8-bit uncompressed Targa file. Now let's look at some code for loading Targa files into memory.
An 8 - B it Ta rga Loa d e r The 8-bit uncompressed Targa format is s o simple that we really only need a single function to load the files into memory. We'll call that function loadTGAO. The letters TGA refer to the file extension used on all types of Targa files. The protorype for this function, which we'll place in the header file TARGA. H , is i nt
L o a dTGA ( c h a r f a r * f i l en a m e , t g a_s t ru c t * t g a )
The parameter filename is a string of characters containing the pathname of the Targa file to be loaded, relative to the current directory. The parameter tga is a pointer to a variable of type tga_struct, which is defined in the TARGA. H header file as struct
t g a_s t r u c t
{
t g a_h ea d e r h ea d e r ; char
far *ID;
BYTE
f a r * i mage;
BYTE
f a r * c o l o r_ma p ;
};
This structure has fields in i t equivalent to all parts o f a Targa file. The header is actually stored wirhin the structure in a field of type tga_header, which we defined back in Listing 4- 1 . The other fields are pointers to the ID string, the image bitmap, and the color map, respectively. We'll need to allocate space for these strucrures from the heap before loading them off the disk. Before we can do this, however, we need to load the header from the disk, because it contains important information about the file. If we were writing a
112
C HAPTE:R FOU R
M d n i puldting ßilmdps
full-service Targa loader that could handle all types of Targa files, we'd use the header information to determine whether to Ioad a direct color or palette mapped image, whether to Ioad a color map from the file, what format the color map and bitmap were to take, and so forth. However, since we'll only be loading 8-bit palette-mapped uncompressed Targa files, we'll be using the header primarily to decide whether the file rhat we have been asked to Ioad is indeed such a file. If it isn't, we'll abort the LoadTGA() function and rerurn an error message to the calling funcrion. To open the Targa file that we've been requested to Ioad, we firsr call the CIC++ open() function: if
( ( i n f i l e =ope n ( f i l e n a me , O_B I N AR Y ) )
== -1 )
return(-1 ) ;
The open() function, called with these parameters, will search for a file with the path name that was passed to rhe LoadTGA() function in the jilename parameter. If found, it will treat this file as a binary file - that is, as a file made up of unsigned byte values that don't represent ASCII characters. If the file isn'r found, the open() function rerurns a value of - 1 . In that event the LoadTGA() function will do the same, aborting the function and rerurning a value of - 1 to the calling function, indicating that an error has occurred. (We'll use the - 1 value to represent all types of errors encountered by this function.) Now that the file is open, we'll use the lseek() function to place the DOS file pointer at rhe beginning of the file (which is where we want to begin reading) and the read() function to read the header from the file into the header field of the tga structure: l s e e k ( i n f i l e , O L , S E EK_S E T ) ; rea d ( i n f i l e , & C t ga -> h e a d e r ) , s i z eof ( t ga_h e a d e r ) ) ;
We pass both functions the file handle rerurned by the open() function, which is contained in the variable injile. The read() function also requires a pointer to the header field of the tga variable and the number of bytes to read into the strucrure, which we've determined by calling the sizeof() function. This standard library function returns the size of a variable structure, which in this instance corresponds to the number of byres of data that we wish to read into it. Once we've read the header data into the header field of the tga variable, we can access fields wirhin the header the way we'd access any other variables wirhin a strucrure. We'll use rhis capability to make sure that the image in the Targa file is of the type that we're expecting. If it isn't, we abort with an error code: if
( t ga -> h e a d e r . i m a g e_type
if
( t ga->heade r . i ma g e_w i d t h > 3 2 0 )
!= 1 )
return(-1 ) ;
if
( t ga->heade r . i ma g e_h e i g h t > 200 )
if
( t ga->heade r . b i t s_p e r_p i xe l
return(-1 ) ;
!= 8)
return(-1 ) ; return(-1 ) ;
GARDE:NS O F IMAG I NATI O N if
( t ga -> h e a d e r . c o l o r_ma p_e n t r y_s i z e
! = 24 )
return(-1 ) ;
T h e s e i n s t r u c t i o n s check t o s e e i f t h e i m age i s o f type 1 (wh i ch i s a n uncompressed color-mapped file); whether the width is greater than 3 2 0 (in which case we can't use it on the mode 1 3h display) ; whether the height is greater than 200 (same prohlem); whether the numher of hits per pixel is something other than 8; and whether the color map contains something other than 24-hit color descriptors. If the file passes all these checks - as the images that we're going to he using in this chapter will - we can begin processing the hody of the file. First we'll load the I D string, in case the calling function is interested in doing something with it: if
( t g a - > h e a de r . I D_ L e n g t h> O )
{
t ga - > I D=new c h a r [ t g a - > h e a de r . I D_ L eng t h J ; r e a d ( i n f i l e , t g a -> I D , t ga -> h e a d e r . I D_L eng t h ) ; }
These instructions first check t o see if the file actually contains an I D string by looking at the ID_length field in the header. If this field is greater than 0, we call the C + + new function, which allocates a chunk of memory equivalent to the value of the ID_length field and returns char * type pointer to the chunk. We then read the ID string into this chunk of memory with the readQ function. Before we can load the color map, we must allocate a 768-hyte section of memory to store it in: if
( ( t g a -> c o l o r_ma p=new u n s i g n e d c h a r [ 3 * 2 56 J ) ==NU L L ) return(-1 ) ;
I f the memory allocation fails, we return to the calling function with an error code. (The expression 3 * 2 5 6 is used instead of 768 to remind us that we're loading 2 5 6 color descriptors that are each 3 hytes long - and because I find it easier to remernher the n umher 2 5 6, which is commonly found in computer programs, than to remernher the numher 768.) Otherwise, we proceed to load the color map, translating it as we do so into a format acceptable to the VGA 2 5 6-color palette-setting function: for
( i nt
for
e n t ry=O;
( i nt
e n t ry=O;
e n t ry++ ) --co l o r )
{ {
r e a d ( i n f i l e , & < tg a->co l o r_ma p [ e n t ry*3+ c o l o r J ) , 1 ) ; t g a -> c o l o r_ma p [ e n t ry*3+c o l o r ] >>= 2 ; } }
Exacdy what sort of translation are we performing here? A VGA color descriptor consists of three values in the range 0 to 63, one each for the red, hlue, and green intensities of the color. Only 6 hits are necessary to store values in this range. So, although we allot an 8-hit hyte for each of these values when we pass them to the VGA 2 5 6 -color palette-setting function, the rwo leftmost bits of those values
CHAPTE:R FO U R
M d n i pu ldting ßitmdps
aren't used. The Targa color descriptors, by contrast, are values in the range 0 to 2 5 5 , so they use all 8 bits of the bytes in the descriptor. To translate them to VGA descriptors, we must shift the binary digits of these values two positions to the right, using the CIC++ >> operator. (Actually, we use the > > = operator, which shifts the values and reassigns them to the variables in which they were already contained.) In addition, the Targa color descriptor values are in the order red, green, blue, while the VGA color descriptors should be in the order blue, green, red. Therefore we have to reverse the order of the descriptors as we load them into the array. That's why the Jor() loop that loads the three color descriptor values counts the index variable color backward from 2 to 0, storing the values in the reverse order from that in which they are read from the disk. Since the image data is already stored in exactly the format we need for a mode 1 3h bitmap, loading the image is a simple matter of calculating the number of bytes it contains (by multiplying the image_width field of the header by the image_height field) , allocating memory for the bitmap array, and reading the bitmap data from the disk into the array: unsi gned i n t
i ma ge_s i z e = t g a -> h e a d e r . i m a g e_w i d t h
* t g a->heade r . i ma g e_h e i g h t ; if
C C t g a -> i ma g e=new uns i g n e d c h a r [ i ma g e_s i z e J ) == N U L L ) return ( -1 ) ;
rea d C i n f i l e , t g a -> i ma g e , i ma g e_s i z e ) ;
Our job is now complete, so we close the Targa file and return to the calling function with a return value of 0, indicating that no errors occurred: c l os e C i n f i L e ) ; return ( Q ) ;
T h e l oa dTGAO F u n ction The complete text o f the loadTGA() function appears i n Listing 4-2.
Listing 4-2 The loadTGAO f u n ction sta t i c int
i nt i nf i le;
I I H a nd l e f o r Ta r g a
L oadTGA C c h a r f a r * f i l ename , t ga_s t r u c t * t ga )
I I Loads an 8-bi t un comp r e s s e d Ta r g a II
f i le
f i l e i n to a
s t r u c t u r e of
t ype
t g a_s t r u c t
{ II
Open f i l e , a bo r t
if
C C i n f i l e=open C f i l e n a m e , O_B I NARY ) ) ==-1 )
i f not
found : r e t u r n ( -1 ) ; continued on next page
G A R D E N S O F I MAG I NATION continuedftom previous page
II
Set
f i l e po i n t e r a t
s t a r t of
fi le:
l s e e k ( i n f i l e , O L , S E E K_S E T ) ; II
R e a d h e ad e r
i nto header f i e l d :
r e a d ( i n f i l e , & ( t g a -> h e a de r ) , s i z e o f ( t g a_h ead e r ) ) ; II
Is
if
( t ga -> h e a de r . i ma g e_type
t h i s a n un comp r e s s e d c o l o r-ma pped i ma g e ? I f not , a b o r t :
II
I s t h e i ma g e w i de r t h a n 320 p i x e l s ?
if
( t g a -> h e a de r . i ma g e_w i d t h> 3 2 0 ) the
i ma g e t a l l e r
!= 1)
r e t u r n ( -1 ) ;
II
Is
if
( t g a - > h ea d e r . i ma g e_h e i g h t > 200 ) i t a n 8-b i t
t h a n 200 p i x e l s ? I f s o , a bo r t :
II
Is
if
( t g a -> h e a de r . b i t s_p e r_p i x e l
II
Does
if
( t g a - > hea d e r . c o l o r_ma p_e n t ry_s i z e
II
If
if
( t g a -> h e a d e r . I D_L e n g t h > O )
i t h a ve a
t h e re ' s a n
I f s o , a bo r t :
re t u r n ( -1 ) ;
i ma g e ? I f
return(-1 ) ;
n o t , a bo r t : ! = 8)
return(-1 ) ;
24-bi t pa l e t t e ? I f n o t , a b o r t :
ID stri ng,
Load
! = 24)
return ( -1 ) ;
it:
{
t g a -> I D=new c h a r [ t g a -> h e a de r . I D_ L en g t h J ; r e a d ( i n f i l e , t g a -> I D , t g a - > h e a de r . I D_ L en g t h ) ; } e l se
t g a-> I D = O
II
A l l o c a t e memo ry f o r pa l e t t e , a b o r t
if
( ( t g a ->co l o r_ma p=new u n s i gned c h a r [ 3* 2 56 J ) == N U L L )
i f not a va i l a b l e :
if
( t g a -> I D )
{
de l e t e t g a -> I D ;
r e t u rn ( - 1 ) ; } II
Load p a l e t t e ,
for
( i nt
for
e n t ry=O;
t r a n s l a t e f o r VGA : e n t ry=O; --co l o r )
{ {
r e a d ( i n f i l e , & < t g a->co l o r_ma p [ e n t ry*3+co l o r J ) , 1 ) ; t ga ->co l o r_ma p [ e n t ry*3+ c o l o r J >>= 2 ; } } II
C a l c u l a t e s i z e of
unsi gned
i nt
i ma g e i n byt e s :
i ma g e_s i z e = t g a - > h e a de r . i m a g e_w i d t h
* t g a -> h e a d e r . i ma g e_h e i g h t ; I I A l l o c a t e memo ry f o r i ma g e , a b o r t de l e t e
if
not a v a i l a b l e :
t g a ->c o l o r_ma p ;
if
( t ga -> I D )
if
( ( t g a -> i ma g e = n e w u n s i g n e d c h a r [ i ma g e_s i z e ] ) == N U L L )
d e l e t e t g a -> I D ;
r e t u r n ( -1 ) ; } II
L o a d i ma g e f rom f i l e :
r e a d ( i n f i l e , t g a -> i ma g e , i ma g e_s i z e ) ;
{
CHAPTER FO U R II
M d n i p u ldling ßilmdps
C l ose f i l e :
c l o se ( i n f i L e ) ; I I R e t u r n w i t ho u t e r ro r : return ( Q ) ; }
An 8 - B it Ta rga D i s p l ay P ro g ra m To demonstrate these functions (and, not incidentally, create a utility that will be useful throughout the remainder of this chapter) , let's write a program called TGASHOW that will read 8-bit uncompressed Targa files from the disk and display them in mode 1 3h. Such a program will need to include the TARGA. H header file a t the begin ning o f any modules that reference the Targa loader functions, and should incorporate TARGA. CPP as part of the project file or makefile used to construct the final code. We'll keep our program simple by purring all the unique code not already incorporated elsewhere in one of our support modules in a single main() function. This function will begin with the usual main() header, but will reference the standard CIC++ argument parameters so that the user will be able to pass the function the name of a Targa file from the command line: vo i d ma i n ( i n t a r g c , c h a r *a rgv[ J )
In case yo u're not familiar with the argument param e ters, the in teger parameter arge will automatically be set equal to the number of arguments (that is, strings of characters separated by blank spaces) that the user types on the command line when the program is invoked, including the name of the program itself. Thus arge will always be equal to at least 1 . We want the user to be able to invoke the program with the name of a Targa file as an argument, like this: t g a s how i ma g e . t g a
So arge should be equal to 2 when the main function begins, since there should be two arguments on the command line (counting the name of the program) . We'll check fo r this at the beginning of the main () fu nction and abort the program with an error code if the wrong number of arguments is found: if
( a rg c ! =2 )
{
II
Are t h e r e 2 a r gumen t s on c ommand
L i ne?
put s ( " W rong numbe r of a r gumen t s . \ n " ) ; ex i t ( - 1 ) ; }
The other argument parameter, *argv[}, is an array of pointers to the actual character strings that make up the arguments typed on the command line. The first element of the array, argv[O}, always points to the name of the program itself. If you ever write a program that needs to know what name it was invoked under,
GARDENS OF I MAG I NAT I O N
this is the best way to obtain that information. For our Targa-viewing program, we'll use the second element of this array, which will be pointing to the name of the Targa file that the user wants to view. We can pass this array element to the loadTGA Q function like rhis: II
Load T a rga
if
( l o a d T G A ( a r gv [ 1 J , & t g a ) )
fi le.
A bo r t
if
not
f ou n d :
exi t ( 1 ) ;
You'll recall that the loadTGAQ function returns a value of 0 if it's successful in loading the file, - 1 if it isn't. Here we simply look for a non-zero value (allowing us to expand our repertoire of error codes at a later date if we wish) and abort to DOS if one is found. The exitQ function returns an error code of 1 to DOS, which can be detected by a batch file if one is used to invoke the TGAS HOW program. The parameter tga to which we pass the loadTGAQ function a pointer is a variable of type tga_struct, defined earlier in the program like this: t g a_s t r u c t
tga;
Now that the Targa file has presumably been loaded, we can set mode 1 3h, clear the screen, and establish a pointer to video memory exacdy as we've been doing in earlier programs (so we won't bother to detail the procedure here) . The palette can be set to the color map from the Targa file using our set_paletteO function, like rhis: II
S e t pa l e t t e
to T a rga
c o l o r ma p :
s e t pa l e t t e ( t ga . c o l o r_ma p ) ;
The image itself can be copied direcdy from the tga. image field into video memory, with the aid of our usual screen pointer. However, we must take into account the possibility that the Targa image will be smaller than the 320 by 200 mode 1 3h screen (as all of the Targa images we use in this chapter will be) . The information about the size of the image is contained in the image_width and image_height fields of the header structure, which contain the width and height of the image in pixels. We can use these as the limiting values of a pair of nested forO loops, which will work their way across the image in the x and y dimensions, copying pixels as they go: II
Copy
for
i ma g e to mode
1 3h d i s p l a y :
( i n t y = O ; y < t g a . h e a d e r . i ma g e_h e i g h t ;
for
( i n t x=D;
x < t g a . h e a de r . i ma g e_w i d t h ;
y++ ) x++ )
{ {
s c r e e n [ y*320+x J = t g a . i ma g e [ y * t g a . h e a d e r . i ma g e_w i d t h + x J ; } }
The position at which to fetch each pixel from the Targa image array is calculated by multiplying the y position of the pixel by the width of the image and adding in the x position. The position at which to place the pixel on the mode 1 3h display is similarly calculated by multiplying the y position of the pixel by the
CHAPTER FO U R
M d n i pu ldting ßitmdps
width of the screen (320) and adding in the x position. This will cause images smaller than 320 by 200 to appear in the upper-left corner of the display. Savvy programmers will note that this is a rather slow method of copying data - it wouldn't be difficult to come up with a fastet technique - but time isn't exactly of the essence in this program .
The TGAS H OW P ro g ra m The complete text of TGASHOWCPP appears in Listing 4-3 .
II TGASHOW . C P P Ve r s i on 1 . 0 D i s p l a y a 256- c o l o r Ta r g a
II
i mage on t h e mode
1 3 h s c reen
II II W r i t t en by C h r i s t op h e r Lampton I I f o r Ga rdens of
I ma g i n a t i on ( Wa i t e G roup P r e s s )
# i n c l ude
< s t d i o . h>
# i n c lude
# i n c l ude
# i n c l ude
< s t d l i b . h>
# i n c l ude
" s c r een . h "
# i n c l ude
" t a r ga . h "
t g a_s t r u c t t g a ; vo i d ma i n ( i n t a r g c , c h a r * a rgv[ J ) { if
( a r g c ! =2 )
{
II
A r e t h e r e 2 a rgume n t s on comma nd
L i ne ?
puts ( " W r ong numbe r o f a r gumen t s . \ n " ) ; e x i t ( -1 ) ; } II
Load Targa
if
( L oadTGA ( a rgv [ 1 J , & t g a ) )
fi le.
Abo r t
if
not
fo und :
ex i t ( 1 ) ;
I I C r e a t e po i n t e r t o v i de o memo r y : c h a r f a r * s c r een= ( c h a r f a r * ) MK_F P ( O x a OOO , O > ; I I S a v e previ ous v i deo mode : i n t o l dmode=* ( i nt
* ) MK_F P ( O x40, 0x49 ) ;
I I Put di s p l a y i n mode 1 3 h : s e t mode ( O x 1 3 ) ; I I C l ea r d i s p l a y : c l s ( s c reen ) ;
continued on next page
GARDENS OF I MAGI NATION continuedfrom previous page
I I S e t p a l e t t e to Ta rga c o l o r ma p : s e t p a l e t t e ( t g a . c o l o r_ma p ) ; I I C opy i ma g e t o mod e 1 3 h d i s p l a y : f o r ( i n t y=O ; y< t g a . h e a d e r . i ma g e_h e i g h t ; y++ ) { f o r ( i n t x = O ; x< t g a . h e a d e r . i ma g e_w i d t h ; x++) { s c r e e n [ y*320+x J = t g a . i ma g e [ y * t g a . h ead e r . i ma g e_w i d t h +x J ; } } I I P a u s e u n t i l u s e r p r e s s e s a key : w h i l e ( ! k bhi t ( ) ) ; I I R e l e a s e memo r y d e l e t e t g a . i ma g e ; d e l e t e t g a . c o l o r_ma p ; i f ( t ga . I D ) d e l e t e t g a . I D ; I I R e s t o r e o l d v i deo mode : s e tmode ( o l dmode ) ; }
There's a copy of this program on the disk that accompanies this book. To give it a test run, CD to the subdirectory called BITMAP in the directory where you placed the files from this book. Then type the following on the command line: t g a s h ow t e s t ma z e . t g a
A 1 60 b y 1 00 image will appear in the upper-left corner o f the mode 1 3h display. The image, which you can see reproduced here in Figure 4- 1 , is an example of the maze graphics that we'll be developing in this chapter. More about this later.
Figure 4·1
The TESTMAZ E.TGA image , as
d i splayed by the TGASHOW program This is a sam ple of the maze g ra p h ics that we ' l l be developi n g later in this cha pter
CHAPTER FOUR
Mdnipu ldting ßitmdps
The PCX F i l e Fo rmat Before we talk about using predefined bitmaps i n animared maze games, we need to discuss one more file format. Alrhough we'll be using the Targa file format for easy compatibility with POVRAY and PicLab, we're going to use the PCX format in all cases where a Targa file isn't strictly necessary. One reason for this is that the PCX format includes data compression, allowing us to store the bitmap data in a smaller amount of space. With state-of-the-art computer games now taking up as much as 35 megabyres of hard drive real estate, the users of our games will thank us for conserving as much of their disk space as we can. (At least, they'll be a little less angry at us if we don't wipe out those last few bytes that they'd intended to devote to spreadsheet data.) Another reason fo r using PCX files is that many paint programs, such as Deluxe Paint, will create files in this format. The PCX format, like the Targa format, begins with a file header describing the image stored in the file. Listing 4-4 shows the declaration for the header format structure, which we'll place in the file PCX. H.
Listing 4-4 The PC X_ H EAD E R stru ctu re s t r u c t p c x_h e a d e r { c h a r man u f a c t u r e r ;
I I A l wa y s s e t
c h a r ve r s i on;
I I A l wa y s
to 0
5 f o r 25 6-co l o r f i l e s
c h a r e n c od i ng ;
I I A l wa y s s e t
c h a r b i t s_p e r_p i x e l ;
I I S h o u l d b e 8 f o r 2 5 6-c o l o r
i nt
xm i n , ym i n ;
to 1
Coo r d i n a t e s f o r t o p l e f t
Coo r d i n a t e s f o r bo t t arn r i g h t
i nt
xma x , yma x ;
II
i nt
h res;
I I H o r i z on t a l
i nt
vres;
I I Ve r t i c a l
r e s o l u t i on o f
I I E G A pa l e t t e ; not u s e d f o r
c h a r r e s e rved;
II
R e s e rved f o r f u t u r e u s e Color p l anes
II char
c o l o r_p l a n e s ;
II
byt e s_p e r_l i ne;
I I Number o f byt e s
i nt
pa l e t t e_type;
II
corner
i ma g e
25 6-co l o r f i l e s
i nt
II
corner
r e s o l u t i on o f i mage
c h a r pa l e t t e 1 6 [ 4 8 J ;
char fi l ler[58J;
fi les
II
in 1
l i ne of
p i xe l s S h o u l d b e 2 f o r c o l o r pa l e t t e
I I N o t h i ng b u t
j unk
} ,.
Several of these fields correspond to the ones that we found of interest in the Targa header, though it isn't always obvious which ones. Oddly, there are no fields that correspond precisely to the image_width and image_height fields in the Ta rga header. Fortunately, this i n formation can be calculated from the information in rhe header like rhis:
G A R D E N S OF I MAG I NATION i n t i ma g e_w i d t h p c x . h e a d e r . xmax - pc x . h e a d e r . x mi n +1 ; i n t i ma g e_h e i g h t = pcx . h e a d e r . ymax - p c x . h e a d e r . ym i n +1 ; =
where pcx is a variable of type pcx_struct. The filler field at the bottom of the structure contains no valid data at all , having apparendy been included to bulk the structure out to an even 1 28 bytes. It's conceivable that this portion of the header will be used in furure versions of the PCX standard, but for now it can be safely ignored. The definition for our PCX structure, which is also included in PCX. H, rs shown in Listing 4-5 .
Listing 4 - 5 Th e PCX_ST R U CT structu re s t r u c t p c x_s t r u c t { p c x_h e a d e r h e a d e r ;
II II
u n s i gned cha r f a r * i m a g e ;
II II
u n s i gned c h a r pa l e t t e [ 3*2 5 6 J ; I I }.
11
S t r u c t u r e f o r h o l d i n g t h e PCX header Poi n t e r to a b u f f e r ho l d i ng t h e 64, 000-byte bi tmap A r ra y ho l d i n g the 768by t e pa l e t t e
,
Aside from the PCX header, the PCX structure contains pointers t o a char array containing the PCX image bitmap and a second char array containing the color descriptors in the palette. These two portians of the PCX file - the image and the palette - follow immediately after the header.
R u n - Le n gth E n c o d i n g The PCX image is not stored i n the PCX file as a straightforward bitmap, the way the uncompressed Targa image was. Rather, it is stored in compressed format. The type of compression used in PCX files is known as run-length encoding, or RLE for short. Run-length encoding is not a specific method of bitmap compression, bur a generic term for a whole battery of data compression techniques. What all of these techniques have in common is that they store bytes in terms of runs. A run is a sequence of bytes that all have the same value. This occurs commonly in bitmaps, where several pixels of the same color often appear in a row. In an
CHAPTER FO U R
Mdnipu ldting ßitmdps
uncompressed 25 6-color bitmap, these rows of identically colared pixels are stored as runs of bytes representing the pixels. If there are 30 pixels in a row, all in palette color 1 1 7, then the bitmap will contain 30 bytes in a row with a value of 1 1 7. This is - quite literally - redundant. If you've seen one pixel with a value of 1 1 7, then you've seen them all - and we don't need to store them all. Instead of storing 30 sequential bytes all with a value of 1 1 7, we can store the number 30 followed by the number 1 1 7. Such a pair of numbers is called a run-length pair; the first number in such a pair always gives the number of bytes in the run and the second number gives the numerical value of those bytes. Storing a run of 30 bytes in this manner would save us 28 bytes of storage, which is weil worth the effort if disk space is at a premium (which it usually is) . When there are several bytes in a row with different values, this sort of run length encoding can be quite inefficient. Each byte would need to be stored as a run 1 byte lang - that is, as a run-length pair in which the first byte is a 1 and the second byte is the original value - which would double the Storage required fo r those bytes. The ideal run-length scheme would include a way of turning RLE on when it's needed and off when it isn't. The PCX RLE scheme does this by letting byte values in the range 1 9 2 though 255 represent run-length values in the range 0 to 63. When a byte in this range is detected, the run-length decoder - the program that decompresses the RLE-encoded data - subtracts 1 92 from it and repeats the following byte value that many times. For instance, if the number 207 is encountered followed by the number 1 6, the decoder translates this into a run of 1 5 (207 minus 1 92) bytes with a value of 1 6, like this: 1 6, 1 6, 1 6 , 1 6, 1 6 , 1 6 , 1 6 , 1 6 , 1 6 , 1 6, 1 6, 1 6 , 1 6 , 1 6 , 1 6
How does the PCX RLE scheme encode individual values i n the range 1 92 to 255 so that they won't be mistaken for run-length values? Alas, it must encode them as runs of a single byte in length - that is, as the number 1 93 followed by the value to be encoded. For instance, an individual byte with a value of 239 would be encoded as the number 1 93 followed by the number 239. This is a ßaw in the PCX scheme that prevents it from achieving the maximum possible run length compression - all those extra run-length values make the data !arger than it has to be - but doesn't interfere in any way with the decoding of the data. In fact, it makes decoding simpler because we can decode these single-byte runs the same way that all other runs are encoded.
GARDENS O F IMAGI NATION
T h e l oa d PCXO F u nct i o n To decompress an RLE-encoded PCX bitmap, we're going to write a function called loadPCX(). Because the RLE-encoding makes the PCX file format a bit more complicated than the uncompressed Targa format, we'll give this function a pair of subsidiary functions that it can call to load the image and the palette. We'll call these subsidiary functions load_image() and load_palette{), respectively. The prototype for the loadPCX() function looks like this: i nt
L oa d P C X C c h a r f a r * f i l e n a me , pc x_s t r u c t * p c x )
The parameters are essentially the same as the ones passed to the loadTGA () function that we described earlier in this chapter. The jilename parameter is a pointer to an array of type char containing the pathname of the PCX file to be viewed, and the pcx parameter is a pointer to a structure of type pcx_struct into which will be placed the file data. We then proceed to open the file and read the header data into the pcx structure, exactly as we did in the loadTGA() function: if
( ( i n f i l e=open ( f i l e n a m e , O_B I NA R Y ) ) ==-1 )
return(-1 ) ;
L s e e k C i n f i l e , O L , S E EK_S ET ) ; r e a d ( i n f i l e , & C p c x -> h e a d e r ) , s i z e o f ( p c x_h e a de r ) ) ;
Now that we have the header data in memo ry, we check to see if the file contains a 320 by 200 or smaller image. I f it doesn't, we return to the calling function with an error code: if
C p c x - > h e a de r . xm a x - p c x - > h e a d e r . xmi n + 1
> 320 )
return(-1 ) ;
if
C p c x - > h e a d e r . ym a x - p c x - > h e a d e r . ymi n+1
> 200 )
r e t u r n ( -1 ) ;
We also check to make sure that the PCX file is i n Version 5 format, since this is the only type of PCX file that can store a 25 6-color image: if
( p c x- > h e a d e r . ve r s i on
!= 5)
return(-1 ) ;
I f all checks out, we call the load_image() and load_palette() functions, close the file, and rerurn to the calling function without an error code: if
C l o a d_i m a g e ( i n f i l e , p c x ) )
return(-1 ) ;
L o a d_pa l e t t e ( i n f i l e , p c x ) ; c l o se ( i nf i l e ) ; ret u rn ( O ) ;
Note that we abort with an error code i f the load_image() function fails, but don't bother to check fo r an error code from load_palette (). That's because the load_palette{) function doesn't return error codes, as you'll see in a moment. The complete text of the loadPCX() function appears in Listing 4-6.
CHAPTER FO U R
M d n i p u ldting ßitmdps
s ta t i c i n t i nf i l e ; i n t L oa d P C X ( c h a r f a r * f i l e name , p c x_s t r u c t * p c x ) { i f ( ( i n f i l e=open ( f i l en a m e , O_B I NARY ) ) ==-1 ) r e t u rn ( - 1 ) ; l s ee k ( i n f i l e , O L , S E E K_S ET ) ; rea d ( i n f i l e , & C p c x->hea d e r ) , s i z eof ( p c x_hea d e r ) ) ; i f ( p c x->heade r . xma x - p c x->hea d e r . xm i n+1 > 320) r e t u r n ( - 1 ) ; i f ( p cx ->head e r . yma x - p c x ->heade r . ym i n+1 > 200) r e t u r n ( - 1 ) ; i f ( p c x-> heade r . ve r s i on ! = 5 ) r e t u r n ( - 1 ) ; i f ( L oad_i mage ( i n f i l e , p c x ) ) r e t u r n ( - 1 ) ; L oad_pa l e t t e < i n f i l e , p c x ) ; c lose( i nfi L e ) ; re t u r n ( O ) ; }
T h e l oa d_i m a g e o F u n ction The load_imageQ function is rather complex, since it must decode the run-length encoding scheme used to encode PCX b itmaps. Basi cal ly, it operates i n rwo modes, identified by the current value o f the integer variable mode. This variable can take either of two values, defined at the head of the function as RUN_MODE and BYTE_MODE. When in BYTE_M ODE, the load_imageQ function reads the next byte from the PCX file and checks to see if it's in the range 1 92 to 255. If it isn't in that range, it is pixel data and the function simply outputs the value of the byte to the buffer that holds the decoded bitmap. If it is in that range, the function subtracts 1 92 from the value, sets the integer variable bytecount equal to the result, fetches the next value from the file and stores it in the integer variable outbyte, and sets the mode variable to RUN_MODE. While in RUN_MODE, the function repeatedly outputs the value of outbyte to the decoding buffer until bytecount has been decremented to 0, thus decompressing the run of bytes. Here's the prototype for the load_imageQ function: i n t L oad_i ma g e ( i n t p c x f i l e , p c x_s t r u c t * p c x ) ;
The first parameter, pcxfile, is the handle of the PCX file as returned by the open() function. The second parameter, pcx, is the same pointer to the pcx_struct variable that was passed to loadPCXQ by the calling function. The complete text of the load_imageO function appears in Listing 4-7.
GARDENS O F I MAG I NATION
Listi ng 4-7 The load_i mageO fu n cti o n i nt II
L o a d_i ma ge ( i n t p c x f i l e , p cx_s t r u c t * p c x ) D e comp r e s s b i tma p a n d s t o r e i n b u f f e r
{ II
S ymbo l i c con s t a n t s f o r e n cod i n g mod e s , w i t h B Y T E M ODE r e p r e s e n t i n g uncom p r e s s ed byte mode II a n d RUNMO DE r e p r e s e n t i n g run- L e n g t h enco d i n g mode : c o n s t i n t BYTEMOD E=O, RUNMO D E = 1 ; II
I I B u f f e r for da t a r e a d f rom d i s k : c o n s t i n t BU F L E N= 5 * 1 02 4 ; I I C u r r e n t e n c od i n g mode be i ng used, i n t mode=BYT EMOD E ; II i n i t i a l l y s e t t o BYTEMODE I I Number o f c h a r a c t e r s read f rom f i l e i n t r e a d l en ; s t a t i c u n s i g n e d c h a r o u t by t e ; I I N e x t b y t e f o r b u f f e r s t a t i c u n s i g n e d c h a r b y t e c ou n t ; I I C o u n t e r f o r r u n s s t a t i c u n s i g n e d c h a r bu f f e r [ B U F L E N J ; I I D i s k read buf f e r II
C a l c u l a t e s i z e o f i ma g e : i n t i ma g e_w i d t h=pc x->heade r . xmax - p c x ->head e r . xmi n + 1 ; i n t i ma g e_h e i g h t =p c x->hea d e r . ym a x - p c x->heade r . ym i n + 1 ; L o n g i m a g e_s i z e = ( L o n g ) i ma g e_w i d t h * i ma ge_h e i g h t ; A l l o c a t e memo ry f o r b i tmap bu f f e r a n d r e t u r n - 1 i f a n e r ror o c c u r s : i f ( ( p c x-> i mage=new u n s i g n e d c h a r [ i m a g e_s i z e J ) ==NU L L ) return(-1 ) ; i n t b u f p t r=O; I I Po i n t t o s t a r t o f buf f e r I I Z e ro c h a r a c t e r s r e a d s o f a r r e a d l en=O; II
II
I I C r e a t e poi n t e r t o s t a r t o f i ma g e : u n s i g n e d c h a r * i mage_pt r=pcx-> i ma g e ; II
L o o p f o r ent i r e L e n g t h o f b i t m a p buf f e r : f o r < L ong i =O ; i < i ma g e_s i z e ; i ++ ) { I I I f we ' r e i n i nd i v i d ua l by t e i f ( mo d e==BYT E MO D E ) { II mode . . . . i f ( b u f p t r>=re a d l e n ) { I I I f p a s t end of bu f f e r . . . I I Po i n t t o s t a r t b u f p t r=O; II
Rea d mo r e b y t e s f rom f i l e i n t o b u f f e r ; i f no more b y t e s L e f t , b r e a k o u t of L oop i f ( ( r e a d l e n = r e a d ( p c x f i l e , b u f f e r , BU F L E N ) ) == O ) b r ea k ; II
} o u t b y te=bu f f e r [ b u f p t r++ J ; I I N e x t b y t e of b i tmap I I lf run- L e n g t h f l ag . . . i f ( o u t b y t e>O x b f ) {
26
CHAPTER FOUR II
C a l c u l a t e number of byt e s i n
M d n i p u ldting ßitmdps
run :
by t e c o u n t = C i n t ) C C i n t ) o u t b y t e & O x 3 f ) ; if
C bu f p t r> = r e a d l e n )
{
I I I f p a s t end o f b u f f e r II
bu f p t r=O;
Po i n t t o s t a r t
I I R e a d m o r e byt es f rom f i l e i n t o bu f f e r ; II
i f n o more b y t e s
if
L e f t , brea k out o f
L oop
C C r ea d l en = r e a d C p c x f i l e , bu f f e r , BU F L E N ) ) == O ) break;
} o u t byte=b u f f e r [ b u f p t r++ J ; II
Swi t c h t o
if
C --by t e c o u n t > Q )
I I N e x t byte o f
b i tmap
run- L en g t h mode : mode = R U N M O D E ;
} } II Swi t c h e l se
if
t o i n d i v i d ua l
by t e mod e :
C --byt e c o u n t = = Q ) mode=BYTEMO D E ;
I I Add n e x t by t e t o b i t ma p bu f f e r : *i mage_pt r++= o u t by t e ; } return
CO);
}
The loa d_pa l ette o F u nction The load_paletteO function i s much simpler. I n fact, it's pretty much identical to the code we used for loading the color map frorn a Targa file, though we've put it in a separate function for consistency wich the load_imageO function. The text of the load_paletteO function is in Listing 4-8.
Listing 4-8 The load_pa letteo f u n ction v o i d L o a d_pa l e t t e C i n t p c x f i l e , p c x_s t r u c t
*p c x )
{ L s e e k C p c x f i l e , -768 L , S E EK_EN D ) ; re a d C p c x f i l e , p c x ->pa l e t t e , 3*2 56 ) ; for
C i nt
for
i =O ;
C i nt
i pa l e t t e [ i * 3 + j ]>>2; }
There are two major differences between the palette loading code for the PCX files and that for the Targa files. Before we can load the PCX palette, we must move the DOS file pointer to a position 768 bytes from the end of the file, since the PCX palette is n't guaranteed to fol low imm ediately after the bitmap.
GARDENS OF I MAG I NATION
(Generally, there is a single 0 byte between the bitmap and the palette, and we could probably get away wich simply skipping this byte. However, to play it safe, we position the file pointer relative to the end of the file, a technique that should work with all 256-color PCX files.) The other difference is that the 3 bytes of the color descriptors are in the same blue, green, red order required by the VGA 256color palette setring function, so we don't need ro step through ehern backwards as we did when loading the Targa color map.
T h e PCXS H OW P ro g ra m
As wich our Targa-loading function, we'll demonstrate the PCX loader by writing a short program to display 256-color PCX images on ehe mode 1 3h display. This program, like the Targa display program, will only work wich 256-color images of 320 by 200 or lower resolution. The text of the program PCXSHOW.CPP appears in Listing 4-9. Because it's essentially identical ro the TGAS H OW program described earlier, no detailed explanations of its inner workings are needed.
Listing 4-9 T h e PCXS H OW program II
PC X S HOW . C P P Ve r s i o n 1 . 1 D i s p l a y a P C X i m a g e on t h e mode 1 3 h s c reen
II II II
W r i t t e n by C h r i s t o p h e r Lampton for G A R D E N S O F I M AG I N A T I O N ( W a i t e G r oup P r e s s )
II
# i n c l ud e # i n c l ud e # i n c l ude # i n c l ud e # i n c l ud e # i n c l ud e
< s t d i o . h>
< s td l i b . h> " s c reen . h " "pcx . h "
p c x_s t r u c t p c x ; vo i d m a i n ( i n t a r g c , c h a r * a r g v [ J ) {
i f ( a r g c ! = 2 ) { I I A r e t h e r e 2 a r gum ent s on command L i ne? pu t s ( " W r ong num b e r of a r gumen t s . \ n " ) ; ex i t ( - 1 ) ; } II
C r ea t e poi n t e r t o v i deo memo ry : c h a r f a r * s c r e en= ( c h a r f a r * ) MK_F P ( OxaOOO,O > ;
C HAPTER FO U R II
Mdnipuldting ßitmd ps
Save p r e v i ous v i deo mode :
i n t o l dmode=* ( i n t * ) MK_F P < Ox40 , 0 x 49 ) ; I I Put d i s p l a y i n mode 1 3 h : se tmode ( Ox 1 3 ) ; I I C l ea r d i s p l a y : c l s ( s c r ee n ) ; if
( L o a d P C X ( a r g v [ 1 J , &p c x ) )
{
s e t mod e ( o l dmode ) ; exi t ( 1 ) ; } i n t i ma g e_w i d t h =p c x . h e a de r . xma x - p c x . h e a d e r . xm i n + 1 ; i n t i ma g e_he i g h t = p c x . h e a d e r . yma x - p c x . h e a d e r . ym i n + 1 ; L o n g i ma g e_s i z e = ( L o n g ) i ma g e_w i d t h * i ma g e_h e i g h t ; s e t p a l e t t e ( p c x . pa l e t t e ) ; for
( i n t y=O;
for
y< i ma g e_h e i g h t ;
( i n t x=O;
x < i m a g e_w i d t h ;
y++ ) x++)
{ {
s c r een [ y*320+ x J =p c x . i ma g e [ y * i m a g e_w i d t h+x J ; } } whi le
( ! kbh i t ( ) ) ;
I I Re l ea s e memo ry de l e t e pc x . i ma g e ; I I R e s t o r e o l d v i deo mode : s e t mod e ( o l dmod e ) ; }
This program is available o n the included disk as PCXSHOWEXE.
B u i l d i n g a B itma p ped M a z e Now that we have the ability to store predesigned bitmaps on the disk and load them into the computer's memory, we can use this capability to construct fully bitmapped three-dimensional images of the interior of a maze on the mode 1 3h display. This is not a trivial task, however. Before we can accomplish it, we'll need to solve several technical problems that we ignored in creating the wireframe maze code in the previous two chapters. One problern we rather o b v i o u s ly igno red was real ism - o r, m o re importantly, the lack thereof To give you an idea of the sort of realism we're shooting for in our bitmapped maze engine, look at the screen shot in Figure 4-2, taken from a recent entry in the Wizardry series of CRPGs. This image is far more realistic than the wireframe graphics in the first five Wizardry games, which
GARDENS O F I MAG I NATION
we emulated with our wireframe maze program. Or go back and Iook at Figure 4- 1 , the one we used to demonstrate the TGASHOW program, which shows you some of the bitmaps that we'll be using. Even bettet, go back into the BITMAP directory and use TGASHOW - or any other graphics display utility you own that can display 8-bit Targa files - to take another Iook at TESTMAZE.TGA. Quite a difference from the graphics in the last chapter, right? Instead of black lines on a white background, we now see fully detailed walls, a reflective tile floor, and a blue sky with puff}r white clouds floating in it. In fact, this image is so detailed that you might wonder how we're going to cram all of those pixels into place fast enough to allow the user to walk araund "inside" the maze in real time. The solurion is to draw all of the pieces of the image in advance, then mix and match those pieces while the program is running to construct a pietute of the way the maze appears from whatever location and direction the viewer is looking at it from. And to do that, we'll turn to the Persistence ofVision ray tracer for help.
Ray Tra ci n g t h e M a z e I've chosen to use the POV ray tracer to generate the maze images in this chapter for rwo reasons. The first is that it's a very good ray tracer. The second is that it's free. If you're not already familiar with POV, that second Statement may catch you off guard. Free? Who gives away perfectly decent pieces of software - especially software as complex and useful as a ray tracer - for free? In this case, the
Figure 4-2 A scene from Crusaders of the Dark Savant. the seventh entry i n the Wiza rdry series. The bitmapped maze g raph ics are a considerable improvement over the wi refra me g raphics i n the first five games of the series
C HAPTER FOU R
M d n i p u ldting ßitm d ps
samaritans who are giving away the software are a group of programmers who make their home on the CompuServe Information Service and who write graphics-related code for the sheer fun of it, not to mention the joy of providing useful, free software to people who have a need for it (or who j ust want to play around with it). Because it's available for free, you might want to get a copy of your own and use it to generate additional graphics for the maze program in this chapter (or for maze programs that you've written yourself) . Take a Iook in Appendix A of this book for more detailed instructions on how to get your hands on POVRAY and how to use it to generate maze graphics for the program that we'll be creating in this chapter. In case you're not already familiar with the concept, a ray tracer is a program that takes a mathematical description of a scene - a description couched in terms of certain standard shapes and the mathematical relationships between them - and creates a picture of that scene as it would appear if it existed in the real world. (Actually, the images created by a ray tracer often seem more realistic than any you might see in the real world, filled with impossibly shiny surfaces and vivid textures. This makes ray tracing an excellent method of generaring hyper-realistic computer game images. ) lt does this by tracing the paths of imaginary light rays as they bounce around the scene, eventually encountering the viewer's eyes. We'll talk more about the mechanics of ray tracing in chapter 8 . For now, we'll j ust use POVRAY as a turnkey application that can generate ray traced images for us. We haven't included POVRAY on the disk that comes with this book, and you don't really need a copy of it in order to use the maze engine that we'll develop in this chapter, because l've already created a set of images that are included on the disk. However, you may want to obtain a copy anyway, so that you can use it to generate new maze graphics for the program in this chapter (or for maze programs that you write yourself) . O nce again, you should take a Iook at Appendix A for details on how to do this.
N i n e Views of the M a ze What sort of images do we want POVRAY to create? Weil, the maze images that we're going to construct (as you can see in the TESTMAZE.TGA image) will consist of four primary elements: the floor, the sky, and the front and side walls of the "blocks" that make up the maze. Thus the images that we create with POVRAY will need to contain these four elements. The sky and floor will probably Iook pretty much the same in every image we construct of the interior of the maze, differing only in the way that they are obscured by the other two elements. lt is the front and side views of the maze blocks that will differ most significandy from one image to the next. So the main thing we need POVRAY to
GARDENS OF I MAG I NATION
do for us is to generate pictures of the fronts and sides of the maze blocks in all the possible ways in which they may appear in a scene. This may sound quite difficult, but in fact it only requires that we generate nine separate images. This will allow us to construct images of the maze as it appears out to five squares in front of the viewer, counting the square in which the viewer is standing. (We can actually get by with even fewer images than this,
Fig ure 4-3a-e These five images.
Figure 4-3b
generated by the POV rav tracer. depict the fronts of maze blocks as they appear out to a distance of five sq u a res in front of the viewer
Fig u re 4-3c
Fig u re 4-3e
Figure 4-3d
CHAPTER FO U R
Mdnipu ldting ßitmdps
as we'll explain at the end of this chapter, but for now we're going to talerate some redundancy in the images in order to simplif)r our programming task.) The first five images that we'll generate with POVRAY will depict the fronts of the maze blocks as they appear at distances of one, two, three, four, and five squares away from the viewer. These images appear in Figure 4-3. On the disk, we have given them the filenames FRONT l .TGA through FRONT5 .TGA. You can view them using TGASHOW or any other file viewer that can display 8-bit Targa files. The remaining four images depict the left and right sides of the maze blocks as they appear at distances of one, two, three, and four squares to each side of the viewer. lt's not necessary to include images of the block sides as they appear at a distance of five squares from the viewer, since they'll no longer be visible on the periphery of the image at that distance. These images appear in Figure 4-4 and on disk as SIDE l .TGA through SIDE4.TGA.
Figure 4-4a-d These fo ur images depict
Figure 4-4b
the left and right sides of maze blocks as they appear at d i stances of one, two, three, and four squares to each side of the viewer
Figure 4-4c
Figure 4-4d
GARDE:NS O F I MAG I NATIO N
S l i c i n g a n d D i c i n g the M a z e Now that we have these images, what do we do with them? We chop them up and use them to construct individual "snapshots" of the maze. I like to think of this as the slice-and-dice method of maze generation. From these nine ray-traced images, we can slice and dice tagether a large number of composite images showing how different parts of the maze look when viewed from various angles. The trick is figuring out what parts of these images to slice and when to slice them. To that end, we'll create a drawmazeO function that will operate essentially like the drawmazeO function that generated our wireframe maze. In fact, it will be possible to drop this function imo a program nearly idemical to the animated wireframe maze program in the last chapter. However, this function will be a great deal more complex than the wireframe maze-drawing function, not j ust because it will deal with bitmaps rather than lines - in fact, bitmaps are actually somewhat easier to put on the display than straight lines - but because we'll use it to render more complex views of the maze. For instance, the wireframe maze-drawing function developed in chapter 2 had difficulty with any parts of the maze that were not along the line of squares proceeding away directly in front of the viewer. And it couldn't handle situations in which four empty squares were arranged tagether in a block. It could draw maze images analogaus to the bitmapped image in Figure 4-5 , but not like the ones in Figure 4-6. We could get away with that sort of approximation of reality in a wireframe maze program because wireframe mazes are inherently unrealistic. If holes appeared in the image where hallways were supposed to be, it wasn't especially bothersome. In a bitmapped maze program, however, we can't talerate such lapses of realism. To get around these problems, we'll use a recursive maze-drawing function. What's a recursive function? It's a function that calls itself. If you're not familiar with the principles of recursive programming, this might sound rather pointless. Why would we write a function that calls itself? As I describe the algorithm behind the function, though, I suspect you'll begin to see why recursion is just the ticket for producing a realistic maze.
The M a z e - D ra wi n g A l g o rith m The maze-drawing function will be based on the simple concept that the three dimensional view of the maze, which we'll be drawing in a reetangular window on the display, divides up neatly into three portions. Let's take a closer look at how it does so.
CHAPTE:R FO U R
M d n i puldting ßitmdps
Figure 4-5 We could draw this image using essentially the same techn iq ues we used to create the wireframe maze graph ics i n cha pters 2 a n d 3 , i f w e su bstituted bitmaps for white l i nes
As in the last two chapters, the maze will be defined as a matrix of squares similar to a checkerboard. Each of these squares is either empty or contains a solid block, presumably carved from some impassable (and visually opaque) substance. At any given moment, the viewer will be standing in the middle of one of these squares, looking in one of the cardinal compass directions - that is, toward one of the sides of the current square. We can therefore visualize the three-dimensional maze view that we'll be drawing on the screen as consisting of
Figures 4-6 a-b The tech n i q ues we used to create the wireframe maze graphics in chapters 2 and 3 wou ld not be adequate to create images l i ke these. even with the Su bstitution of bitmaps for white l i nes
Figure 4-Gb
GARDE:NS OF I MAG I NATI O N
MIDDLE
RIGHT
Figure 4-7 The maze view window divides neatly i nto left. right. and middle views
three parts: the view to the left of the current square, the view to the right, and the view straight down the middle. Figure 4-7 shows how the view window on the display divides up into these three views. Each of these three views Iooks across one of the three visible squares adjoining the current square. (There are actually four squares adjoining the current square, but the one to the rear of the viewer isn't visible, so we won't be concerned with it here.) If one of these squares contains a block, then the ponion of the view window that Iooks across that square will be completely filled with the image of that block, or a section of that image. If the square to the left of the current square contains a block, for instance, the left portion of the view will be filled with an image of the right side of that block (see Figure 4-8). If the square to the right contains a block, the right portion of the view will be filled with an image of the left side of that block (see Figure 4-9). And if the square in front of the current square contains a block, the middle portion of the view will be filled with an image of the front of that block (see Figure 4- 10). Each of these images - of the left, right, and front sides of a maze block as seen at a distance of one square - are available in one of the TGA files that we created with the POV ray tracer. In this case the left and right images are in SIDE1 .TGA and the front image is in FRONT I .TGA. If all three of these squares contain blocks, then drawing the view will be a simple matter of slicing these images from the SIDE I .TGA and FRONT I .TGA files and dicing them into the appropriate portians of the view window. Figure 4- 1 1 shows what the resu!ting image would Iook like. Drawing this image is simply a matter of checking the squares to the left, right, and in front of the
C HAPTER FO U R
M d n i puldiing ßiimdps
Maze
View
Figure 4-8 lf the square to the left of the cu rrent square contains a block, the left portion of the view will be fi lled with a n image of the right side of that block
Maze
View
Fig ure 4-9 lf the square to the right of the cu rrent square contains a block, the right portion of the view will be fi l led with a n image of the left side of that block
GARDENS OF I MAG I NATION
Maze
View
Figure 4-1 0
Solid Wall
lf the square i n front of the current square
conta ins a block, the middle portion of the view w i l l be filled with an image of the front of that block
Figure 4-1 1 blocks
The view from the current square if all three visible adjoining squares conta in
CHAPTE:R FO UR
Mdnip u ldting ßitmdps
currenr square to see if they conrain blocks. If they do, the images of the blocks are copied from the TGA files (which we will have stored in memory previously, using the loadTGA O function) into the appropriate positions on the display. Bur this is an unusually easy case to handle. In fact, it is the simplest image that our maze-drawing function will ever be called upon to render. What if one of the visible squares adjoining the current square is not occupied by a block? What do we do then? Obviously, we don't draw the image of a block if there isn't one there - bur what do we draw in its place? We can't simply draw nothing; that would leave a gaping black hole in our drawing, which is in herently unrealistic. We might have been able to get away with this in our wireframe maze program, but such a lazy programming technique isn't adequate to the realism of a bitmapped maze. We could simply proceed to check to see if the squares on the other side of the adjoining square are occupied, then draw any blocks that we find there, bur this is an awkward thing to do. There may be as many as three additional squares visible rhrough each of the squares adjoining the currenr square and we'll need to search each of them to see if they're occupied by blocks. And if rhey aren't occupied by blocks, we'll need to search even further inro rhe maze, looking for blocks to draw. If we're not careful, our drawmazeO function could become quite complicated. Fortunately, there's a fairly simple approach that we can take that will strip away much of the complexity. Each of the squares adjoining the current maze square presenrs almost exactly the same visual problern that the currenr square does that is, each is bordered by three squares that could potenrially conrain visible blocks. (Once again, we'll ignore the fourth adjoining square, the one to the rear of the square currently under consideration. Although in many cases this square may be visible, the fact that we can see through it teils us that it isn't occupied by a block.) If we write a simple function that examines the current square (which we know to be empty because we're standing in it) to see if it's surrounded by any visible blocks (and which draws them if it finds them) , that function can call itself recursively with slightly modified parameters to examine any adjoining empty squares to see if they are in turn surrounded by visible blocks. In other words, all we need to do is write a function to deal with a single square and Iet recursion handle the rest. This will presenr some technical problems, bur they aren't difficult to solve. We'll have to be sure that the function only draws those blocks that would be visible from the current position and that it doesn't try to draw blocks that are off the left and right edges of the view window. However, this is a simple matter of passing a pair of parameters to the function telling it what the left and right Iimits of the current view actually are. Ini tially, these will be the left and
GARDENS OF I MAG I NATI O N
right horizontal coordinates of the entire view window. (We'll measure these coordinates relative to the 1 60 by 1 00 p ixel view window itself, so the left horizontal coordinate will initially be 0 and the right horiwntal coordinate will be 1 5 9 .) But when the function calls itself recursively, these will become the left and right Iimits of the part of the view window covered by the square currently being examined. When the function calls itself to handle the square to the immediate left, for instance, it will pass itself the left and right coordinates of the portion of the view window that represents the left view, as we illustrated in Figure 4-8 .
T h e B itma p p ed d ra w m a z e o Fu ncti on Now that we have a t least some idea o f how the maze-drawing function will work, let's write it. The prototype for the function looks like this: vo i d d r a wma z e ( i n t x s h i f t , i n t ys h i f t , i n t
L v i ew, i n t
rvi ew,
c h a r * s c reen ) ;
The first thing you're likely to notice is that there are a lot more parameters being passed ro this version of the function than there were to the version that drew wirefrarne mazes. Bear in mind that this function is designed to work with only one square of the maze at a time; it will call itself recursively to deal with other squares in the maze. Thus the first rwo parameters, xshift and yshift, tell us how far the square currently being examined is from the square in which the viewer is standing. The first parameter, xshift, teils us how far (in squares) this square is to the left of the viewer's square (with negative values representing positions to the right of the viewer's square) . The second param eter, yshift, tells us how far forward the square currently being examined is from the viewer's square. This value will always be positive, since we aren't interested in the unseen squares to the rear of the viewer's square. On the first call to the function, these values should both be 0, since the square in which the viewer is standing is always the first square that drawmaze() should examine. During recursive calls, the function will shift these parameters to the left, right, and front, if the squares in those directions are visible and no blocks are found in them. The lview and rview parameters represent the left and right horizontal coordinates of the screen window in which any visible blocks are ro be drawn. Initially, these will be the same as the left and right horizontal coordinates of the view window - that is, 0 and 1 5 9 - but during recursive calls they will be repeatedly reduced to those sections of the view window that are visible through the square currently being examined. We don't want the function to keep calling itself recursively forever; this could cause it to lock up in an endless loop. As a more practical matter, we don't want it ro call itself to exam ine squares beyond the visibility limit inherent in our
-140
C HAPTER FO U R
M d n i pu ldtinq ßitmdps
prerendered TGA files. Since these files only show the sides of blocks up to five squares distant, we'll set the viewing distance as covering the viewer's current square plus four additional squares. We'll define this distance at the front of the program with the constant VIEW_LIMIT (which we'll set equal to 4) and begin the drawmaze() function by checking to see if it has been reached: II
R e t u r n i f we ' v e r e a c h e d t h e
r e c u r s i ve
if
( ( a bs ( x s h i f t ) > V I EW_L I M I T )
I I
L i mi t :
( a b s ( y s h i f t ) > V I E W_L I M I T ) )
return;
This will prevent the function from making further recursive calls to itself In practice, we don't want this limit ever to be reached, since invoking this escape clause will leave a gaping black hole in the maze drawing. So we must be careful to design mazes in which the viewer can never see more than four squares ahead in any direction. Later in this chapter, we'll suggest some ways to avoid - or at least make l ess restrictive - this Iimitation. (Note that this doesn't mean avoiding more than four squares in a row, but avoiding lines of sight that extend for more than four squares. Since lines of sight can go at an angle to the grid of squares, there will be situations where the corridor in front of the view extends for four or fewer squares, but a fifth square is nonetheless visible araund a corner. Ir may require some trial and error to find such situations and restructure the map of the maze to avoid them.) Now that we've gor that minor technicality out of the way, we can get down to the real work of the function, which is to check the squares to the left, front, and right of the current square to see if they are occupied by blocks. We'll break this imo three separate tasks and check the square to the left first. We'll do this with the aid of the same data structures that we used in the last two chapters to define positions and movements within the maze. You'll recall that the viewer's position is contained in the variables pos.x and pos.y, and that the viewer's orientation is contained in the variable dir. Because the meaning of such directions as left, right, and forward changes according to the viewer's orientation, we also created three arrays - called left, right, and forward- that defined the way in which the x and y coordinates wirhin the maze change when we move in each of those directions. For instance, the variables left[O].x and left[O].y contain the changes in the x and y coordinates when our orientation is in direction 0 (which happens to be north) and we move to the left. Similarly, the variables forward[2].x and forward[2].y contain the changes in the x and y coordinates when our orientation is in direction 2 (south) and we move forward. We can use these data structures to calculate coordinates wirhin the maze of the square currently being examined and assign those coordinates to the variables sx and sy, like this: II
Ca l c u l a t e coo r d i na t e s of
squa r e t o
Lef t :
GARDENS O F IMAG I NATION i n t sx = pos . x + ( x s h i ft+1 )
*
L e f t [ d i r J . x + ys h i f t * f o rwa r d [ d i r J . x ;
int sy
*
L e f t [ d i r J . y + ys h i f t * f o rwa r d [ d i r J . y;
=
pos . y + ( x s h i ft+1 )
We multiply xshift by the left increment for the current position because xshift represents movement to the left of the viewer's position and we multiply yshift by the fotward increment because yshift equals movement fotward from the viewer's position (see Figure 4- 1 2) . We know that the parameters lview and rview represent the left and right Iimits of our current drawing window. Now we must calculate the Iimits of the lefthand portion of that window, the portion that represents the view to the left of our current position. We'll store these Iimits in the integer variables vleft and vright. Calculating the left Iimit is easy, since it's the same as the left Iimit of our overall view window: v l e f t = Lv i e w ;
Calculating the right Iimit of the lefthand view is trickier - a Iot trickier. In fact, there's not really any way to calculate it at all, since we're working with predefined bitmaps and we must match our view to the positions of the blocks wirhin those bitmaps; othetwise, they won't be drawn properly when we slice and dice our images together. So, in order to see where the right edge of the lefthand view is, we must Iook at the images created by POVRAY to see where it drew the edges of the block. If the square that we're currently examining is the one in which the viewer is standing, then the left portion of the view extends from horizontal coordinate 0
3 2
Forward
1
•
0 3
2
0
Left
Ployer
-1
-2
-3
Right-----
Figure 4-1 2 Moving left, right, and forward from the viewer's position
xshift
CHAPTER FO U R
Mdnipu ldting ßitmdps
(the extreme left side of the view window) to horizontal coordinate 39. (These coordinates are defined relative to the contents of the window, not the display in which the window will eventually be placed.) You can see this by looking at FRONT l .TGA. Although it's hard to teil where one block ends and another begins by looking at the greenish granite walls of the maze, it's possible to identif)r block boundaries by looking at the checkerboard patterns on the floor. Each maze square contains 1 6 checkers, arranged four on a side. The four checkers aligned with the middle portion of the green wall define the block directly in front of the viewer. The block ponions to the left and right of this define the positions of the two adjoining squares. If you count the pixels from the left edge of the view to the right edge of the leftmost block, you'll find that they extend from horizontal coordinate 0 to horizontal coordinate 39, as in Figure 4- 1 3. (Weil, okay, you're probably not enthusiastic abour sirring around counting pixels on the display, so I've counted them for you using Deluxe Paint.) Similarly, the central portion of the view (corresponding ro the extent of the block immediately in front of the viewer) extends from horizontal coordinate 40 to horizontal coordinate 1 20 and the righthand portion of the view extends from horizontal coordinate 1 2 1 to horizontal coordinate 1 59. Bur these coordinates are only valid if we're examining the square where the viewer is standing. As the maze-drawing function moves recursively from square to square, the precise horizontal coordinates represented by the left, right, and center views will change. For instance, from the square ro the immediate left of
0
39
Figure 4-13 T h e leftmost block i n FRO NT1 .TGA extends across t h e image from horizontal position 0 to horizontal position 39
GARDE:NS O F I MAG I NATI O N
the current square, the entire view covers only the lefthand portion of the display - that is, the area from horizontal coordinate 0 to horizontal coordinate 39. Wirhin that portion, the lefthand portion of the view from that square only covers the segme n t of the view window from horizontal coordi nate 0 to horizontal coordinate 7. (This is the extent of the leftmost block front in F RONT2 .TGA . ) The central portion of the view extends from horizontal coordinate 8 to horizontal coordinate 39. And there is no righthand portion to the view from that square, since it would be outside the area that we can view through that square. Whew! How are we going to take care of all that in our recursive function? How can we determine where the lefthand, righthand, and central portians of the current view are in the view window? The answer is surprisingly simple. We must treat the grid of maze squares radiating outward in front of the view as a network of intersecring grid lines, as shown in Figure 4- 1 4 . All we need to worry about are the screen coordinates of the points at which these grid lines intersect, since these correspond to the segments of the view window taken up by the fronts and sides of the maze blocks. We can determine these by studying the TGA files produced by the ray tracer using a program like Deluxe Paint. Then we can encode them in a two-dimensional array that can be referenced by the drawmaze() function. Don't worry; you won't have to dig up a copy of Deluxe Paint and do this yourself, because l 've already done it for you. Here's the resulting array: i n t ma t r i x [ V I EW_L I M I T + 1 J [ V I EW_L I M I T *2+2 J={
CHAPTER FO U R
} ,.
{ 1 60 , 1 60 , 1 60, 1 60 , 1 2 1 , 4 0 ,
0,
0,
0,
0},
{ 1 60 , 1 60 , 1 60 , 1 5 3 , 1 0 5 ,
8,
0,
0,
0},
{ 1 60 , 1 60 , 1 60 , 1 32 , 98, 6 3 , 2 9 ,
56,
0,
0,
0},
{ 1 60 , 1 60 , 1 47 , 1 2 1 , 9 4 , 6 7 , 4 0 , 1 4 ,
0,
0},
{ 1 60 , 1 60 , 1 4 1 , 1 1 7 , 9 3 , 68, 4 4 , 2 0 ,
0,
0}
M d n i p u ldtinq ßitmdps
The first row of elements in this array corresponds to the grid line running horizontally in front of the viewer - that is, to the row of block fronts shown in the image FRONT l .TGA. There are only two visible intersection points berween this grid line and the vertical grid lines running away from the viewer. These occur at horizontal coordinates 40 and 1 2 1 in the view window. We've placed those numbers in the center of the row and assigned the other intersection points horizontal coordinates corresponding to the edge of the display, indicating that they are actually off of the edge. The second row of elements corresponds to rhe horizontal row behind thar row, equivalent to the row of block fronts shown in rhe image FRONT2.TGA. And so forth. The two middle elements of each array correspond to the intersection points between these rows and the rows of block sides depicted in SIDE l .TGA. The two elements ro each side of these co rrespond to the i n tersecti o n p o i n ts with the rows o f b l ock s i des i n SIDE2.TGA. And so forth. This gives us a relarively simple way of derermining the current left portion of the display. We can do it like this: v r i g h t=ma t r i x [ ys h i f t J [ x s h i f t +VI EW_L I M I T+1 J ;
This gives us the position i n the matrix[} array corresponding to rhe grid intersection immediately forward and to the left of the square currendy being examined. lt's possible rhat rhe right edge that we've calculated in this manner will actually be fanher to the right than the right edge of the viewing window passed ro us by the calling function. If so, the edge passed by the calling function takes priority over this edge; we must clip this edge to ir: if
( v r i g h t > rv i e w )
v r i g h t = r v i ew;
After all of the above, the width of our new viewing window may be 0; this would be true, for instance, if we've recursed all the way past the left edge of the original view window, into that part of the grid where we've set all intersection points equal to either 0 or 1 60. So we calculate the width of the window: vw i d t h=v r i g h t -v l e f t ;
I f the width isn't 0, we proceed t o check the square to the left o f the current square - the one that's visible through the left portion of the current view - to see if it's occupied by a block:
GARDENS O F I MAG I NATION if
( v w i d t h> O ) { if
( ma z e [ s x J [ sy J )
{
If it is, we call a function called display_sliceO that takes a horizontal slice out of one of the TGA images and copies it to the video buffer: d i s p l a y_s l i c e ( x s h i f t +VI EW_L I M I T+ 1 , v l e f t , v r i g h t , s c r een ) ; }
We'll Iook at the display_sliceO function in a moment. The first parameter in the call is the number of the file containing the desired slice, where FRONT l .TGA through FRONT5.TGA are 0 through 4 and SIDE l .TGA through SIDE4.TGA are 5 through 8. We're drawing the block to the left of the current block, so we know that we'll be drawing a side view. Thus the file that we want to slice the i m age fro m is at l e a s t n u m b e r 5 , wh i ch is V I EW_L I M I T + 1 . (We use VIEW_LIMIT+ 1 to determine the number of the file rather than 5 so that it will be easier to change the view Iimit at a later date, though we'll need to add additional bitmap files should we decide to do this.) We add to this the xshift distance from the viewer's position, since this will define how many squares away from the center the side that we wish to draw is and thus which TGA file it will come from. The remaining three parameters define the left and right horizontal coordinates of the slice that we wish to draw and the address of the screen buffer we wish to draw it to. What if we don't find a block in the square to the left? Simple. We call the drawmazeO function recursively, shifting one square to the left, and start all over agam: e l s e d r a w m a z e ( x s h i f t + 1 , y s h i f t , v l e f t , v r i g h t , s c r ee n ) ; }
Now we j ust repeat this process for the central and rightmost portions of the view. Here's how we handle the center portion of the view: I I Get
L e f t- r i g h t
e x t e n t of
c e n t e r pa rt
of
v i ew :
v l e f t =ma t r i x [ y s h i f t J [ x s h i f t + V I EW_L I M I T+ 1 J ; v r i g h t =ma t r i x [ y s h i f t J [ x s h i f t +V I EW_L I M I T J ; if
( v leftrvi ew)
v l e f t = L v i ew ; v r i g h t=rvi ew;
vw i d t h = v r i g h t -v l e f t ; if
( vw i d t h>O ) { II
If
II
draw
the
s qu a r e d i r e c t l y a h e a d i s oc c u p i e d ,
if
( m a z e [ s x J [ syJ )
f ro n t of
c u be : {
d i s p l a y_s l i c e ( y s h i f t , v l e f t , v r i g h t , s c r e e n ) ; }
CHAPTER FO U R II
E l s e ca l l
f u n c t i on
r e c u r s i ve l y t o d ra w
M d n i pu ldtinq ßitmdps
th e v i ew f r om
II n e x t s q u a r e f o r wa rd : e l s e d ra wm a z e ( x s h i f t , y s h i f t + 1 , v l e f t , v r i g h t , s c reen ) ; }
And we handle the rightmost portion of the view in a nearly identical manner: II
Ca l c u l a t e coord i na t e s of
sx
pos . x + ( x s h i f t -1 ) *
sy = pos . y + ( x s h i f t -1 ) II
Get
*
Left-ri ght extent
s qua r e to
ri ght :
L e f t [ d i r J . x + ys h i f t * f o rwa r d [ d i r J . x ; L e f t [ d i r J . y + ys h i f t * f o r w a r d [ d i r J . y ; of
r i g h t pa r t of
v i ew :
v l e f t =m a t r i x [ y s h i f t J [ x s h i f t +V I EW_L I M I T J ; if
( v l e f t < L v i ew ) v l e f t = L v i ew ;
v r i g h t = r v i ew; vwi d t h =v r i g h t -v l e f t ; if
( vw i d t h > O ) II
I f t h e squa re to t h e r i g h t
I I d ra w if
{
Left
s i d e of
( ma z e [ s x J [ s y J )
i s o c c up i e d ,
cube :
{
d i s p l a y_s l i c e ( V I EW_LI M I T + 1 - x s h i f t , v l e f t , v r i g h t , s c r ee n ) ; } II
E l s e ca l l
I I next
f u n c t i o n r e c u r s i ve l y to d r a w t h e v i ew f rom
squa r e to t h e r i g h t :
e l s e d r a wma z e ( x s h i f t -1 , y s h i f t , v l e f t , v r i g h t , s c r e e n ) ; } }
The complete text of the drawmazeO function appears in Listing 4- 1 0.
Listing 4-1 0 The b itma pped d rawmazeo f u n ction vo i d d ra wma z e ( i n t xs h i f t , i n t ys h i f t , i n t I I R e c u r s i ve f u n c t i on I I ma z e squa r e a t
to d ra w
Left,
POS . X+X S H I F T ,
L v i ew , i n t
r v i ew, c h a r * s c reen )
r i g h t a nd c e n t e r wa l l s f o r t h e
POS . Y+YSH I F T , a s v i ewed f rom POS . X ,
I I POS . Y , w i t h i n t h e s c r e e n w i ndow b o r d e red by h o r i z on t a l I I LV I EW a n d R V I E W o n t h e
L e f t and
II
w i t h X S H I FT a nd Y S H I F T s e t
II
f rom POS . X , PO S . Y out
to 0,
ri ght
C o o r d i na t e s
r e s p e c t i ve l y . W h e n ca l l ed
d r a w s en t i r e ma z e a s v i ewed
t o V I EW_L I M I T squa r e s .
{ i n t v l e f t , v r i g h t , vw i d t h ; I I Return if
i f we ' ve
r e a c h ed t h e r e c u r s i ve
( ( a b s ( x s h i f t ) > V I EW_L I M I T )
I I
L i mi t :
( a b s ( y s h i f t ) > V I EW_LI M I T ) ) continued on next page
GARDE:NS O F IMAG I NATION contim«dfrom previous page
return; II
C a l c u l a t e coo rd i n a t e s of
i nt s x i nt
pos . x + ( x s h i f t + 1 )
sy = pos . y +
I I Get
< xs h i f t + 1 )
l ef t- r i g h t extent
of
squa r e t o
lef t :
*
l e f t [ d i r J . x + ys h i f t * f o r w a r d [ d i r J . x ;
*
l e f t [ d i r J . y + ys h i f t * f o r w a r d [ d i r J . y ;
l e f t pa r t
of
v i ew :
v l e f t = l v i ew; v r i g h t =ma t r i x [ y s h i f t J [ x s h i f t +V I EW_LI M I T+ 1 J ; if
( v r i g h t > rv i ew )
v r i g h t = r v i ew;
vw i d t h = v r i g h t -v l e f t ; if
( vw i d t h >O ) II
If
II
draw
if
{
t h e squa re
to
t he
ri g h t s i de o f
( ma z e [ s x ] [ s y ] )
left
i s o c c u p i ed,
c ube :
{
d i s p l a y_s l i c e ( x s h i f t + V I EW_L I M I T+ 1 , v l e f t , v r i g h t , s c r een ) ; } II
E l se
ca l l
II
next
sq u a r e t o t h e
f u n c t i on
r e c u r s i ve l y to d r a w t h e v i ew f rom l eft :
e l s e d r a wma z e ( x s h i f t + 1 , y s h i f t , v l e f t , v r i g h t , s c r e en ) ; } II
C a l c u l a t e coo rd i na t e s o f s q u a r e d i r e c t l y a h e a d :
sx
pos . x + x s h i f t *
l e f t [d i r J . x +
( y s h i f t+1 )
* forwa rd[di r J . x ;
sy = pos . y + x s h i f t *
left[di rJ . y +
( y s h i f t+ 1 )
* fo rwa r d [ d i r J . y ;
II Get
l e f t - r i g h t extent
of
c e n t e r pa r t
of v i ew :
v l e f t =ma t r i x [ y s h i f t J [ x s h i f t + V I EW_L I M I T + 1 J ; v r i g h t =ma t r i x [ y s h i f t J [ x s h i f t + V I E W_L I M I T J ; if
( v l ef t < lvi e w )
if
( v r i g h t > rv i ew )
v le f t = l v i ew; v r i ght=rvi ew;
v w i d t h = v r i g h t -v l e f t ; if
( vw i d t h> Q ) II
If
II
draw
if
{
t he square d i re c t l y a head f ro n t o f
( m a z e [ sx ] [ sy ] )
i s o c c u p i ed,
cube : {
d i s p l a y_s l i c e ( ys h i f t , v l e f t , v r i g h t , s c r ee n ) ; } II
E l s e ca l l
I I next
f u n c t i on
recurs i ve l y
to d r a w t h e v i ew f rom
s q u a r e f o r w a rd :
e l s e d r a wma z e ( x s h i f t , y s h i f t + 1 , v l e f t , v r i g h t , s c r e e n ) ; } II sx
C a l c u l a t e coo rd i na t e s o f s qua r e t o r i g h t : pos . x + ( x s h i f t -1 )
*
l e f t [ d i r J . x + yshi f t * forward[d i r J . x;
sy = pos . y + ( x s h i f t -1 )
*
l e f t [ d i r J . y + yshi f t * forward[d i r J . y;
CHAPTE:R FO U R II
Get
left-ri ght extent
of
r i ght
part
of
Mdnip u ldting ßitmd ps
v i ew :
v l e f t =ma t r i x [ y s h i f t J [ x s h i f t +V I EW_L I M I T J ; if
( v l e f t < l v i e w ) v l e f t = l v i ew ;
v r i g h t = rv i ew; vw i d t h=vr i g h t -v l e f t ; if
( vw i dt h>O ) II
{
I f t h e squa re
II draw if
left
to t h e r i g h t
s i d e of
( ma z e [ s x J [ s yJ )
i s o c c u p i ed,
cube :
{
d i s p l ay_s l i c e ( V I EW_L I M I T + 1 - x s h i f t , v l e f t , v r i g h t , s c r e e n ) ; } II
E l se c a l l
II
n e x t squa re
funct i on to t h e
r e c u r s i ve l y to d r a w t h e v i e w f rom ri ght :
e l se d ra wm a z e < x s h i f t - 1 , ys h i f t , v l e f t , v r i g h t , s c r e e n ) ; } }
The d raw_s l iceo F u n ct i o n The text of the draw_sliceO function, which draws a vertical slice out o f a 160 by 1 00 bitmap stored in an array to the equivalent position in the 1 60 by 1 00 view window, appears in Listing 4- 1 1 .
vo i d d i s p l a y_s l i c e ( i n t II
D i sp l a ys v e r t i c a l
i ma g e n u m , i n t
s l i ce o f
leftx, i nt
r i g h t x , c h a r f a r * s c reen)
1 60 x 1 00 bi tmap f rom h o r i z o n t a l
I I c o o rd i n a t e L E FTX t o h o r i z on t a l
coor d i n a t e R I G HTX
{ for
( i n t y=O;
for
yx[2} and poly->x[3} fields in the polygon structure, since we're going to observe the convention that all polygons have perfectly vertical right and left edges and therefore the same x coordinates at the top and bottarn vertices of that edge.) The leftx and lefty parameters represent the x and y coordinates of the upper left corner of the viewport that the polygon is to be clipped against. The rightx and righty parameters represent the coordinates of the lower-right corner of the viewport. The color parameter is the palette nurober of the color in which the polygon is to be drawn and the *screen parameter is a pointer to the video display or to the buffer in which the drawing is to take place.
The Po lyg o n - D raw i n g A l g o rith m How will we go about drawing the polygon? The algorithm that we're going to use will break the polygon into a series of lines, each of which will be drawn on the video display with a for() loop. Figure 5- 1 1 shows how this will work. Figure 5-1 1 a shows an unfilled polygon drawn as a series of connected edges. Figure
GAR DENS O F I MAG I NATI ON
Figure s-10 The coord inates of the vertices of the polygon w i l l be specified in clockwise order sta rti ng at the upper left corner
5- 1 1 b shows a filled version of the same polygon drawn as a series of vertical lines. What makes this drawing algorithm possible is the fact that we have limited ourselves to polygons with vertical left and right edges. The left and right edges will be treated as two of the lines in the polygon's fill. Figuring out where to draw these two lines will be simplicity itself: The line for the left edge will be drawn vertically from coordinates poly->x[O},poly->y[O} to poly- >x[3},poly->y[3} and the right edge will be drawn vertically from coordinates
Figure 5-1 1 a An u nfilled polygon d rawn
Figure 5-1 1 b The same polygon d rawn
a s a series of connected edges
as a series of vertical l i nes
CHAPTER FIVE
PolyQon Mdzes
po/y- >x[J},po/y- >y[l} to poly->x[2},poly- >x[2}. The real trick in drawing the polygon will be figuring out where the intervening lines will be drawn. lt helps to remernher that the top and bottom coordinates of these lines are all points on the top and bottom edges of the polygon. Thus, we can calculate these coordinates the same way we calculate the points on a line, using Bresenham's algorithm. If you were paying attention back in chapter 2, you'll recall that Bresenham's algorithm uses a fo rm of in cremental division to calculate the changes in coordinates along a line. A value known as an error term is maintained and the difference between either the y or x coordinates of the endpoints (whichever was smaller) is added to this value and the value is compared with rhe lengrh of the line on each iteration of the line-drawing loop. When the error term becomes greater than the length of rhe line in the other dimension, the pixel position is advanced in the direction represented by the error term. We can use that same trick to calculate the endpoints of the venical lines making up rhe polygo n, except that we'll need to maintain a pair of error terms - one for the top coordinates of rhe line and one for the bottom coordinates.
C l i p p i n g Aga i n st the Left E d g e of the Viewpo rt Before we can perform any polygon drawing, we must perform some polygon dipping. However, in the name of efficiency, we are going to employ different methods for dipping against each edge. We could simplify our task greatly by having the polygon-drawing function step through ali of the vertical lines that make up the polygon, checking before drawing to see if the line falls wirhin the viewport. If the line is not in the viewport, it would sim ply not be drawn. Similarly, when drawing the individual pixels that make up the vertical line, the function could check to see if each pixel is outside the viewport and not draw it if it is. This method, however, would waste a Iot of the CPU's valuable time, since it would require the function to step thro ugh a !arge number of lines and pixels that don't need to be drawn. Fortunately, there's an alternate method that we can use to dip the polygon or, rather, several alternate methods. During the initialization portion of the polygon-drawing function, we can check to see if any portion of the polygon extends beyond the left edge of the viewport. If it does, we can recalculate the coordinates of the upper-left and lower-left corners of the polygon to reflect the point at which they cross the edge. This method has a disadvantage too: lt will require floating point math, which is notoriously slow. If the user has a math coprocessor in his or her computer, this method will still probably be faster than the first method; but if no math coprocessor is present, this method could well be slower. Since rhe majoriry of machines now have math coprocessors insralled the coprocessor comes Standard with every 486DX chip, rhough not wirh the
GARDE:NS OF IMAGI NATION
4 86SX - we'll go with this second method. (In a later chapter, we'll discuss fixed point arithmetic, which can provide us with a high speed replacement for the slow floating point operations that we'l l be using in this chapter. Readers interested in speeding up this program may want to rewrite it using fixed point math, which should run faster both on machines with coprocessors and without.) We can check for a polygon that crosses the left edge of the video display by comparing the value of po!y->x[O}, the x coordinate of the upper-left corner of the polygon being pointed to by the parameter poly, with the value of leftx, the x coordinate of the upper-left corner (and therefore the common x coordinate of all points on the left edge) of the viewport, to see which is smaller: if
( po l y-> x [ O J < L e f t x )
{
If po!y->x[O} is smaller than leftx, we know that the polygon extends past the left edge of the viewport. So we'l l need to recalculate both po!y->x[O},po!y- >y[Oj and po!y->x[3},po!y->y[3}. (Actually, we don't need to recalculate po/y->x[3}, since it should always be idenrical to po!y->x[O}.) Recalculating the x coordinates where the upper and lower edges of the polygon cross the left edge of the screen is simple enough. We can simply reset these coordinates to the x coordinate of the left edge, which is stored in leftx. Finding the y coordinates at which the edges cross the left edge is a bit tougher. We'll need to use the so-called point-slope equation to find these coordinates, which will in turn require some floating point (or fixed point) math.
Ca l c u l a ti n g the S l o p e First we must calculate the slopes o f the edges. You'll recall from chapter 2 that the slope of the line is the ratio of the change in y coordinates along the length of the line to the change in x coordinates. Thus, to obtain the slope, we must divide the change in the y coordinate by the change in the x coordinate. In fact, we'll need to do this rwice, once for the upper edge of the polygon and once for the lower edge of the polygon, since each of these can have a different slope. We'll store the slope of the top edge in the variable tslope and the slope of the bottom edge in the variable bslope, like this: t s l op e= ( f l o a t ) ( po l y->y[ 1 ] -po l y->y [ Q ] ) / ( po l y->x [ 1 ]-po l y-> x [ Q J ) ; bs l op e= ( f l oa t ) ( po l y ->y [ 3 ] -po l y->y [ 2 ] ) / ( po l y->x [ Q ] - po l y->x[ 1 ] ) ;
As in chapter 2, we determine the change in x and y coordinates by subracting the initial x and y coordinates of the line from the ending x and y coordinates. You'll notice that we don't reference poly->x[2} and po!y->x[3} when calculating the slope of the bottom edge, using instead the starring and ending points of the upper edge. In fact, we'll never reference the x coordinates of the lower edge
CHAPTE:R FIVE
POII,JCJOn Mdzes
anywhere in the polydrawO function, using instead the identical x coordinates of the top edge. This allows us to safely skip some operarions that reference the x coordinates of rhe bottarn edge, making this function slighdy more efficient. You'll also notice that we force the value of the division to ßoating point with the (jloat) cast. If we failed to do this, the result of the division would be an integer, even though tslope and bslope (declared earlier in the funcrion) are ßoating point variables.
The Point- S i o p e Eq uati o n Now that we have the slope, we can calculate the y coordinate o f the edges using only the slope, the x coordinate of the edge to be clipped againsr, and the coordinates of one point on each line (hence the name "point-slope equation"). The general formula for this is: new_y =
y + s l ope * C xm i n - x >
where x and y are the coordin ates of any point o n the line, xmin i s the x coordinate of the left edge, and new_y is the y coordinate of the point at which the line crosses the left edge. Using this formula, we can use tslope and bslope ro find the new coordinates of the upper- and lower-left corners of the polygon: po ly->y[ Q J = po l y-> y [ O J + t s l o pe* C l e f t x -po l y->x [ Q J ) ; po ly-> y [ 3 J = po l y->y[ 3 J +bs l o pe* C l e f t x -po l y->x [ Q J ) ; po l y->x [ O J = L e f t x ; }
You'll notice that this code ignores the x coordinate o f the lower-left corner, using instead the identical x coordinate of the upper-left corner. And the new value of that coordinate isn't calculated here, since we won't be using it later. This ends the initial polygon clipping, though we'll be doing more in a moment. There's no need to check right away to see if the polygon exrends past the other three edges of the viewport, since we'll be using different methods for clipping each of these. More about this in a moment.
The Drawi ng Beg i n s Since the x and y coordinates at which we are drawing will change as we traverse the polygon from left ro right, we'll store the initial drawing coordinates in the integer variables x and y. lnitially, these values will be equal to the coordinates of the upper-left corner: i n t x=po l y->x [ O J ; i n t y=po ly->y [ Q J ;
GARDE:NS OF IMAG I NATI ON
Now we need to calculate the changes in the y coordinates along the top and bottarn edges, for the variation on Bresenham's algorithrn that we'll be using to draw those edges. We'll store the difference in y coordinates for the top line in the integer variable topdiff, and the difference in y coordinates for the bottarn line in the integer variable botdiff i n t top d i f f = po l y-> y [ 1 J-po l y->y [ O J ; i n t bo t d i f f = po l y->y [ 2 J-p o l y ->y [ 3 J ;
We also need to calculate the height of the first vertical line ro be drawn in the polygon. Since this line is at the far left edge of the polygon, its height will be the sarne as the height of the left edge: i n t h e i g h t = po l y->y [ 3 J - po l y-> y [ O J ;
We also need t o know the width o f the polygon - that is, the nurnber of verrical lines that we will be drawing. This value can be obtained by subtracting the x coordinate of the polygon's left edge frorn the x coordinate of the polygon's right edge and adding 1 to it: i n t w i d t h=po ly->x [ 1 J-po l y-> x [ 0 J + 1 ;
C l i p p i n g Aga i n st the R i g ht Edge The value that we use here for the x coordinate of the left edge has already been clipped against the left edge of the viewport. However, the value that we are using for the x coordinate of the right edge has not yet been clipped. So we rnust check ro see if the l ine needs clipping: i f ( p o l y-> x [ 1 J > r i g h t x )
I f it does need clipping, we only need to revise the value of width to reßect the width of that portion of the polygon that will actually be drawn - that is, the portion inside the viewp ort. lt's not necess ary to recal culate the x and y coordinates at which the two edges cross the right edge of the viewport. Instead, we'll write the drawing code so that it stops drawing when it reaches that edge. Because we' ll be using the variable width as part of the Bresenharn code, we'll put the revised value in a new variable, which we'll call vwidth. This integer variable will be assigned a value equal to the distance between the right edge of the polygon and the right edge of the viewport if the two overlap. Otherwise, it will be set to the current value of width: vw i d t h = w i d t h - ( po l y-> x [ 1 J - r i g h t x ) ; e l se vwi d t h = w i d t h ;
CHAPTE R FIVE
Po lygon M dzes
The E rror Te rms Bresenham's algorithm, a s was noted i n chapter 2, uses a value known as an error term to determine when it is time to shift the coordinates of the line in x or y dimension. We need to calculate the coordinates of the top and bottarn edges, so we'll use a pair of integer variables to hold these terms. The one fo r the top edge will be called toperror, and the one for the bottarn edge will be called boterror: i n t tope r r o r =O; i n t bot e r r or =O;
The M a i n Lo op Now we can begin drawing the vertical lines that will rnake up the polygon. How rnany vertical lines will we be drawing? One for every pixel in the visible width of the polygon, a value that we've already calculated and stored in the variable vwidth. We'll use a for() loop to step through the polygon's width: for
( i n t w=O;
w r i g h t y ) w h e i g h t = r i g h t y-wy;
Finally, before the line can be drawn, we need to calculate the video memory address corresponding to the screen coordinates x, vy: u n s i gned
i n t p t r=wy*320+x;
o ra w i n g t h e U n e The actual drawing of the line can be handled by another forO Ioop: for ( i nt
h=O;
h= w i d t h ) { bot er ro r-=w i d t h ; i f ( bot d i f f > O ) h e i g h t++;
GARDENS OF IMAGI NAT I O N e l s e --he i g h t ; } } }
When dealing with the bottom edge, we don't need to advance the value of y, but we do need to change the height of the line, either upward or downward as appropriate. Now we are ready to draw the next line, which completes the polygon-drawing loop. The full text of the polydraw function is in Listing 5-2.
T h e p o lyd raw o F u n ct i o n
Listing 5 - 2 T h e po lyd rawo fu nction # i n c l ud e < s t d i o . h> # i n c l ud e # i n c l ud e " po l y d r a w . h " II
F u n c t i o n t o d r a w a po l ygon d e f i n e d by POLYTY PE p a r a me t e r
II
POLY
II
l ef t hand
II
Coo r d i na t e s o f R I G H T X , R I G HTY .
in
c o l o r C O L O R c l i pped a g a i n s t a v i ewpo r t w i t h upper c oo r d i n a t e s of L E F T X , L E FTY and
vo i d po l y d r a w ( po l y t ype * po l y , i n t i nt
ri ghty, i nt
leftx, i nt
l owe r r i g h t h and
lefty, i n t r i g h t x ,
c o l o r , c h a r *sc reen)
{ f l oat
t s l o pe, bs l o pe;
i n t ww i d t h ,wy , w h e i g h t ; II
I f po l ygon c r o s s e s
left
if
( po l y- > x [ O J < l e f t x )
{
II
s i de o f v i ewp o r t ,
clip
it:
C a l c u l a t e s l op e s f o r top and bot tom edges : t s lope = ( f l o a t ) ( po l y->y [ 1 J -po l y->y [ Q J ) I ( po l y->x [ 1 J-po l y->x [ Q J ) ; bs lope = ( f l o a t ) ( po l y->y [ 3 J -po l y->y[ 2 J ) I ( po l y->x [ O J - po l y-> x [ 1 J ) ; II
F i n d new endpo i n t s
for
c l i pped
l i ne s :
po l y-> y [ Q J= po l y->y [ O J + t s l ope* ( l e f t x-po l y->x [ Q J ) ; po l y-> y [ 3 J = po l y->y [ 3 J + bs l o pe* ( l e f t x-po l y->x [ Q J ) ; po l y-> x [ O J = l e f t x ; } II
I n i t i a l i ze x , y coo r d i n a t e s f o r po ly gen d ra w i n g :
i n t x=po ly->x [ O J ; i n t y=po ly->y [ O J ;
C HAPTER FIVE II II
Polygon M dzes
Ca l c u l a t e t h e c h a n g e i n y coord i na t e s for top and bot tom edg e s : i n t topd i f f =po l y->y [ 1 J- po l y-> y [ O J ; i n t bo t d i f f=po l y->y [2 J-po l y->y [ 3 J ;
II
I n i t i a l i z e h e i g h t a nd w i d t h o f c l i pped p o l ygen : i n t h e i g h t= po l y->y [ 3 J -po l y-> y [ Q J ; i n t w i dt h=po l y-> x [ 1 J -po l y-> x [ 0 J + 1 ;
II
C l i p po l ygen w i d t h a g a i n s t r i g h t s i de of v i ewpo r t : i f ( po l y-> x [ 1 J>r i g h t x ) ww i d t h =wi d t h - ( po l y-> x [ 1 J - r i g h t x ) ; e l s e wwi d t h =w i d t h ;
II
I n i t i a l i z e top a nd bot t om e r ro r t e rms : i n t tope r r o r=O; i n t bot e r r or =O;
II
Loop a c ross wi d t h of po l ygen : for < i n t w=O; wO )
{
y++ ; --he i g h t ; } I I o r dow n . else { --y; h e i g h t ++ ; } } Is
II
it
t i me
to move t h e bot t om edge up or down?
b o t e r ro r+=a b s ( bo t d i f f ) ; whi le
( bo t e r ror>=w i d t h )
II
so,
If
move
{
it:
bo t e r r o r-=w i d t h ; if
( bo t d i f f> O )
h e i g h t ++;
e l s e --hei g h t ; } } }
T h e P O LYD E M O P ro g ra m For a look at the polydrawO function in action, we'll write a short demonstration program that will draw a polygon on the display. We'll use the same setup that we've used in earlier chapters for turning on mode 1 3h, saving and restoring the old video mode, establishing an offscreen video buffer, and so forth. Once the i ni ti alization is complete, we'll plug the appropriate val ues into a variable of t y p e polytyp e to d e s c r i b e a po l y go n w i th an u p p e r e d ge exte n d i n g from coordinates 50,90 to coordinates 250, 1 0 and a lower edge extending from coordinates 50, 1 00 to coordinates 250, 1 20: po l y . x [ O J = S O ; po l y . y [ O J = 9 0 ; po l y . x [ 1 J = 2 5 0 ; po l y . y [ 1 J = 1 0 ; po l y . x [ 2 J = 2 5 0 ; po l y . y [ 2 J = 1 2 0 ; po l y . x [ 3 J = 5 0 ; po l y . y [ 3 J = 1 1 0 ;
Then we'll pass this structure to the polygon-drawing function: po l yd r a w ( & po l y , 0 , 0 , 3 1 9 , 1 99 , 1 , s c r e en ) ;
CH APTER FIVE
POII,JQOn Mdzes
Figure 5-12 The video output from the program POLY D E M O . C P P
And that's all there is to drawing a filled polygon. The full text of the program POLYDEMO.CPP is shown in Listing 5-3 and the video output is in Figure 5 - 1 2.
#i n c l ud e
#i n c l u d e
#i n c l ud e
# i n c l ude
< s t d l i b . h>
# i n c l ude
" s c reen . h 11
# i n c l ud e
" po l yd r aw . h "
# i n c l ud e
" b resnham . h "
vo i d ma i n O
{ I I D e c l a r e po l ygen s t r u c t u r e : po l ytype po l y ;
I I C r ea t e po i n t e r t o v i deo memo r y : c h a r f a r * s c r e e n= ( c h a r f a r * ) MK_F P ( OxaOOO, O > ;
I I Save p r e v i o u s v i deo mod e : i n t o l dmode=* ( i n t * ) M K_F P ( O x 4 0 , 0 x49 ) ;
I I Put di s p l a y i n mode 1 3 h : s e tmode ( Ox 1 3 ) ;
I I C l ea r di s p l a y : c l s ( s c r een ) ; continued on next page
GARDENS OF IMAGI NATI ON continuedfrom previow page
I I P u t po l ygon d a t a
i n POLYTYPE s t ru c t u r e :
p o l y . x [ O J = SO; po l y . y [ Q J =90 ; po l y . x [ 1 J = 2 5 0 ; po l y . y [ 1 J = 1 0 ; po l y . x [ 2 J =2 5 0 ; po l y . y [ 2 J = 1 2 0 ; po l y . x [ 3 J = 5 0 ; po l y . y [ 3 J = 1 1 0 ; II
D r a w t h e po l ygon :
po l yd r a w < & po l y , 0 , 0 , 3 1 9 , 1 99 , 1 , s c r e e n ) ; I I H o l d po lygon on s c r een u n t i l whi l e
keys t r oke
< ! kb h i t ( ) ) ;
I I R e s t o r e o l d v i deo mod e : s e t mode ( o l dmode ) ; }
The P O LYC LI P P ro g ra m Ah, but what about the code we placed in the function to clip the polygon against a window? How do we know that it works correcdy? To find out, we'll create a slight variation on POLYDEMO.CPP that will draw a box on the screen representing a viewport, then clip the polygon to that viewport. To draw the box, we'll resurrect the drawbox() function that we used in chapters 2 and 3. The complete listing of POLYCLI P.CPP appears in Listing 5-4 and a picture of its video output in Figure 5- 1 3 .
# i n c l ud e
< s td i o . h>
# i n c l ud e
# i n c l ud e
< c on i o . h>
# i n c l ud e
< s td l i b . h>
# i nc l ud e
" s c r e en . h "
# i nc l ud e
" po l yd r a w . h "
# i n c l ud e
" b re s n h a m . h "
vo i d d ra wb o x ( i n t
Leftx , i nt
char
L e f t y, i n t r i g h t x , i n t r i ghty,
* s c reen ) ;
vo i d m a i n O { II
D e c l a r e po l ygon s t r u c t u r e :
CHAPTER FIVE
Po l y gon M dzes
po l y t y pe po l y ; I I C r ea t e poi n t e r t o v i deo memory : c h a r f a r * s c reen= ( c h a r f a r * ) MK_F P ( 0xa000, 0 ) ; II
Save p r ev i ous v i deo mode : i n t o l dmode=* < i n t * ) MK_F P ( Q x40, 0x49 ) ; I I Put d i s p l a y i n mode 1 3 h : se tmode ( O x 1 3 ) ; I I C l ear d i sp l a y : c l s ( s c r een ) ; I I Draw box o n d i s p l a y : d r a wbox < 70 , 50 , 2 00 , 1 30 , s c r een ) ;
po l y . x [ O J = 5 0 ; po l y . y[Q J=90; po l y . x [ 1 J=250; po l y . y [ 1 J = 1 0 ; po l y . x [ 2 J =250; po l y . y [ 2 J = 1 20 ; po l y . x [ 3 J =50; po l y . y [ 3 J = 1 1 0 ; po l y d r a w ( &po l y , 7 1 , 5 1 , 1 99, 1 2 9 , 1 , s c r ee n ) ; I I Ho l d po l ygen o n s c reen un t i l keyst roke w h i l e ( ! k bh i t ( ) ) ; II
Restore o l d v i deo mo de : setmode ( o l d mo de ) ;
}
vo i d dr aw box ( i n t l e f t x, i n t l e f t y , i n t r i g h t x , i n t r i g h t y , c h a r * s c reen ) {
l i n ed r a w ( l e f t x , l e f t y , r i g h t x , l e f t y , 1 5 , s c r ee n ) ; l i nedra w ( r i g h t x , l e f t y , r i g h t x , r i g h t y , 1 5 , s c r ee n ) ; l i n edraw( r i g h t x , r i g h t y , l e f t x , r i g h t y , 1 5 , s c r een ) ; l i n edra w ( l e f t x , r i g h t y, l e f t x , l e f t y , 1 5 , s c reen ) ; }
A Po l yg o n M a z e It's not hard to plug this polygon-drawing function inro rhe maze-drawing program that we created in chapter 4. Most of the necessary changes will be in the drawmaze() function. We'll also need to recast the array matrix[}, which
G A R D E: NS OF IMAG INATION
represents the screen y coordinates at which the horizontal and vertical walls of the maze cross one another on the display to take into account points that are j ust off the visible edge of the viewport: i n t m a t r i x [ V I EW_L I M I T+2 J [ V I E W_L I M I T*2+2 J = { 0,
0,
0,
0},
{ 1 87 , 1 87, 1 87 , 2 38 , 1 4 3 , 4 7 , - 4 9 ,
0,
0,
0},
{ 1 8 7 , 1 87, 1 8 7 , 1 7 9 , 1 2 3 , 6 6 ,
1 0,
0,
0,
0},
{ 1 8 7 , 1 87, 1 87 , 1 5 8 , 1 1 7 ,
73,
31 ,
0,
0,
Q},
{ 1 8 7 , 1 87, 1 7 7 , 1 4 4, 1 1 1 ,
78,
45,
1 2,
0,
0},
5 6 , 30 ,
4,
0}
{ 1 8 7 , 1 87, 1 87 , 1 8 7 , 1 8 7 ,
0,
{ 1 87, 1 85 , 1 59, 1 33 , 1 07, 82, };
We'll also need t o create two new arrays, which represent the x coordinates of the tops and bottoms of the polygons viewed at various distances: i nt
t o p [ V I E W_L I M I T+ 2 J = { - 1 1 , 2 0 , 3 6 , 4 2 , 46 , 4 9 } ;
i nt
bo t t om [ V I EW_L I M I T+ 2 ] = { 1 3 1 , 99 , 83 , 77 , 7 3 , 70 } ;
Because some o f the polygon edges will actually extend beyond the limits o f the viewport, we have included values in all of these arrays that exceed the limits of our viewport. All coordinates are relative to the upper-left corner of the viewport, which is 1 88 pixels wide, so all negative numbers and numbers larger than 1 87 fal l outside of the viewport.
A N e w M a z e - D ra wi n g F u nction The new maze-drawing function will be much like the old one. We'll use the same recursive algorithm for determining what walls need to be drawn as we used in chapter 4 . (You might want to reread that chapter if you're having trouble remembering how the algorithm works. ) Almost all of the changes concern the function calls that perform the actual drawing. In the bitmapped version that we developed in chapter 4, the drawsliceO function was called to copy a slice from the appropriate bitmap onto the display. These will be replaced by calls to the polydrawO function. Before we can call polydrawQ, we must put a description of the polygon to be drawn in a variable of type polytype. The necessary values can be taken from the matrix, top, and bottom arrays. Here is how we draw the view through the lefthand portion of the current maze sq uare, as requi red by the recursive algorithm that we're using: po l y . x [ O J =m a t r i x [ y s h i f t J [ x s h i f t +V I E W_LI M I T+ 1 ]+X W I N D OW; po l y . y [ O J = t o p [ y s h i f t ]+YWI N D O W ; po l y . x [ 1 J=ma t r i x [ y s h i f t + 1 J [ x s h i f t + V I EW_L I M I T+1 J+XW I N DOW; po l y . y [ 1 J = t o p [y s h i f t +1 J +YW I N D OW;
C HAPTER FIVE
Polygon M dzes
po l y . y [ 2 J= bo t t om [ ysh i ft+ 1 J +YW I N D OW; po l y . y [ 3 J =bo t t om [ys h i f t ]+YW I N D OW;
Then we call polydrawO to draw the polygon: po l yd r a w ( & po l y , v l e f t +X W I N D OW,YW I N D OW , v r i g h t+XW I N D OW, Y W I N DOW+WHE IGHT , 2 , s c r e e n ) ;
As in the previous version, we pass the x coordinates of the right and left edges of the current viewing window wirhin the viewport, contained in the vleft and vright coordinates. (See chapter 4 for a more detailed explanation of what these values represent.) Now, however, we must also pass the y coordinates of the top and bottom of the viewport. To all of these values, we must add the offsets of the view wi ndow (which are contained in the XWI N D OW and YWINDOW constants) to rranslate our view wi ndow relative coordi nates into absolute coordinates on the video display. We repeat this process to draw the view over the middle portion of the current square: po l y . x [ Q J =ma t r i x [ y s h i ft+ 1 J [ x s h i ft +V I E W_L I M I T+1 ]+XWI NDOW; po l y . y [ O J = t o p [ y s h i f t+1 J+YW I N DOW; po l y . x [ 1 J =ma t r i x [ y s h i ft+1 J [ x s h i f t +V I E W_L I M I T J +X W I N D OW; po l y . y [ 1 J = t o p [ y s h i f t+1 J+Y W I NDOW; po l y . y [ 2 J = bot tom[ y s h i f t +1 J+ YW I N D OW; po l y . y [ 3 J = bottom[ysh i f t+1 J+YW I N D OW; po l yd r a w ( & po l y , v l e f t+XW I N DOW , Y W I N D OW , v r i g h t +X W I N D OW, Y W I N DOW+W H E I GHT, 2 , s c r een ) ;
and the view over the righthand portion of the current square: po l y . x [ O J=ma t r i x [ y s h i f t +1 J [ x s h i f t+V I E W_LI M I T J + X W I N D OW; po l y . y [ Q J = t o p [ y s h i f t +1 ]+YW I N DOW; po l y . x [ 1 J=ma t r i x [ y s h i f t J [ x s h i f t +V I E W_L I M I T J +X W I N D OW;
Figure 5-13 The video output from the program POLYCLI P . C P P
GARDE:NS OF I MAG INATI ON po l y . y [ 1 J = t op [ y s h i f t J+ Y W I N D OW; po l y . y [ 2 J = b o t t o m [ y s h i f t ] +YW I N DO W ; po l y . y [ 3 J =b o t t o m [ y s h i f t +1 J+YW I N D O W ; po l yd r a w ( &po l y , v l e f t+X W I N D O W , Y W I N D O W , v r i g h t +XW I N DOW, Y W I N DOW+W H E I GH T , 2 , s c r e en ) ;
The New drawmazeo Fu nction
The text of the polygon-based version of the drawmaze() function appears Listing 5 - 5 :
Listing s-s The polyg o n - based d rawma zeo f u n ction vo i d d r a w ma z e ( i n t x s h i f t , i n t ys h i f t , i n t II
R e c u r s i ve f u n c t i o n to d r a w
I I ma z e II
POS . Y ,
II
LV I E W a n d RV I EW on t h e
rvi ew, c h a r * s c r een )
r i g h t and c e n t e r wa l l s f o r t h e
POS . Y+Y S H I FT, a s vi ewed f r om POS . X ,
w i t h i n t h e s c r e e n w i ndow b o r d e r e d by h o r i z o n t a l left
I I w i t h X SH I FT and YSH I FT s e t II
Left,
s q u a r e a t POS . X + X S H I F T ,
Lvi ew, i n t
f r om POS . X, POS . Y out
and r i ght
C o o rd i n a t e s
re spec t i v e l y . W h e n c a l l e d
to 0 , d r a w s en t i r e ma z e a s v i ewed
t o VI EW_L I M I T s q u a r e s .
{ i n t v l e f t , v r i g h t , vw i d t h ; po l y t ype po l y ; II
Return
if
( ( abs ( xsh i f t )
if
we ' ve r e a c h e d t h e
recur s i ve
> V I E W_L I M I T )
I I
L i mi t :
( a b s ( y s h i f t ) > V I EW_L I M I T ) )
return; II
C a l cu l ate
coo rd i na t e s of
left :
sx
(xshi ft+1 )
*
L e f t [ d i r J . x + ys h i f t * f o rwa r d [ d i r J . x ;
i nt
sy = pos . y + ( x s h i f t + 1 )
*
L e f t [ d i r J . y + ys h i f t * f o r w a r d [ d i r J . y ;
I I Get
pos . x +
s q u a re t o
i nt
L e f t- r i g h t e x t e n t of
L e f t pa r t of
view:
v l e f t = l v i ew; v r i g h t =ma t r i x [ y s h i f t + 1 J [ x s h i f t +V I EW_L I M I T+1 J ; if
( v r i g h t> r v i ew )
vri ght=rvi ew;
vw i d t h =v r i g h t -v l e f t ; if
( vw i d t h>O )
{
II
If
II
draw right
if
t h e squa r e
to
the
s i de of
( m a z e [ s x J [ sy J )
Left
i s o c c u p i ed,
cube :
{
po l y . x [ O J =ma t r i x [ y s h i f t J [ x s h i f t +V I EW_L I M I T+1 J+XWI NDOW; po l y . y [ Q J = t o p [ y s h i f t J + Y W I N DOW; po l y . x [ 1 J =ma t r i x [ y s h i f t + 1 J [ x s h i f t + V I EW_L I M I T+1 J+XW I N D OW; po l y . y [ 1 J = t o p [ ys h i f t + 1 ] + Y W I N DOW;
m
C HAPTER FIVE
Polygon M dzes
po l y . y [ 2 J = bo t t om [ y s h i f t + 1 ]+YW I N DOW; po l y . y [ 3 J = bo t t om [ y s h i f t ]+Y W I N D OW; po l yd r a w ( &po l y, v l e f t+XW I N DOW , Y W I N DOW , v r i g h t +XWI N DOW, Y W I N DOW+W H E I GHT, 2 , s c reen ) ; } II
E l s e ca l l f u n c t i o n r e c u r s i v e l y to draw t h e v i ew f r om n e x t squa r e to t h e l e f t : e l se d ra wma ze ( x s h i f t + 1 , y s h i f t , v l e f t , v r i g h t , s c r ee n ) ; II
} I I Ca l c u l a t e coord i n a t e s o f squa re d i re c t l y a h ead : pos . x + x s h i f t * l e f t [ d i r J . x + ( y s h i f t +1 ) * for wa rd[ d i r J . x ; sx sy = pos . y + x s h i f t * l e f t [ d i r J . y + ( y s h i f t +1 ) * f o r w a r d [ d i r J . y ;
II Get l e f t - r i g h t e x t e n t of c e n t e r pa r t of v i ew : v l ef t=m a t r i x [ y s h i f t + 1 J [ x s h i f t +VI EW_L I M I T+ 1 J ; v r i g h t=ma t r i x [ y s h i f t + 1 J [ x s h i f t +VI EW_L I M I T J ; i f ( v l e f t < l v i ew ) v l e f t = l v i ew; i f ( v r i ght>r v i ew ) v r i g ht =rvi ew; vw i d t h =v r i g h t -v l e f t ;
i f < vw i d t h>O ) { II
I f t h e squa re d i r e c t l y a h ead i s o c c u p i ed, draw f r ont of cube : i f (maze[sxJ [ syJ ) { po l y . x [ O J=ma t r i x [ y s h i f t +1 J [ x s h i f t +V I E W_L I M I T+ 1 J + XW I N DOW; po l y . y [ O J = t o p [ y s h i f t +1 ]+ Y W I N D OW; po l y . x [ 1 J =ma t r i x [ y s h i f t+1 J [ x s h i f t +V I E W_L I MI T J + X W I N DOW; po l y . y [ 1 J=to p [ y s h i f t +1 J+Y W I N D OW; po l y . y [ 2 J=bot tom[ ys h i f t +1 J+YW I N D OW; po l y . y[3 J = bo t t om [ y s h i f t +1 J+YW I N DOW; po l y dra w ( &po l y , v l e f t + X W I N D OW , Y W I N D OW , v r i g h t+XW I N D OW, YWI N DOW+WHE I G HT, 2 , s c r een ) ; } II
II
E l s e ca l l f u n c t i on r e c u r s i v e l y to d r a w t h e v i ew f rom next squa re f o r wa rd : e l se dr awmaz e ( x s h i f t , y s h i f t +1 , v l e f t , v r i g h t , s c r een ) ; II
} I I Ca l c u l a t e coor d i n a t e s of squa re to r i g h t : sx pos . x + ( x s h i f t - 1 ) * l e f t [ d i r J . x + ys h i f t * fo rwa rd[ d i r J . x ; sy = pos . y + < x s h i f t - 1 ) * l e f t [ d i r J . y + ys h i f t * forward [ d i r J . y ; I I Get l e f t - r i g h t e x t e n t of r i g h t pa rt of v i ew : v l ef t=ma t r i x [ y s h i f t +1 J [ x s h i f t +VI EW_L I M I T J ; i f ( v l e f t < l v i ew ) v l e f t = l v i ew; v r i g h t =r v i ew; continued on next page
GARDE:NS O F I MAG IN ATI ON continuedfrom previou.r poge
vw i d t h =v r i g h t -v l e f t ; if
C vw i d t h>O ) II
If
II
draw
if
{
t h e squa r e
to t h e
L e f t s i d e of
C ma z e [ s x J [ s y ] )
ri ght
i s o c c up i ed,
c ube :
{
po l y . x [ O J =ma t r i x [ y s h i f t + 1 J [ x s h i f t +V I EW_L I M I T J + X W I N DOW; po l y . y [ Q J = t o p [ ys h i f t + 1 ] + Y W I N DOW; po l y . x [ 1 J =ma t r i x [ y s h i f t J [ x s h i f t +V I E W_L I M I T J + X W I N DOW; po l y . y [ 1 J = t o p [ y s h i f t J + YW I N DOW; po l y . y[2 J = b o t t o m [ y s h i f t ] + YW I N D OW; po l y . y [ 3 J =b o t t o m [ y s h i f t + 1 J + Y W I N D O W ; po l yd r a w C &po l y , v l e f t + XW I N DO W , YW I N DOW , v r i g h t + XW I N D OW, Y W I N DOW+W H E I G H T , Z , s c r e e n ) ; } II
E l s e ca l l
II
next
f u n c t i on r e c u r s i ve l y t o d ra w t h e v i e w f rom
squa r e
to
the
ri ght :
e l s e d r awma z e ( x s h i f t - 1 , y s h i f t , v l e f t , v r i g h t , s c ree n ) ; } }
Ta u ri n g the P o l yg o n M a z e Yo u can try out the complete POLYMAZE. CPP program by CDing to the POLYGONS directory from the distribution disk that came with this book and typing POLYMAZE. Once the program is running, you can walk through the polygon-based maze the same way that you moved through the bitmapped maze in chapter 4, by using the arrow keys on the keyboard or moving the mouse. However, you'll notice that this maze is nowhere near as visually interesting as the bitmapped maze in chapter 4 was. The walls are a uniform green color and it's difficult to determine where one corridor ends and a side corridor begins. The output from this program is shown in Figure 5- 1 4. Must we sacrifice real ism in o rder to build a po lygo n-based maze? Not necessarily. There are several ways in which this maze could be made more interesting, though we don't have space in this book to try them all. One would be to assign different colors to different wall segments. The palette numbers for these colors could be placed in the maze[} array instead of the Os and ls that we've been using since chapter 2. Or we could change the intensity of the wall colors depending on how much light is falling on them. If we assume that all light comes from a torch or lantern being held by the player, walls at a greater distance from the player would be more dimly illuminated than walls that are closer, and wal ls turned at an angle to the p l ayer would be more dimly
CHAPTER FIVE
Po l y g o n Md zes
Figure 5-14 The video output from the program POLYMAZE.CPP
illuminared rhan walls rhat face rhe player head on. This technique is called lightsourcing, and we'll be srudying it in more derail in a larer chapter. For now, we'll concenrrate an yet anorher method of mak.ing rhe walls appear more interesting and real isric. This techn ique will allow us ro place acrual birmaps an rhe walls and rotate those bitmaps along with rhe walls. In rhe next chapter, we'll rake a closer Iook at texture mapping.
olid-colored polygons are boring. While they may be adequate for producing the distant scenery visible through the window of a simulated airplane, filled polygons just don't cut it as a method of drawing dungeon walls. Fortunately, solid colors aren't the only thing that we can fill polygons with. We can also fill polygons with bitmaps. If we want to create the illusion of an ivy-covered brick wall, we can fill our polygon-based dungeon walls with bitmaps depicting bricks and ivy. If we want to create a window that looks out of the dungeon omo the rolling countryside, then we can fill a polygon with a bitmap of a window. And so on. So how do we go about filling a polygon with a bitmap? Do we simply copy the bitmap byte by byte into the polygon's interior? Not quite. In order to make the illusion complete, the bitmapped images must appear to grow smaller as the polygons recede into the distance and !arger as we grow doser to them. And they must appear to rotate when the polygons rotate. Pulling off such an effect requires a bit more programming trickery than simply filling a polygon with a solid color. To graphics programmers, this trickery is known as texture mapping.
GARDENS O F I MAG I NATION
Two - a n d - a - H a lf- D i m e n s i o n a l Textu re M a p p i n g Mapping a bitmap onto a three-dimensional polygon is, in a sense, a two-step process. The bitmap must be scaled for distance, as the polygon grows larger and smaller, and the bitmap must be rotared when the polygon rotates. Scaling is the easier of the two steps. To scale a bitmap, all you need to do is selectively add and remove pixels as you copy the bitmap to the video buffer, thus making the image appear to grow larger and smaller. Rotaring the bitmap, on the other hand, can be a great deal more difficult, especially if the polygons on which you are mapping the bitmap can rotate on all three axes. Fortunately, as we noted in the last chapter, the polygons in a maze game don't really need to rotate on all three axes. They only need to rotate on their y axes. This gready simplifies the task of mapping bitmaps onto the polygons. In a sense, what we will be doing in this chapter isn't three-dimensional texture mapping. lt is two-and-a-half-dimensional texture mapping.
The Two - step P ro g ra m Because it is the simpler of the two steps, we'll starr by discussing the scaling of bitmaps. As n o ted above, this is a relatively simple matter of adding and subtracting pixels from the bitmap, ro make it appear to grow larger and smaller. The most difficult part of this process is deciding when to add pixels and when to subtract them. In a few specific instances, this decision is quite easy to make. If we wanted to double the size of a bitmap, for instance, we would simply repeat every pixel (and every line of pixels) twice. And ro halve the size of a bitmap, we would remove every other pixel (and every other line of pixels). But what do we do if we want to increase the size of a bitmap to, say, 1 37 percent of its original size? The solution is to turn ro our old friend incremental division. So far, we've used incremental division to draw lines and ro find the upper and lower edges of a filled polygon. Now we're going ro use it to scale bitmaps. To demonstrate, we'll create a simple program that will scale the familiar bitmap depicted in Figure 6- 1 . You'll find this image in the texture direcrory as M O NA . T G A . T h e b ackgro u n d over w h i c h i t w i l l be d i s p l ayed i s i n MONAB G . TGA.
196
t=
CHAPTE:R SIX
Texture M d p p i ng
B itma p sca l i n g The bitmap-scaling demo will begin with a few constant definitions: const
F RAMES_PER_S E C O N D = 2 ;
I I A n i ma t i on speed
const
I MAGE_X=2 5 ;
I I Loca t i on of b i tma p i n TGA
const
I M AGE_Y=2 1 ;
F I LE
const
I MAGE_W I DTH=76;
II W i d t h of b i tma p b e f o r e s c a l i n g
const
I MA GE_H E I G H T=96;
I I H e i g h t of b i t m a p b e f o r e s c a l i n g
const S C REENX=20;
I I S c reen
L o c a t i on f o r s c a L e d b i t m a p
We'll be creating a brief animation of sorts, so we first set the frames-per second rate. The IMAGE_X and IMAG E_Y constants represent the x , y coordinates o f the upper-left corner o f the Mona image i n the TGA file and the IMAGE_WIDTH and IMAGE_HEIGHT constants represent the width and height of that image. We'll pass this information to the grab() function (which we developed back in chapter 4) so that it can copy the bitmap into an array. Finally, we set a constant equal to the x coordinate at which we will be displaying the Mona bitmap on the screen. (We don't set a constant for the y coordinate because that coordinate will change as the bitmap is scaled.) We'll also need a couple of TGA structures, one to hold the image to be scaled and one to hold the background image upon which the scaling will take place. We'll call these structures mona and bg: tga_s t r u c t mona , bg
We also need an array in which to store the portion of the bitmap that we'll be "grabbing" from the mona structure: c h a r bi t ma p [ I MA G E_W I DTH* I MAGE_H E I GH T J ;
Fig u re 6-1 A fa m i l iar bitmapped image
GARDE:NS OF I MAG I NATI O N
The mainO function will begin with the usual folderol: setting u p a pointer to video memory and to an offscreen buffer, saving the old video mode, initializing the timer, and setring the video mode 1 3h. Then we'll load the two TGA images into their respective structures: if
( L oa dT G A ( " mona . t g a " , & t g a ) )
{
s e t mo d e ( o l dmod e ) ; exi t ( 1 > ; } if
< L oa d T G A ( " monabg . t g a " , &b g ) )
{
s e t mo d e ( o l dmod e ) ; exi t ( 1 >; }
We also need to set the palette, which raises an interesting question: Do we use the palette from MONA.TGA or the palette from MONABG .TGA? As it happens, either palette will do. Because these images are designed to be displayed together on the screen, it was necessary to use the same palette on both. For some tips on how to match palettes between two or more images using only the public domain PieLab program, see Appendix A. To set the palette in our program, we'll arbitrarily use the palette from MONA.TGA: s e t p a l e t t e ( mo n a . co l o r�a p ) ;
We'll cominue scaling the image umil somebody presses a key: wh i l e
( ! kbh i t ( ) )
{
B u t before we begin, we'll want to copy the background image (which is comained in the image field of the bg structure) into the screen buffer: for
( un s i gned i n t
i =O ;
i 1 0 0 >
{
y e r r o r -= 1 00 ;
If the error term is equal to or greater than 1 00, we'll subtract 1 00 from it and start drawing the row. We use a whileO Statement here rather than an ij(), so that the loop will be execured more than once if yerror is still greater than 1 00 after 1 00 has been subtracted from it. This will only happen if percent is greater than 1 00 and will cause the pixel to be drawn more than once in succession. This will make the pixel appear wider than it normally would, increasing the vertical size of the drawing (which is appropriate when the percentage is greater than 1 00) . Before we can begin drawing pixels, there are a few details to take care of. The first is to record the value of scrptr (which, as you recall, contains the current pixel offset in the screen buffer) in a variable called old_scrptr. We'll use this value later to calculate the offset for the next row of pixels to be drawn, as you'll see in a moment: u n s i g n e d i n t o l d_s c r p t r= s c r p t r ;
We'll also need to save the value of bitptr, which contains the current offset in the pixel buffer, in a variable called old_bitptr: u n s i g n e d i n t o l d_b i t p t r = b i t p t r ;
The H o ri z o nta l E rro r Te rm Just as we used an error term to determine which pixels in a row are to be repeated and which are to be skipped, we'll also use an error term to determine
CHAPTER SIX
Texture M d p p i nq
which columns of pixels are to be repeated and which are to be skipped. Because it will be used to scale the image on its x axis, we'll call this integer variable xerror: i n t xer ror=O;
We'll use a forO loop to irerate through all of the pixels in the row: for
( i n t x=O;
x < I MA G E_W I DT H ;
x++)
{
On each iteration, we'll add 1 00 to xerror to determine if we should draw the next pixel. If xerror is greater than 1 00, we'll draw the pixel and subtract 1 00 from xerror - and we'll continue drawing the pixel and subtracting 1 00 until xerror is less than 1 00: x e r ror+=pe r c e n t ; wh i l e ( x e r ro r > 1 0 0 )
{
x e r ror-= 1 00;
D raw i n g a Pixel After one heck o f a lot o f preparation, we are finally ready to draw the next pixel. This is a simple matter of copying the pixel value at offsec bitptr in the bitmap array to the position in the screen buffer pointed to by scrptr: sc reen_bu f f e r [ s c r p t r J=mona . i ma g e [ b i t p t r J ;
That being done, we can increment scrptr to point to the position at which the next pixel will be drawn and end the innermost whileO loop, the one that repeats the pixel drawing if the scaling percentage is greater than 1 00: }
sc rpt r++;
We deliberately do not increment bitptr to point at the next pixel in the bitmap buffer, because we may need to draw the current pixel more than once. As soon as the innermost whileO loop stops executing, however, we move bitptr forward by one and terminate the forO loop that draws the row of pixels: }
b i t p t r++;
The current row of pixels has now been drawn. We'll advance scrptr to the next row of pixels on the display. The easiest way to do this is to take the offset of the first pixel in the row j ust finished and add 320 (the width of the display) to it. Fortunately, we stored the offset of the first pixel of the row in the variable
old_scrptr:
G A R D E: N S OF I MAG I NATI O N s c r p t r=o l d_s c r p t r + 3 2 0 ;
If the current row of pixels in the bitmap buffer is being drawn more than once (that is, if the value of percent is greater than 1 00), we'll need to reset bitptr back to the first pixel in the row. We've stored the offset of that pixel in the variable old_bitptr: b i t p t r =o l d_b i t p t r ; }
And that ends the whileO loop that draws the rows of pixels, repeating them if necessary. We now must advance bitptr to the next line of pixels in the bitmap and end the for() loop that iterates through all of the rows of the image. Advancing bitptr is easy, since we've just reset it to the beginning of the last row. All we need to do is add the width of the bitmap to it: b i t p t r += I MA G E_W I DT H ; }
The scaled bitmap has now been drawn, but we'll need to move the contents of the screen buffer into video memory so that the user can see the drawing. Then we'll close all outstanding loops: b l i t s c r e en ( s c r e e n_bu f f e r ) ; } }
T h e B ITSCALE . C P P P ro g ra m There's nothing left to do at this point except restore the old video mode and end the program. We've done this in so many programs now that we don't need to show the details. The complete text of BITSCALE.CPP, however, appears in Listing 6- 1 :
I I B I T S C A L E . C P P V e r s i on 1 . 0 II
S c a l e s a b i t m a p on t h e mode 1 3 h d i s p l a y
II I I W r i t t e n b y C h r i s t o p h e r La m p t on II
for
G A R D E NS
OF
# i n c l ud e
< s t d i o . h>
# i n c l ud e
2
IMAGINATION
( Wa i t e G r oup P r e s s )
CHAPTER SIX # i n c l ud e
< c on i o . h>
# i n c l ud e
< s t d l i b . h>
# i n c l ude
# i n c l ude
" s c reen . h "
# i n c l ude
" t a rg a . h "
# i n c l ude
" b i tma p . h "
#def i ne
F R A M E $ P E R_S E C O N D 2
const
Texture M ö p p i nq
I MA G E_X =2 5 ;
c o n s t I MA G E_Y=2 1 ; const
I MA G E_W I DTH=76;
const
I MAGE_H E I GHT=96;
const
S C R E E NX=20;
const
S C R E E N Y=O;
L o n g t i c k s_ p e r_f r a m e ;
I I Number II
of
c l o c k t i c k s per
f r ame o f a n i ma t i on
t g a_s t r u c t mon a , bg ; c h a r b i t ma p [ I MAGE_W I DTH* I MAGE_H E I G H T J ; voi d ma i n O { II
C r e a t e po i n t e r
to v i deo memo r y :
c h a r f a r * s c r een= ( c h a r f a r * ) MK_F P ( OxaOOO , O > ; I I S a v e p r e v i ous v i deo mode : i n t o l dmode=* ( i n t * ) MK_F P ( O x 4 0 , 0 x 4 9 ) ; I I C r e a t e o f f s c r een v i deo b u f f e r : c h a r f a r * s c r e e n_bu f f e r =new u n s i g n ed c h a r [ 64000 ] ; I I P u t d i s p l a y i n mode 1 3 h : s e t mode ( Ox 1 3 ) ; I I S e t numbe r o f t i c k s p e r f r ame : t i c k s_ pe r_f rame = C L K_T C K I II
F RA M E S_PER_S E C O N D ;
I n i t i a l i z e t h e f rame t i me r :
c l o c k_t
L a s t f rame=c l o c k ( ) ;
I I Load bi t m a p f rom TGA f i l e : if
< L oadTGA ( " mona . t g a " , &mona ) )
{
s e t mod e ( o l dmode ) ; exi t < 1 > ; } if
( L oa d TGA ( "monabg . t g a " , &bg ) )
{
s e t mod e ( o l dmode ) ;
continued on next page
GARDENS O F I MAGI NATI ON continuedfrom previotts page
ex i t ( 1 ) ; } II
Set
pa l e t t e
to
TGA pa l e t t e :
s e t p a l e t t e ( mona . c o l o r_ma p ) ; II
Keep
whi le II
L oop i ng un t i l
( ! kb h i t ( ) )
somebody h i t s a
ke y :
{
C o py ba c kg round i ma g e t o s c r een bu f f e r :
for
( u n s i g ned
int
i =O ;
i 1 00 )
rows to be d r a wn :
{
I I R e s e t e r ro r t e r m : x e r ror-= 1 00 ; II
Copy n e x t p i x e l
f r om b i tmap to s c reen :
s c reen_bu f f e r [ s c r p t r J =mona . i ma g e [ b i t p t r J ; II
Po i n t t o n e x t p i x e l o n s c reen :
s c r pt r++; } II
Po i n t
to n e x t p i xe l
i n b i t ma p :
b i t p t r++; } II
Po i n t t o next
row on s c r e e n :
s c rpt r=o ld_s c rpt r+320; II
Repeat
s a me
row i n b i t ma p :
bi t p t r=o l d_b i t p t r ; } I I Po i n t t o n e x t
row
i n bi tmap :
b i t p t r+= I MA G E_W I DT H ; } I I Move s c reen bu f f e r
to s c reen :
b l i t s c r ee n ( s c reen_bu f f e r ) ; } } wh i l e
C ! kbh i t ( ) ) ;
I I Re l e a s e memo ry d e l e t e bg . i ma g e ; de l e t e bg . c o l o r_m a p ; if
C bg . I D )
d e l e te tga [ i i J . I D ;
de l e t e mona . i ma g e ; de l e t e mona . c o l o r_ma p ; if
( mona . I D )
d e l e t e mona . I D ;
s c reen_buf f e r ; I I R e s t o r e o l d v i deo mode : se tmod e ( o l dmode ) ; }
Texture M d p p i n g
GARDENS O F I MAG INAT I O N
R u n n i n g the P ro g ra m To see B ITSCALE.CPP in action, go to the directory called TEXTURE and type B ITSCALE. You'll see Mona go from small to large, as in Figures 6-2a through c, then back to small again. To stop the process, hit any key, though the program won't acknowledge your request until it has run through a complete scaling cycle.
Text u re M a p p i n g Now that we've seen how to scale an image, we're ready to develop some more versatile texture- mapping code. S urprisingly, the complete texture-mapp ing function will use the same principles of bitmap scaling that we j ust used to create B ITSCALE.CPP to perform both the scaling and the rotaring of polygons. By limiting ourselves to a subset of all polygons, the way we did in the polygon drawing function in the last chapter, we will be able to reduce texture mapping to a simple scaling system, where the polygon is drawn as a series of vertical lines, each of which can be scaled individually.
Flgure 6-2a Grow i n g . . .
Figure 6-2b Growing . . .
Figure 6-2c Grown!
CHAPTE:R SIX
Text u re Mdppinq
The G ro u n d R u l e s Let's set out some ground rules for the type of texture mapping that we'll be doing in this chapter. First, we are only going to work with reetangular bitmaps. This may seem like an obvious Statement, since bitmaps are almost always rectangular, but in texture mapping there are some advantages to working with bitmaps that have nonreetangular shapes, especially if the polygon onto which the bitmap is being mapped has the same nonreetangular shape. We're not going to worry about such special cases in this book, however. Second, the polygons onto which we will be mapping these bitmaps will always have four vertices, as the polygons we generared in the last chapter did. And the left and right edges of those polygons will always be venical. Further, each corner of the bitmap to be mapped onto the polygon will correspond to an equivalent corner of the polygon. Thus the upper-left corner of the bitmap will correspond to the upper-left corner of the polygo n, and so forth. Figure 6-3 shows the relationship berween a reetangular bitmap and a four-sided polygon. The surprising thing about mapping bitmaps to this li mited subset o f polygons is that we don't need t o know anything abour the way in which the polygon has been rotared in order to map the texture onto it. All we need to know are the x,y coordinates of the four corners of the polygon. (In fact, we don't even need to know the x coordinates of the rwo lower corners of the polygon, j ust as we didn't need to know these coordinates when we were drawing filled polygons in the last chapter.) Once we know those coordinates, we can draw the polygon as a sequence of venical lines, as in chapter 5. But instead of drawing each line in a solid color, we will copy the bitmap pixels from the equivalent
Figure 6-3 Each of the corners of the bitmap will correspond to one of the corners of the polygon
GARDENS OF IMAG INATION
vertical line in the texture map, skipping or repeating pixels as necessary to match the length of the vertical line of pixels in the texture map to the lengrh of the vertical line in the polygon. In effect, we will be performing a separate scaling operation on each vertical line copied from the bitmap to the polygon. (We'll also be skipping or repeating entire vertical lines, as necessary to scale the texture horizonrally.) Not surprisingly, we'll use a pair of error terms to determine the precise amount by which the texture map needs to be scaled in the horizontal and vertical dimensions.
T h e Textu re - M a p p e d P o l yg o n F u n cti on To draw texture-mapped polygons, we'll write a function called polytextQ, which will be similar to the polydrawO function that we developed in the last chapter. This function will be placed in the file POLYTEXT.CPP. The prototype for the function, which will be placed in the file POLYTEXT. H, Iooks like this: vo i d po l y t e x t < t po l y t y pe * t p o l y , i n t L e f t x , i n t L e f t y, i n t r i g h t x , i n t r i g h t y , c h a r * s c reen )
Yo u'll note that these parame ters are s i m ilar to the ones passed to the polydrawO function in the last chapter. The integer variables leftx, lefty, rightx, and righty define the x,y coordinates of the upper-left and lower-right corners of the viewport that the polygon is to be clipped against. There's no color parameter passed to the function, because we won't be filling the polygon with a solid color. The description of the polygon itself is passed to the function in a structure of tpolytype. Sim ilar to the polytype structure that we used in the last chapter, tpolytype is defined in POLYTEXT. H like this: s t r u c t t po l y type { i nt x[4J; i nt y[4J; i n t w t ext ; i n t htext; c h a r * t e x t ma p ; }. ,
The x and y arrays work j ust like they did in the polytype structure, holding the x and y coordinates of the four corners of the polygon, starring at the upper-left corner and proceeding counterclockwise around the shape. The wtext and htext fields contain the width and height, respectively, of the reetangular bitmap to be mapped onto the polygon. The pointer variable textmap points at the array of type char that contains the bitmap to be mapped onro the polygon. The actual polygon drawing is handled in much the same way as it was in polydrawQ. However, there is additional code here to handle the texture mapping.
208
CHAPTE:R SIX
Textu re M d p p i ng
Since we've already covered the polygon-drawing algorithm in chapter 5 , we'll skip over that part of the function and discuss the texture-mapping additions. At the beginning of the function, we create a pointer into the texture map array, which will initially be set to 0: u n s i gned
int
tpt r=O;
We then check to see if the polygon crosses the left edge of the viewport, as in Figure 6-4. If it does, we clip it. However, in addition to clipping the polygon as we did in chapter 5 , we must also clip rhe texture - that is, we must reposition the tptr variable so that it points not at the first pixel in the texrure map, but at the first pixel that falls wirhin the viewport. To find the new position for tptr, we calculate three integer values: wtemp (which contains the width of the unclipped polygon) , ojftemp (which contains the width of the portion of the polygon that extends beyond the left edge of the viewport) , and ratio (which represents the ratio of the width of the polygon to the width of the texture to be mapped onto the polygon) : i n t w t emp=tpo ly-> x [ 1 J - t p o l y->x [ O J + 1 ; i n t o f f t emp= L e f t x -t po l y->x [ O J ; f l o a t x r a t i o= ( f l o at ) tpo l y->w t e x t / w t emp;
What are we going to do with these values? We need to calculate how much of the texture map will extend beyond the left edge of the viewport. Unfortunately, this isn't necessarily the same as the porrion of the polygon that extends beyond the left edge of the viewport. We'll be scaling the bitmap to fit the polygon, so one pixel in the bitmap may be equivalent to several pixels in the polygon - or vice versa. The ratio variable teils us what the actual scaling ratio will be. If it's
Figu re 6-4 A polygon overlapping the left edge of the viewport
GARDE:NS O F IMAG I NATI ON
less than 1 , then we will be increasing the size of the textute map to fit the polygon. If it's more than 1 , then we will be reducing the size of the texture map. For now, though, we'll j ust multiply ratio by the portion of the polygon that extends off the lefthand side of the viewport to get the number of bitmap pixels that would have been mapped into that porrion had we been drawing it. We can then advance tptr past this porrion of the bitmap: t p t r =o f f t emp*x r a t i o;
We must also subtract this value from the wtext field, which contains the width of the texture map, to get the clipped width of the texture map: t po l y->w t e x t -= t p t r ;
We then proceed t o clip the polygon itself, just as we did i n the polydrawQ function. Before we can begin the actual drawing, though, we'll pur the height and width of the textute map into a pair of integer variables, so that we don't need to continually reference the tpoly structure to get those values. (This may or may not speed up the program slightly, depending on how intelligently the compiler translates our high-level instructions into optimized machine code.) i nt
t h e i g h t = tpo l y-> h t e x t ;
i nt
t w i d t h = t po l y-> w t e x t ;
We also need to create a pair of error terms that will tell us when it's time to advance vertically and horizontally in the texture map. First we'll create the horizontal error term, which we'll call txerror: i nt
t x e r ro r =O;
Next, we'll initiate a forO loop that wil l step across the entire width of the polygon, j ust as in polydrawQ. However, at the head of this loop, we'll create a variable called t that will serve as a pointer into the texture map. (The tptr variable also serves this function, but we'll need to preserve the current value of tptr for later use.) i nt
t=tptr;
A s before, we'll clip the polygon against the top edge of the viewport. This time, however, we'll also need to clip the texture. First we'll calculate the ratio of the height of the bitmap to the height of the polygon: f l oa t y r a t i o= ( f l oa t ) tpo l y-> h t ex t / h e i g h t ;
Then we'll multiply this ratio b y the portion o f the polygon that extends above the top edge to calculate how much of the bitmap extends over that edge: t += ( L e f t y - y ) * y r a t i o ;
CHAPTE:R SIX
Texture Mdpping
After the clipping of the polygon and texture map are complete, we'll establish yet another error term to help us scale the texture in the y dimension: i n t t y e r ror= O ;
Now we begin the for() loop that draws the vertical column of pixels, just as we did in polydraw(). However, instead of setting each pixel in the column to a single color, we copy a pixel from the texture map into video buffer: s c r e e n [ p t r J = t po l y- > t e x t ma p [ t J ;
Should we now advance to the next pixel i n the texture map? That depends on the value of tyerror: whi le
( tyer rorx [ O J < L e f t x ) II i nt
i n t o t ex t u r e map a r r a y :
i n t t p t r=O;
po l ygon c r o s s e s
Left
s i d e o f v i ewpo r t ,
clip i t :
{
C a l c u l a t e o f f s e t of
c l i p i n t e x t u r e map a r r a y :
wt emp= t po l y-> x [ 1 J - t p o l y- > x [ O J + 1 ;
i n t o f f t emp= L e f t x- t po l y->x [ O J ; f l oa t x ra t i o= ( f l oa t ) t po l y->w t e x t l w t em p ; t p t r=o f f t em p* x r a t i o ; t p o l y->w t e x t - = t p t r ; II
C a l c u l a t e s l op e s f o r top a n d bot t om edg es :
t s l o pe = ( f l o a t ) ( t po l y->y[ 1 J - t po l y->y[ Q J ) I ( tpo l y-> x [ 1 J - t po l y-> x [ Q J ) ; bs l o pe = ( f l oa t ) ( t po l y->y[ 3 ] - t po l y->y [ 2 J ) I ( t po l y->x [ Q J - t po l y-> x [ 1 J ) ; II
F i n d new endpo i n t s
f o r c l i pped
L i nes :
t p o l y->y [ Q J = t po l y->y[ Q J + t s l o p e * ( L ef t x - t p o l y->x [ Q J ) ;
CHAPTE:R SIX
Texture M d p ping
t po l y->y [ 3 J = t po l y->y [ 3 J +bs l ope * ( l e f t x- t po l y->x[ Q J ) ; t p o l y->x [ O J = l e f t x ; } II
I n i t i a l i z e x , y coord i n a t e s f o r po l ygon d r a w i ng : i n t x=tpo l y->x [ O J ; i n t y= t po l y->y [ Q J ; II
Ca l c u l a t e t h e c h a n g e i n y coor d i na t e s f o r t op a nd bot tom edges : i n t topd i f f =tpo l y-> y [ 1 J - t po l y->y [ O J ; i n t bot d i f f =tp o l y->y[ 2 J - t po l y->y [ 3 J ; II
II
I n i t i a l i z e h e i g h t a nd w i d t h of c l i pped po l ygon : i n t h e i g h t = t po l y-> y [ 3 J - t po l y-> y [ Q J ; i n t w i d t h = t po l y-> x [ 1 J - t po l y->x [ Q ] + 1 ;
I I C l i p po l ygon w i d t h aga i n s t r i g h t s i d e of v i ewpo r t : i f ( t po l y-> x [ 1 J> r i g h t x > vw i d t h =w i d t h- < t po l y->x [ 1 J- r i g h t x ) ; e l s e vwi d t h =w i d t h ; II
I n i t i a l i z e h e i g h t a n d w i d t h of t e x t u r e ma p : i n t t h e i g h t = t po l y-> h t e x t ; i n t t w i d t h = t po l y->w t e x t ; I I I n i t i a l i z e top and bot t om e r r o r t e rms : i n t toper r o r =O; i n t bo t e r r o r =O; II I n i t i a l i z e hor i zo nt a l e r r or t e rm for t e x t u r e ma p : i n t t x e r ro r=O; I I Loop a c r oss w i d t h of po l ygon : f o r ( i n t w=O; wh t e x t l h e i g h t ; t+=( l e f t y-y) *yr a t i o ; } e l se { vy=y; v h e i g h t =h e i g h t ; } II
continued on next page
GARDE:NS O F I MA G I NATI ON continued.from previous page
if
( ( v y+vh e i g h t ) > r i g h t y ) v h e i g h t = r i g h t y-vy;
II
Po i n t
to v i deo memory o f f s e t
u n s i g n ed II
for
top of
L i ne :
i n t p t r=vy*32 0 + x ;
I n i t i a l i z e v e r t i c a l e r r o r t e rm f o r t e x t u r e ma p :
i nt
t y e r ro r = O ;
II
Loop t h rough
II
L i ne , adva n c i ng PTR t o t h e n e x t
I I each pixel fo r ( i n t h = O ;
t h e p i xe l s
in
t h e c u r r e n t ver t i c a l row o f p i x e l s a f t e r
i s d ra w n . h t ex t m a p [ t J ; II
Is
wh i l e
it
t i me t o move t o n e x t
( t ye r r o r < t h e i g h t )
row o f p i x e l s i n m a p ?
{
t ye r ro r+=he i g h t ; t += t w i d t h ; } t y e r ro r-=t h e i g h t ; pt r+=320 ; } x++; II
Adv a n c e x coord i n a t e
to next vert i c a l
L i ne :
t o p e r r o r+=a b s ( t o pd i f f ) ; II
If
whi le II
so move i t
up . . .
( t op e r ror>=w i d t h ) If
so move
it
{
up . . .
t op e r ror-=w i d t h ; if
( t opdi f f> O )
{
y++ ; -- h e i g h t ; } e lse { --y; h e i g h t ++; } } bo t e r r or +=a bs ( bot d i f f ) ; wh i l e
( bo t e r r o r >= w i d t h )
{
bo t e r ro r-=w i d t h ; if
( bo t d i f f > Q )
h e i g h t++;
e l s e --h e i g h t ; } II
Is
it
whi l e
t i me t o move t o n e x t p i x e l ( t x e r r or< t w i d t h )
t x e r ro r+= w i dt h ; t p t r++;
{
c o l umn
i n map?
CHAPTE:R SIX
Textu re M d p p i ng
}
t x e r ror-= t w i d t h ; } }
A Textu red Polygon Demo To test this function, we'll rewrite the POLYDEMO .CPP program from the last chapter as TEXTDEMO.CPP. The changes will be minor. We'll create a TGA structure to Ioad a bitmapped image into: p c x_s t r u c t wa l l ;
What bitmap will we use? Let's try using one of the bitmapped images from our earlier bitmapped maze program . Specifically, we'll use a portion of the image from FRONT l . TGA: i f C l oadTGA C " f r on t 1 . t g a " , & wa l l ) ) ex i t C 1 ) ;
Now let's allot some memory in the tpoly structure to store the portion of the bitmap that we'll be "grabbing" out of FRONT l .TGA: t po l y . t e x t map=new c h a r [ 9 5 *79 J ;
What portion o f the bitmap should we grab? Let's go for the portion that shows a complete wall segment: g r a b C 47 , 2 0 , 9 5 , 7 9 , pc x . i ma g e , t po l y . t e x t ma p ) ;
The rest of the code i s identical to that i n POLYDEMO. CPP, except that when we set up the tpoly structure to be passed to the textdemoO function, we must include values for the width and height of the bitmap: t po l y . w t e x t = 9 5 ; t po l y . h t e x t =79; po l yt e x t C & t po l y , 6 0 , 4 0 , 9 5 , 1 7 5 , s c reen ) ;
The TEXT D E M O P ro g ra m The complete text of the TEXTDEMO.CPP program appears in Listing 6-3.
# i n c l u de # i n c l u de # i n c l ude # i n c l ude
< s t d l i b . h>
conrimud on next page
GARDENS OF I MAG I NATION continu.�dfrom previous page
# i n c l ud e # i n c l u de # i n c l ud e # i n c l ud e # i n c l ud e
" screen . h " " po l y t e x t . h " " bresnham . h" "pcx . h " " b i tmap . h "
v o i d d r a wbox ( i n t L e f t x , i n t L e f t y , i n t r i g h t x , i n t ri g h t y , c h a r * s c reen ) ; p c x_s t r u c t wa l l ; c h a r f a r b i t ma p [ 9 5 * 79 J ; v o i d ma i n O { I I De c l a r e po l ygon s t r u c t u r e : t po l y t ype tpo l y ; II
if
Load wa l l b i t ma p : < L oa d T G A ( " f r on t 1 . t g a " , & w a l l ) ) ex i t < 1 > ;
II
A l l o t memo ry f o r w a l l b i t ma p : t p o l y . t e x tmap=new c h a r [ 9 5 *79 J ; I I G r a b w a l l i ma g e i n a r r a y : g r a b < 0 , 0 , 9 5 , 79 , p c x . i ma g e , t po l y . t e x t map ) ;
C r e a t e po i n t e r t o v i deo memo r y : c h a r f a r *sc reen= ( c h a r f a r * ) MK_F P < Oxa000, 0 ) ;
II
II
S a v e p r evi ous v i deo mode : i n t o l dmode=* ( i n t * ) MK_F P ( Ox40,0x 49 ) ;
II
Put d i s p l a y i n mod e 1 3 h : se t mode < O x 1 3 ) ;
II
S e t pa l e t t e t o I C E MAP pa l e t t e : s e t p a l e t t e ( p c x . pa l e t t e ) ;
I I C l e a r d i sp l a y : c l s ( s c reen ) ; I I D r a w box o n d i s p l a y : d r a w bo x ( 60,40 , 9 5 , 1 7 5 , s c r ee n ) ;
t po l y . x [ O J = 5 0 ; t po l y . y [ Q J = 50; t po l y . x [ 1 J = 1 00; t po l y . y [ 1 J = 1 0; t po l y . x [ 2 J =1 00 ; t p o l y . y [ 2 J = 1 90; t p o l y . x [ 3 J=50; t p o l y . y [ 3 J=1 50; tpo l y . wt ext=95;
2:16
CHAPTER S I X
Texture M d p p i nCJ
t po l y . h text=79; po l y te x t < & t p o l y,60,40,9 5 , 1 7 5 , s c r ee n ) ; I I Ho l d po l ygon o n s c reen un t i l keys t r oke w h i l e C ! kb h i t ( ) ) ; I I Re l e a s e memo r y de l e t e wa l l . i ma g e ; de l e te wa l l . c o l o r_ma p ; i f ( wa l l . I D ) d e l e t e wa l l . I D ; de l e t e tp o l y . tex tm a p ; I I Restore o l d vi deo mo de : se tmode < o ld mod e ) ;
} vo i d d r awbo x < i n t l e f t x , i n t l e f t y , i n t r i g h t x, i n t r i g h t y , cha r * s c reen) {
l i ne d r a w ( l e f t x , l e f t y, r i g h t x , l e f t y , 1 5 , s c r ee n ) ; l i n edr a w ( r i g h t x , l e f t y , r i g h t x , r i g h t y , 1 5 , s c r een ) ; l i n edr a w ( r i g h t x , r i g h t y , l e f t x , r i g h t y, 1 5 , s c r ee n ) ; l i nedra w ( l e f t x , r i g h t y , l e f t x , l e f t y , 1 5 , s c r een ) ; }
R u n n i n g the P ro g ra m To run TEXTDEMO.CPP, go to the TEXTURE directory and type TEXTDEMO. A texture-mapped polygon will appear on the display (see Figure 6- 5 ) . Try changing the polygon and cl ipping parameters and recompiling the demo. If you're really brave, try loading a diffe rent bitmap and mapping it onto the polygon.
Figure 6-5
A texture-ma pped polygon as drawn by
TEXTDEMO.CPP
GARDE:NS OF IMAGI NATI ON
A Text u re - M a p pe d M a z e Now that we have a function to draw texture-mapped polygons, ler's plug it into the P O LYMAZE program from the last chapter. The main diffe rence between the program TEXTMAZE and the earlier program POLYMAZE is that a textute m ap is loaded i n during initial ization (the same one we used in TEXTDEMO. CPP) and the polytextO function is called instead of polydrawQ. In addition, there is a new function in this program called drawbgQ. Ir draws a background image behind the textured polygons. You' ll recall that the maze images back in chapter 4 contained not only pictures of rhe walls, but of the floor and sky as well. The background image that we'll use for the drawbgQ function will come from the file EMPTY.TGA. Ir is the image of an empty maze, with only fl oors or ceili ngs. We'll copy this into the viewport with the drawbg() function, then draw the texture-mapped polygons on top of it: voi d d r a wbg ( i n t num, c h a r far * s c r e e n ) {
uns i gned i n t o f f s e t 1 =Y W I N D OW*320+XW I N D OW; u n s i g n e d i n t o f f s e t 2 =0; for ( i n t i =O ; i " screen . h "
C HAPTER SIX # i n c l ude # i n c l ude # i n c l ude # i n c l ude # i n c l ude # i n c l ude
" i o . h" "evntmgr1 . h " " b i tmap . h " " p c x . h '' " t a r ga . h " " po l y t e x t . h "
#def i n e #def i n e
FRAMES_PER_S E COND 5 W H I C H_EVENTS KEYBOARD_EVENT S+MO U S E EVENTS
Texture M d p p i ng
I I F u n c t i o n p rototype s : vo i d d i s p l a y_s l i c e ( i n t i ma g e num, i n t l e f t x , i n t r i g h t x , c h a r f a r * s c r e en ) ; voi d drawb g ( i n t num, c h a r f a r * s c reen ) ; voi d d r awma z e ( i n t x s h i f t , i n t ys h i f t , i n t l v i ew , i n t l r vi e w , c h a r * s c r ee n ) ;
con s t VI EW_L I M I T
cons t con s t con s t const
=
4 ; I I Number o f squa res v i s i b l e b o t h I I t o rwa rd a nd to t h e s i d e . I I ( S hou l d not be c h a n ged w i t h out II a d d i ng add i t i o na l b i tmaps . )
X W I NDOW 1 1 0; Y W I N DOW = 0; W W I DTH = 1 88 ; W H E I GHT 1 20; =
II
Upper l e f t c o r n e r of v i e w w i ndow
=
I I B u f f e r s f o r compa s s p i c t u r e a nd b a c k g round : p c x_s t r u c t compa s s e s , p c x ; t ga_s t r u c t bg [ 2 J , wa l l ; c h a r f a r b i tmap[ 95*79 J ; I I A r ra y of compa s s f a c e s : u n s i gned c h a r f a r * c ompa s s_f a c e [ 4 J ; II
MAP O F T H E MA Z E
II II
E a c h e l ement r e p r e s e n t s a p h y s i c a l squa re w i t h i n t h e ma z e . A va l ue o f 0 means t h e squa r e i s empty, a nonz e ro va l u e I I means t h a t t h e s q u a r e i s f i l l e d wi t h a s o l i d c u be . Map I I must be d e s i gned so t h a t no squa re can be vi ewed over a I I g r e a t e r d i s t a n ce t h a n t h a t def i n e d by V I EW_L I M I T . II
u n s i g n ed c h a r maz e [ 1 6 J [ 1 6 J = { {1,1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ,1 ,1 ,1 }, { 1 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 , 0,0 , 1 } , { 1 ,0, 1 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 ,0 , 1 , 1 ,0 , 1 } , { 1 ,0 , 1 , 1 , 1 , 1 , 1 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 1 , 1 } , {1 ,0,0,0,0, 1 , 0 , 0 , 0 , 0 , 1 , 0 , 1 ,0 , 1 , 1 } , { 1 , 0 , 0 , 0 , 0 , 1 , 0, 1 , 1 , 1 , 1 , 0 , 0 , 0 , 1 , 1 } , { 1 , 1 , 1 , 0 , 0 , 1 , 1 , 0 , 1 , 0 , 0 , 1 , 1 , 0, 1 , 1 } , { 1 ,0 , 1 , 0 , 1 , 1 , 1 , 0 , 0 , 0 , 1 , 0 , 0, 0 , 1 , 1 } , { 1 ,0, 1 , 0 , 0 , 0 , 1 , 1 , 1 , 0 , 0 , 0 , 1 , 1 , 1 , 1 } ,
continued on next page
G A R D E N S OF I MAGI NATION continu•dfrom previous pag•
}.
{ 1 , 0 , 0 , 1 , 1 , 0, 1 , 0 , 1 , 1 , 1 , 0 , 1 , 1 , 1 , 1 } , { 1 , 1 , 0 , 0 , 0 ,0, 1 , 0 , 1 , 0 , 0 , 0 , 0 , 0, 1 , 1 } , { 1 , 0 , 0 , 0 , 1 , 1 , 1 , 0, 1 , 0, 1 , 1 , 1 , 0, 1 , 1 } , { 1 , 0 , 1 , 1 , 1 ,0,1 ,0,0,0, 0,1 ,0,0,0, 1 } , {1 ,0,0, 0 , 1 ,0, 1 , 1 , 1 , 1 ,0,1 ,0,0,0, 1 } , { 1 , 1 ,0,0,0, 1 , 1 ,0,0,0, 0 , 1 ,0,0,0, 1 } , {1,1,1,1 ,1,1,1,1,1,1,1,1,1,1,1,1}
,
t y p e d e f s t r u c t xy { i n t x,y; }; I I D i r e c t i o na l i n c r e me n t s f o r no r t h , ea s t , s o u t h , w e s t II respe c t i ve l y : s t r u c t x y forwa r d [ 4 J = { { -1 , 0 } , { 0 , 1 } , { 1 , 0 } , {0,-1 } } ; s t r u c t x y L e f t [ 4 J = { { 0 , - 1 } , { -1 , 0 } , { 0 , 1 } , { 1 , 0 } } ; s t r u c t xy r i g h t [ 4 ] = { { 0 , 1 } , { 1 , 0 } , { 0 , - 1 } , { -1 , 0 } } ; I I V i e w e r ' s s t a r t i n g po s i t i on : s t r u c t x y pos= { 3 , 8 } ; I I S t r u c t u r e to h o l d po l ygen da ta : t po l y t y pe t po l y ; I I Ho r i zo n t a l s c reen pos i t i on s c o r respond i n g to I I v i s i b l e i n t e r s e c t i o ns on t h e m a z e g r i d : i n t ma t r i x [ VI EW_L I M I T +2 J [ VI EW_L I M I T *2+2 J={ 0, 0, 0, 0, 0}, { 1 87, 1 87, 1 87 , 1 87 , 1 87, 0}, 0, 0, { 1 87 , 1 8 7 , 1 8 7 , 2 38, 1 4 3, 4 7 , -49, 0}, 0, 0, { 1 87 , 1 8 7 , 1 87 , 1 79 , 1 2 3 , 6 6 , 1 0 , 0}, 0, 0, { 1 87 , 1 8 7 , 1 87 , 1 5 8 , 1 1 7 , 7 3 , 3 1 , { 1 87 , 1 8 7 , 1 77, 1 4 4 , 1 1 1 , 7 8 , 4 5 , 1 2 , 0 , 0 } , 0} 4, { 1 87 , 1 8 5 , 1 59 , 1 3 3 , 1 07 , 8 2 , 5 6 , 30, }; i n t t o p [ V I E W_L I M I T+ 2 ] = { - 1 9 , 2 0 , 3 6 , 4 2 , 4 6 , 4 9 } ; i n t bot t om [ V I EW_L I M I T+2 J = { 1 3 8 , 9 9 , 83 , 7 7 , 7 3 , 7 0}; i n t d i r=1 ;
II V i e w e r ' s c u r r e n t he a d i ng in ma ze, II w h e r e O=n o r t h , 1 =e a s t , 2= s ou t h , 3=we s t
L e n g t i c k s_ pe r_f r a m e ; I I Numbe r of c l o c k t i c k s per II f r ame o f a n i ma t i on vo i d ma i n ( vo i d ) { e v e n t_s t r u c t ev e n t s ; s t r u c t xy newpo s ; I I C r e a t e po i n t e r t o v i deo memo ry : c h a r f a r * s c reen= ( c h a r f a r * ) M K_F P < O x a000,0 ) ; I I C r e a t e of f s c reen v i deo b u f f e r : c h a r f a r *sc re en_b u f f e r =new u n s i g n ed c h a r [64000 ] ;
CHAPTE:R SIX
Textu re M d p p i ng
I I Load compa s s p i c t u r e : i f ( L oadPCX ( " c ompa s s . p c x " , & compa s s es ) ) ex i t ( 1 ) ; I I Load comp a s s f a c e s i n to a r ra y : for ( i n t num=O ; num3 ) d i r=O; bgnum--; i f C bgnum 1:: .
;iHt
Ll!.J' ii"
-: .[:. I.
I'
!-
I !1:iF
l� dL..t !.L
-
�-�w...Jt# ��t-"'=
-8 9.
'' ·
;,
11
..
•, ·...
A polar coord inate system superimposed
over a Cartesian coordinate svstem
We can calculate the magnitude of the vector using the Pythagorean method that we used earlier. Once again, however, it's not necessary to first subtract the starring from the ending coordinates of the line to get its lengrh, because it starts at 0,0. So we can j ust use the x,y coordinates of the point as the x,y lengths of the line. We can measure the distance from the origin to the point at 7,5 with the calculation di stance
=
sq r t ( 7 *7+ 5 * 5 )
or d i s t a n ce
sq r t ( 4 9+2 5 )
or d i stance
s q r t ( 74 )
or d i s t a nce
=
8 . 602
Thus the magnitude of the vector between the origin and the point at 7,5 is 8. 602. And the direction of the vector, expressed in terms of the Cartesian coordinate system, is 7,5. Thus we can describe the point in polar terms as being at a distance of 8. 602 from the origin along a vector of 7,5. This may seem like
GARDENS OF I MAG I NATION
,II
J.
-7
-6
-5
I
-4 -3 -2
JJ tt-J t
I
-2 -3
-4
I I
-9
2 3 4 5
I
'
6 J
8
l, I.
'-f'--+---+-1-i�---
!:�
�±
Figure 7-23
-1 -I
�
.._
:Jtc·-+-�-� -..J�-!-
A point at coord i n ates 7,5 from the origin
an unnecessary amount of information, since merely the numbers 7,5 suffice to express the position of this point wirhin a Cartesian coordinate system. Bur bear in mind that we can also express the vector as any eq uivalent ratio: 1 4, 1 0 or 28 ,20 or even 3 . 5 , 2 . 5 . And this flexibility allows us to perform translations from vectors to angles. To see how, let's Iook at the concept of the unit vector.
T h e U n it vecto r A line can have any positive magnitude and any possible direction vector. There is, however, one special direction vector associated with a special length of line (or portion of a line) . It is the vector measured along a single Cartesian unit of the length of a line and it is known, logically enough, as the unit vector. lt has special properties, a few of which we are going to mention here. Any vector can be converted into a unit vector simply by dividing both the x and y components of the vector by the magnitude of the line along which the vector was originally measured. This is called normalizing the vector. lt is clone like this: no rma l i z ed_x x I ma gn i t ud e ; no rma l i z ed_y = y I ma g n i t ud e ; =
where x and y are the x and y coordinates of the end of a line that starts at the origin, magnitude is the distance from the origin to the end in Cartesian units,
CHAPTE:R SE:VEN
Spdce: The Findl Frontier
-1 Figure 7-24
A u n it vector of .81 376, .58126
and normalized_x and normalized_y are the normalized coordinates of the line's direction vector. For instance, the vector to the point in Figure 7-23 was 7,5 and the magnitude of the vector was 8.602, so we can normalize this vector by dividing both the x and y components of the vector by 8.602, like this: no rma l i zed_x = 7 I 8 . 602; no rma l i zed_y 5 I 8 . 6 02 ; =
If you performed these calculations, you would learn that the unit vector for the line that goes from the origin of the Cartesian coordinate system (or, for that matter, any Cartesian coordinate system) to the point at coordinates 7 , 5 is . 8 1 376, . 5 8 1 26. (See Figure 7-24.)
The U n it C i rc l e l f we draw all the possible points that are one unit away from the origin of a Cartesian coordinate system, as in Figure 7-25 , they will form a neat circle around the origin. The circle is called a unit circle, because its radius is precisely one Cartesian unit. Every possible line extending one or more units from the origin crosses the unit circle. And if we measure that vector of a line along the portion of the line that extends from the origin to the unit circle, we will obtain - obviously - a unit vector. If you think about it for a second, you'll see that
GARDENS OF I MAG I NATION
Figure 7-25
A u n it circle araund the origin of a Cartesian
coord i nate svstem
the x,y components of this unit vector are also the Cartesian coordinates for the point at which the line crosses the unit circle.
Rotati ng a P o i nt For those of you who didn't fall completely asleep during the foregoing hurst of heavy mathematics, I have a (possibly) unexpected gift. I am going to show you how to use the coordinates at which a line crosses the unit circle to create your own three-dimensional world. This may seem like a pretty spectacular bonus to receive from a pair of numbers in a Cartesian coordinate system, but the concept of the normalized coordinates of a unit vector are at the heart of every ßight simulator and virtual reality program on the market. The normalized coordinates of a vector - the coordinates at which the vector crosses the unit circle - can be used to translate back and forth between angles measured as degrees (or radians) and Cartesian direction vectors. So special are the x and y components of the normalized direction vector that they have been given names: They are the cosine and sine, respectively, of the angle at which the vector extends from the origin. Even if you slept through trigonometry (if not through the first part of this chapter) , you probably remernher the terms cosine and sine. However, you p robably remernher them in terms of right angles . After al l , the very term
C HAPTER SEVEN
Spdce: The Findl Fro ntier
"trigonometry" comes from an outmoded word meaning "triangle. " And, indeed, the sine and cosine we have stumbled upon at the point where a vector crosses the unit circle are the selfsame sine and cosine that you learned about in trig dass. In fact, if you remernher enough of your trig, you can probably see how these rwo different ways of looking at sine and cosine are actually the same, since it's pretty easy to find right triangles in a Cartesian coordinate system (as we did a moment ago, when we used the Pythagorean theorem to calculate the magnitude of a vector) . However, we're not going to discuss the theory of sines and cosines at any greater length in this chapter - you've had enough mathematics to stupef)r a rhinoceros. Standard mathematical algorithms for calculating sines and cosines can be found in any book on trigonometry. We won't be going into them here because our CIC++ compiler is supplied with Standard library functions (the protorypes for which are in the header file MATH.H) fo r calculating rhem. These library functions are slow, but they're good enough for the demonstration programs that we'll be writing in rhis book. Later we'll develop our own high-speed sine and cosine routines (though we won't use rhe Standard mathematical algorirhms for doing so) . For our purposes, the most important rhing about sines and cosines is that rhey allow us to translate from an angle to Cartesian coordinates in space. And this in turn will allow us to take a point at a given set of coordinates relative to the origin and rotate it araund the origin by any number of degrees or radians. (As it happens, the Standard library sine and cosine functions that come with our CIC++ compiler expect an angle to be measured in radians.) And this leads us to rhe topic with which we will finally wrap up this chapter on mathematics: coordinate transformations.
coord i nate Tra nsfo rmations We will generally deal with three- (and rwo-and-a-half-) dimensional scenes i n terms of rhe Cartesian coordinates o f points wirhin rhose scenes. As we saw earlier in this chapter, a polygon-fill flight simularor deals with three-dimensional scenes in terms of the three-dimensional x,y,z coordinates of rhe vertices of polygons. As we'll see in rhe next chapter, a maze can be dealt with in terms of the rwo-dimensional x,y coordinates at which walls and objects are placed wirhin rhe maze. In both Aight simulators and maze games, the secret of creating the illusion of movement is to move a point realistically in rwo and three dimensions. Moving a p o i n t w i r h i n two- and t h ree - d i m e n s i o n a l coordin ate systems is called transforming the point. There are primarily rwo ways in which we will want to transform points: the first is to translate them and rhe second is to rotate rhem.
2 55 ____.
G A R D E: N S OF I MAGI NATION
Tra n s lating a Point
Translating a point means, simply, to move it from one place in the coordinate system to another along a straight line. Generally, a transformation is expressed in terms of how many Cartesian units we want the x and y coordinates of the point to change during the translation. Actually translating a point is a simple matter of adding these values to the current x,y coordinates of the point, like this: n e w_x n e w_y
= =
o l d_x + x_t r a n s ; o l d_y + y_t r a n s ;
where old_x, old_y are the original coordinates of the point, trans_x, trans_y is the d i s tance that we wish to m ove the p o i n t from i ts original posi tion, and new_x, new_y are the coordinates of the point after the translation. For instance, in Figure 7-26, the point 7,8 has been translated by -2,3 units to 5, 1 1 . Rotati n g a Point
Rotaring a point is somewhat more complex than translating it. However, there are Standard formulas for doing so, based on the sine and cosine functions. Here, for instance, is the formula for rotaring a point to a given angle relative to the origin of a Cartesian coordinate system: n e w_x n e w_ y
= =
o l d_x * c o s i n e ( ANG L E ) - o l d_y * s i n e ( AN G L E ) ; o l d_x * s i n e < ANGLE ) + o l d_ y * c o s ( AN G L E ) ;
where old_x, old_y are the original coordinates of the point, ANGLE is the angle that we wish to rotate it araund the origin (expressed in whatever unit the sine 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 0
I
I
I.
l J. j_IU._j
' ····· ··
�0 (5, 1 1 ) I motion is line Trunslated = �t l ;=:b_ of ._j:±t (7,8) :--, f:J...J......L.... .. f-LJ...J f-LJ J �: t_LJ1 J I I -i
\
·
-
-2,3
!--
Figure 7-26 256
I. .l. 1, I L 1i l
...J J J� ::._j I t 1
· j,
>
.. ,
...
5
6
8 .9 1 0 1 1 1 2
Translating a point
l-5
C HAPTER SE:VEN
Spdce: The Findl Frontier
and cosine functions are expecting) , and new_x, new_y are the coordinates of the point after the rotation. This rotation is always performed araund the origin of the system. If you wish to rotate a point araund some point other than the origin of the system, you must first translate that point relative to the origin by the amount that the center of the rotation is displaced from the origin. That's a simple matter of subtracting the coordinates of the center of rotation from the coordinates of the point (or adding the negative of the coordinates, which comes to the same thing) . Then perform the rotation as described above and add the displacement back to the newly rotared coordinates. Here is one way in which this might be clone: t emp_x = o l d_x - c e n t e r_y; t emp_y = o l d_y - c e n t e r_y; new_x = temp x * co s i n e C AN G L E )
- o l d_y * s i n e C AN G L E ) ;
new_y = t emp_x * s i n e C ANGLE ) + - o l d_y * cos C A N G L E ) ; new_x += c e n t e r_x; new_y += c e n t e r_ y ;
where center_x and center_y are the coordinates of the point araund which we wish to rotate the point at old_x,old_y by AN GLE, and temp_x, temp_y are temporary value holders. The rotared position of the point will once again be at
new_y, new_x. Three -Dimensional Rotation
We can also rotate a point in a three-dimensional coordinate system, but this is really j ust a matter of performing three separate two-dimensional rotations. There are three different axes in a three-dimensional coordinate system - x, y, and z - and any two of these can be said to describe a two-dimensional plane. When we perform a two-dimensional rotation, as we did above, we are rotaring the point in the x,y plane, that is, the plane formed by the x and y axes. But we can also rotate points in the x,z plane (the plane formed by the x and z axes) and the y,z plane (the plane fo rmed by the y and z axes) . To rotate an object three dimensionally, we must rotate it separately in each of these planes. (We can also think of this as rotaring the point around each of the three coordinate axes, where rotation in the x,z plane is rotation araund the y axis; rotation araund the x,y plane is rotation araund the z axis; and rotation araund the y,z plane is rotation around the x axis.) The amount that this point is to be rotared should be expressed by three angles: the angle for rotation araund the x axis (rotation in the y,z plane), the angle for rotation araund the y axis (rotation in the x,z plane), and the angle for rotation araund the z axis (rotation in the x,y plane) . Here is a fo rmula for rotation around the x axis:
GARDENS OF IMAG I NATI ON
n e w_y new_z
=
=
o l d y * cos i ne ( ANGLE ) o l d_y * s i n e C AN G L E )
- o l d_z * s i n e C ANGLE ) ;
+ o l d_z * co s i n e C A NGLE ) ;
n e w_x = o l d_x ;
Here is the formula for rotation around the y axis: new_z
=
o l d_z * co s i n e ( ANG L E )
new_x
=
o l d_z * s i n e C AN G L E )
- o l d_x * s i n e C ANGLE ) ;
+ o l d_x * cos i n e C A NGLE ) ;
new_y = o l d_y
And here is the formula for rotation around the z axis: n e w_x
=
o l d_x * c o s i n e C A N G L E ) - o l d_y * s i n e C ANG L E ) ;
new_y = o l d_x * s i n e C AN G L E ) new_z
=
+ o l d_y * c o s i ne C A NGLE ) ;
o l d_z ;
You'll notice that the formula for rotation around the z axis is identical to our earlier formula for two-dimensional rotation, since this is also rotation in the x,y plane that we normally use for two-dimensional Cartesian coordinates.
Let ' E r R i p That's enough mathematical background for now. We'll pick up additional math as we go along, building on the concepts that were introduced in this chapter. Now that we have the ability to translate points in two- and three-dimensional coordinate systems, we can starr writing some two-and-a-half-dimensional maze generation code, which will simulate three-dimensional motion through a maze without quite going all the way to the third dimension. In the next chapter, we'll stumble our way into the newest, most exciting frontier of all: ray-cast graphics.
n the more than two decades since Nolan Bushnell unleashed Pong on an unsuspecting world, video games have become increasingly more realistic. Pong featured a large white pixel being batted back and forth by a pair of horizontal lines, in a crude simulation of Ping-Pong. If the game were rereleased today, it would probably feature a pair of animared players knocking a high resolution ball back and forth across a texture-mapped table. Two decades from now, a halogram of the two players may pop out of the computer and into your living room. Watch out for that backstroke! Maze games have not escaped this tidal wave of increased realism. Players demand maze graphics that are both vivid and fast. The challenge facing us as programmers is to come up with a method for depicting exceptionally vivid mazes that is not so complicated that it slows the resulting animation to a crawl. Unfortunately, realism and speed are often incompatible. By and large, the more realistic computer graphics are, the langer it takes to draw them. So for the moment, l et's forget about speed. Let's find the most realistic possible method of generaring computer graphics, then find a way to speed it up. We may sacrifice a few elements of realism when we start Stripping our maze generation algorithms down for speed. But, if we play our cards right, the game players will never notice.
GARDENS OF I MAG I NATION
s o a ki n g U p Rays What's the most realistic method o f creating computer graphic images? That's hard to say. There are several techniques now available for generaring realistic images on a computer screen , and you can easily start an argument among graphics programmers by asking which is best. However, the best-known method of generaring realistic computer graphics is almost certainly ray tracing. The very name "ray tracing" has taken on nearly magical connotations in recent years. Even people who don't know a floppy disk from a frisbee can spot the ray-traced animation sequences in movies like Beauty and the Beast. Ray traced images seem to glow with a vividness that is almost otherworldly. Surely a program that creates such wonderful graphics must itself be an amazingly complex piece of code. Only the most brilliant programmers could possibly dream of writing such a program, right? Wei l , n o . Ray tracing may create beautiful images, but the ray tracers themselves are marvelously simple, at least in concept. Some complications result when programmers add clever tricks to the ray-tracing code in order to make the images render more quickly. But the basic algorithm underlying ray tracing is very Straightforward and easy to understand. To prove it, we'll take a look at how ray tracers work.
Looki n g at the wo r l d Take a look at the world araund you. What do you see? Tables and chairs? Trees and houses? A desk with a microcomputer on top? In a sense, the answer is: None of the above. No matter what you think you are seeing, what you are really seeing is rays of light. Sight is the most intense of human senses, the one to which the largest portion of our brain is devoted. Sight begins when rays of light pass through the lenses of our eyes and strike photosensitive nerves inside them. These nerves pass information about those rays to our brain, which process that information to produce the images that we see. All of the information in that image comes from rays of light that are either bouncing off or being created by the objects at which we are looking. This may seem like an obvious Statement, yet it's easy to forget the important r o l e that l ight plays i n o u r vis ual sys t e m . O bj ects such as the s u n and incandescent lamps produce rays of light. These rays are colared by certain properties of the objects producing them. The rays then proceed to strike the surfaces of other objects, at which point they are either absorbed by the surfaces (causing those surfaces to become warmer) or reflected by them. Actually, most surfaces both reflect and absorb light. Because this absorption is usually selective - only certain colors of light are absorbed - the color of the reflected light is
CHAPTER E I G HT
Rd y Trdcing
Ught Source
Surfoce Figure 8-1 A rav being traced backward from the viewer i nto the world inside the computer
usually different from the color of the light that originally struck the surface. When those reßected rays of light reach our eyes, we see only the colors that were not absorbed, which is why most surfaces in our field of view have colors other than those of the object producing the light. For instance, when white light from an incandescent bulb strikes the surface of the filing cabinet sirring next to my desk, the surface absorbs the colors green and blue, leaving only red. Which is why the cabinet appears to be red when I look at it. If I were to place a colored filter over the nearest lamp to filter the red out of the light, the cabinet might well appear nearly black, because it would absorb all of the colors that managed to get through the filter. In order to produce realistic images, a ray tracer tracks rays of light through a computer-simulated "world" and determines what color those rays would have after reßecting off of various surfaces. It then uses this information to determine the color of the pixels on the display. However, ray tracers don't attempt to trace every ray of light leaving every light source in the simulated world, or even a representative sample thereof That would take much too long. Instead, ray tracers work backward. They start at the eyes of the viewer sitting in front of the computer and trace rays oflight backward into the computer display (see Figure 8- 1 ) . How many rays of light does a ray tracer actually trace? Usually, a single ray for every pixel in the image being produced. One by one, the ray tracer traces
GARDENS O F I MAG I NATION
Viewer
Figure 8-2 Each pixel on the display is given the color of the l ight rav that passes through it
rays backward from the viewer's eyes through each pixel on the display into the simulated world inside. The pixels on the display are then given colors based on the color of the light source that initially produced the ray minus any colors absorbed by surfaces or filters that it encountered on the way (see Figure 8-2) . Ray tracing is j ust about the simplest method ever developed for creating a graphic image, hyper-realistic or otherwise. Because each ray of light is tracked separately, it is not necessary to worry about inadvertendy displaying hidden surfaces in the image or clipping portians of the image against the edges of the viewport. In fact, ray tracing has only one major drawback: It is slow. Tracing all of those rays takes time. There's no way that any ray tracer in existence today could be used to produce animared computer images on the fly. Nonetheless, let's take a closer look at ray tracing, then figure out how we can use some of the basic principles of the technique in a high-speed animation engine. In order to get an inside look at ray tracing, we'll spend the rest of this chapter developing an actual working ray tracer.
A Ray Tra c e r of o u r own The first issue that we need to deal with i n building a ray tracer is the way in which we will store the simulated world that we want to trace. For the simple ray
C HAPTER EIGHT
Rd(J Trd c i ng
Sphere
Figure 8-3 The two types of objects supported by our rav tracer
tracer that we're about to write, there's no need to get fancy. We'll store our "world" as a simple one-dimensional array, in which each element contains a description of one object in the world. The objects themselves will be simple shape primitives that can be described by a few parameters. To keep things as simple as possible, our ray tracer will only support two types of shapes: spheres and infinite planes (see Figure 8-3). Even this simple arrangement, however, introduces some complications in our program. The information needed to describe an infinite plane is quite different from the information used to describe a sphere. Thus we'll need to use the type of CIC++ structure known as a union to cram two different types of structures, or pointers to those structures, into a single element of the array. So we'll begin by creating separate structures for each type of object, then create a single type of pointer that can point at both of them. Before defining either of these structures, we'll define a special type o f structure t o hold numerical triplets, which we'll b e using i n a couple o f ways i n our object definitions. We'll refer to this type of structure as vector_type: s t r u c t v e c t o r_type { doub l e x , y , z ; };
One obvious use for this structure will be storing three-dimensional spatial coordinates, as described in the last chapter. Another use will be for describing
GARDENS O F IMAG I NAT I O N
the spatial orientation of certain surfaces, as you'll see in our next definition. The definition for the infinite plane looks like this: s t ruct
p l a n e_type {
ve c t o r_t ype
s u r f a c e_no rma l ;
doub l e d i s t a n c e ; };
The vector_type structure here represents the so-called surfoce normal of the plane. The surface normal is a line perpendi cular to ( or "normal to , " as a mathematician might put it) the surface of the plane. It can be represented by a three-number vector representing the change in coordinate values along a unit of the line, as shown in Figure 8-4. (See chapter 7 fo r a detailed discussion of vecto rs . ) This vector gives us the o rientation of the plane in space. Since the plane is infinite - that is, it stretches away forever in two dimensions - the only other thing we need to know abour it is where it is along the length of the surface normal. If we imagine the surface normal as a line Stretching forever through space along one dimension, then an infinite number of infinite planes could be dispersed along its length. Thus we further define the plane with the variable distance. If we imagine that the surface normal intersects the 0,0 origin
Surfoce normal (perpendicular fo plane)
Fig u re 8 - 4 The s u rface normal of a plane
1266
CHAPTER E I G H T
Rdl,l Trdcing
point of our universe at some point in its infinite length, then distance represents the number of units from the the origin of the universe to the surface of the plane (see Figure 8-4) . With those two values - the surface normal and the distance from the origin - we have completely described the infinite plane. We don't need to know the orientation of the sphere in space because a sphere, being symmetrical in all dimensions, has no real orientation. Pur another way, an upside-down sphere Iooks exacdy like a rightside-up one. (This is not true if the sphere has some sort of pattern mapped onto its surface, but we won't be using any surface patterns in our ray tracer.) What we need to know is the spatial location of the sphere - that is, the coordinates of the sphere's center. lt's traditional in writing a ray tracer to use variables called /, m, and n to represent these coordinates. We also need to know the radius of the sphere, in coordinate units. Here's the definition for the sphere structure: s t r u c t s p h e r e_type { doub l e } ,.
i nt
l ,m,n;
r;
I I Center of sphere I I Ra d i u s of s p h e r e
Now that we have these two structures, we'll use them to create a type of variable that can point to both of these structures: u n i on ob j e c t_un i on { p l a n e_type *p l a n e ; } ,.
s p h e r e_type * s p h e r e ;
Finally, we'll create a structure that can be used to define an object in the ray tracer's world: s t r u c t obj e c t_type { i n t t ype_o f_obj e c t ; i nt color; ob j e c t_un i on obj ; };
The first field, type_of_object, is a number defining what type of object the structure represents. We'll need to define constants representing the two types of objects that we will be tracing: c o n s t S PH E R E = 1 ; const
I N F I N I T E_PLA N E = Z ;
If the type_of_object field is set to 1 , the object is a sphere. If the type_of_object field is set to 2, the object is an infinite plane. Other values are not defined, though this ray tracer could easily be expanded to recognize other object types.
GARDENS OF I MAG I NATI O N
The color field will define the color that should be given to the surfaces of the object when they are fully illuminated. I n p ractice, this will represent the maxim u m possible color of the surface; in most instances, the surface will be shown at least partially in shadow and won't achieve this maximum color. We'll have more to say about color in a moment. Finally, the obj field can be either a pointer to a plane_type structure or a sphere_type structure, depending on which we want it to represent. Thus we define it as an object_union structure.
Lew - Reso l ut i o n Rays Before we define the actual objects in the ray-traced world, let's talk a little about how we're going to set up the video display. Since the game that we're going to write in this book will use the 320 by 200 by 256-color VGA graphics mode, the simple ray tracer that we'l l develop in this chapter will use that mode too. Unfortunately, a ray-traced image usually requires a lot more than 256 colors, so we'll simplify our task by limiting the number of colors in the images that we produce. lnstead of a full-color ray trace, we'll create a gray-scale ray trace. The VGA adapter can generate 64 different shades of gray, enough to create some nicely detailed and subdy shaded images. We'll need to create a custom palette to hold these shades of gray. We'll store that palette in a char array, defined like this: c h a r pa l [ 7 68 J ;
Then we'll p u t some code in the ini tialization portion of the ray-tracer program that will generate a gray-scale palette. In an RGB color system like the one used by the VGA adapter, all colors in which the levels of red, green, and blue are equal will appear to be gray. Since the VGA adapter can generate 64 possible shades of each color component, it can display 64 possible shades of gray, from 0,0,0 to 63,63,63. Here's a for() loop that will pur 64 shades of gray into the first 64 elements of a palette array: for
( i n t g r ey=O;
p a l [ g r ey*3J
=
g r eyd i s t a n c e ;
And we'll set the variables XO, YO, and ZO to the starring coordinates of the ray and the variables Xd, Yd, and Zd to the direction vector of the ray: XO=rays t a r t . x ; YO=rays t a r t . y ; ZO=rays t a r t . z ; Xd= r a yve c . x ; Yd= ray vec . y ; Zd=rayve c . z ;
We can then calculate the first portion of the combined equation of the line and plane:
GARDE:NS OF IMAGI NATI ON doub l e vd=a *Xd+b*Yd+ c * Z d ;
I f this isn't equal to 0, we'll continue: if
( vd ! =0 )
{
Then we'll calculate the second part of the equation: doub l e vO=- < a * X O+b * Y O + c * ZO+d ) ;
and derive the value of t: t = vO / v d ;
This is where the variable prev_t comes in. I f the ray intersects more than one surface in the scene, we only want to know the color of the nearest one, since it will block the ray and keep it from striking the second object (unless the first surface is transparent, a possibility that we are not including in our ray tracer) . The variable prev_t holds the distance to the last surface the ray struck. (lnitially, we set it to the farthest distance possible, so that any objects that it struck would be at smaller distances.) We check to see if t is both positive (so that we know the surface is in front of the viewer) and smaller than prev_t before we continue: if
( ( t < p r ev_t ) && C t>=Q ) )
{
If t passes both tests, we set prev_t equal to the distance to this surface: p r e v_t
= t;
and calculate the coordinates in space of the intersection point, storing them in the vector_rype variable loc: l o c . x=XO+Xd * t ; l o c . y=YO+Y d * t ; loc . z=ZO+Zd*t;
To get the actual lightsourced color of the surface, we pass the color of the plane (stored in objects[ob}. color) to the lightsourceO function: c o l o r = l i g h t s ou r c e ( o b j e c t s [ o b J . c o l o r , l o c , n o rm ) ;
And that's all we need to do i f the object i s a plane. At the end of the function, we'll return the value of color to the calling function.
Tra c i n g a S p h e re We follow a similar course of action if the object is a sphere: c a s e S PH E R E :
We set the variables Xe, Yc, and Zc equal to the coordinates of the center of the sphere, and Sr equal to the radius:
282
CHAPTE:R EIGHT
Rdi:J Trd c i ng
doub l e X c=o b j e c t s [ o b J . ob j . s p h e re-> l ; doub l e Y c=o b j e c t s [ o b J . ob j . s p h e re->m; doub l e Z c=o b j e c t s [ o b J . ob j . s p h e re-> n ; doub l e S r=o b j e c t s [ o b ] . ob j . s p h e re-> r ;
Once again, we set XO, YO, and ZO equal to the starting point of the ray, and Xd, Yd, and Zd to the direction vector of the ray: XO=ray s t a r t . x ; YO= r a y s t a rt . y; ZO=ray s t a r t . z ; X d = r a yve c . x ; Yd= rayve c . y ; Zd=rayve c . z ;
Then we set the variables b and quadratic equation:
c
equal to the b and c components of the
b=2 . 0* ( X d * ( X0-X c ) +Y d * ( Y0-Y c ) +Zd* ( Z0-Z c ) ) ; c = ( X0- X c ) * ( X 0-X c ) + ( Y0-Y c ) * ( Y 0- Y c ) + ( Z0-Zc ) * ( Z0-Z c ) - S r * S r ;
We then calculate the discriminant using the quadratic formula and check to see if it's either 0 or positive: if
( ( d i s c r i m i n a nt=b*b-4* c ) >=0 )
{
If it is, we calculate the first and second roots of the equation: doub l e sd=sq r t ( d i s c r i m i n a n t ) ; t0 = ( -b-sd ) l 2 ; t 1 = ( -b+sd ) l 2 ;
We then set t equal to the smaller of tO and tl: if
< < tO>O >
I I
< t 1 >0 > >
{
if
< t O>O )
t=tO;
if
< < t 1 < t O > && < t 1 > =0 ) )
t= t 1 ;
If t is less than prev_t, we calculate the position of the point in space where the ray intersects the sphere and the surface normal of the sphere at that point: if
( t random C 1 0000) ) newcolor=ce i L C newco l o r ) ; e l s e new c o l o r= f loor C n ewc o l o r ) ;
lt's possible that we just rounded a gray-scale value above the range allowed by our program, so we check for values greater than 63 and round them down to 63: if
C n ewco l o r >63 ) newco l o r=63;
Finally, we pass the lightsourced color back to the calling function: r e t u r n C i n t ) newco l o r ; }
The p l oto F u n cti on The plot() function is s o simple that it's barely worth describing. We use the same formula that we've used in earlier chapters for calculating a video memory offset from a pair of coordinates: voi d p l o t C i n t x , i n t y, i n t co l o r ) {
i nt c; sc reen[ y*320+x J=co l o r ; }
The RT D E M O P rog ra m The complete text of the ray- tracing program , called RTDEMO . CPP, 1s Listing 8- 1 :
m
GARDENS O F IMAG INATION
Listing 8-1 The RTD E M O . C P P prog ra m II
R T D E M O . C PP V e r s i o n 1 . 0
II
S i m p l e demo n s t r a t i o n o f
r a y t r a c i ng
( i n gray s c a l e )
II II
W r i t t en by C h r i s t o p h e r Lamp t on f o r Ga rdens of
II
# i n c l ud e
I m a g i na t i on
< s t d i o . h>
# i n c l ud e
# i n c l ud e
< c on i o . h>
# i n c l ud e
< s t d l i b . h>
# i n c l ude
" s c r ee n . h "
# i n c l ud e
. . pcx . h u
# i n c l ude
" ma t h . h "
# i n c l ud e
" t i me . h "
# i n c l ud e
"va l u e s . h "
const
( Wa i t e Group Pres s )
R E C T A N G L E= 1 ;
const
I N F I N I T E_PLA N E = 2 ;
const
S PH E R E = 3 ;
s t r u c t v e c t or_type { } ,.
d o u b l e x , y, z ;
s t r u c t p l an e_type { v e c t o r_type } ,.
s t ru c t
s p h e re_t ype {
doub l e } ,.
s u r f a c e_n o rma l ;
doub l e d i s ta n c e ;
l , m, n;
doub l e r ;
II
C e n t e r of
sphere
II
Rad i us of
sphere
u n i on o b j e c t_un i on { p l a n e_type * p l a n e ; s p h e r e_t ype * s p h e r e ; }; s t r u c t o b j e c t_type { int
t ype_of_o b j e c t ;
i nt
color;
o b j e c t_u n i on o b j ; }; co n s t
N U M B E R_O F_OBJ E C T S = 2 ;
co n s t BACKGROU N D_C O LO R=6;
CHAPTER E: I G H T const const const const const
RdiJ Trdcing
doub l e AMBI E N T_I NTENS I TY= . 1 4 ; v e c t o r_type L I G HT_SOURCE={ 500, -200, 1 5 0 } ; VI EWER_X= 1 60; VI EWER_Y =1 00; VI EWER_D I S TANC E=1 00;
s t r u c t p l a ne_t ype p l a n e = { { 0 , - 1 ,0}, 500 }; s t r u c t sphe re_t ype s p h e r e = { 0 , 40, 300, 1 0 0}
II S u r f a c e no rma l I I Di s t a n c e f r om o r i g i n I I Coord i na t e s of c e n t e r I I Ra d i u s
st r u c t obj e c t_type o b j e c t s [ NUMBER_O F_O BJ E C T S J = { { S P H E R E , 5 6, ( p l a ne_t ype *) & s p h e r e } , { ! N F I N I T E_PLA N E , 4 8 , ( p l a ne_t ype * ) &p l a n e } }. ,
i n t t r a c e_ray ( v e c t o r_t ype r a yv e c , v e c t o r_type r a y s t a rt ) ; vo i d p l o t ( i n t x, i n t y , i n t co l o r ) ; vector_type get l o c ( doub l e t , v e c t o r_type rayv e c , v e c tor_type r a y s t a r t ) ; i n t l i g h t s o u r c e ( i n t c o l o r , v e c t o r_type l o c , v e c t o r_type n o r m ) ; i n t i ma g e = 1 3 ; cha r f a r * s c reen; vo i d ma i n O { c h a r pa l [76 8 J ; v e c t o r_type rayvec , r a y s t a r t ; ra ndom i z e ( ) ; for ( i n t g r ey=O; g r ey l ; do ub l e Y c =obj e c t s [ obJ . o bj . s p h e r e ->m; doub l e Z c =obj e c t s [ o b J . o b j . s p h e r e->n; doub l e S r =obj e c t s [ o b J . o b j . s p h e re-> r ; XO=rays t a r t . x ; YO=rays t a r t . y ; ZO=ray s t a r t . z ; Xd=rayvec . x ; Yd=rayvec . y ; Zd=rayv e c . z ; b=2 . 0 * ( Xd * C X O- X c ) + Yd* ( Y 0- Y c ) + Z d * ( Z0- Z c ) ) ; c = ( X0-X c ) * ( X0-X c ) + ( Y0-Y c ) * ( Y 0-Y c ) + ( Z0- Z c ) * ( Z 0-Z c ) -S r*S r ; if
( ( d i s c r i m i n a n t =b*b-4 * c ) >= 0 )
{
doub l e sd=sq rt ( d i s c r i m i n a n t ) ; t O = < -b- s d ) / 2 ; t 1 = ( -b+sd ) / 2 ; if
< < t O> O > I I < t 1 >0 » if
< t O>Q )
{
t=tO;
if
< < t 1 && C t 1 >=0 ) )
if
( t
random C 1 0000 ) ) newco lor=cei l C newco lor ) ; e l se new c o l o r = f l o o r ( ne w c o l o r ) ; if
C n e w c o l o r>63 )
return
C i nt )
new c o l o r =63;
newco l o r ;
}
To run the p rogram , go to the directory called RAYTRACE and type RTDEMO. The image of a ray-traced gray sphere and plane will appear (see Figure 8- 1 2) . A second version of the program is available in that directory under the name RAY2 . Type RAY2 and you'll see a second trace, with a larger number of spheres in several different shades of gray (see Figure 8- 1 3) . If you have a copy of the Borland C++ compiler, you might want to design some scenes of your own and recompile the tracer.
Figure 8-1 2 The rav-traced sphere and plane
290
C HAPTER E I G HT
Rdy Trdcin = I MAGE_H E I G H T )
{
Earlier, we ini tialized tyerror to 64, to the value of the I MAGE_HEIGHT constant, which guarantees that the first pixel in the column will be drawn. If the error term is greater than or equal to IMAGE_HEIG HT, we copy the current pixel from the bitmap to the viewport: sc r e e n [ o f f s e t J = t e xtmap s [ t i l e pt r J ;
We then reset the error term by subtracting IMAGE_HEIGHT from it: tye r r o r-= I M AGE_H E I G H T ;
and we move ojfset to point at the next line in the viewport: o f f s e t+=320; }
That closes the while() l o o p . I f t h e e r r o r term i s s t i l l greater t h a n IMAGE_HEIGHT after IMAGE_HEIGHT has been subtracted from it, the loop will execute again - and will continue executing until the error term is less than IMAGE_HEIGHT. This causes the pixels to be enlarged when the screen column is langer than the column in the bitmap.
GARDENS OF IMAG I NATI ON
Now we starr the process all over again by adding the unclipped height of the screen column to the error term: t y e r ro r + = h e i g h t ;
If this causes the error term t o become greater than the height o f a bitmap column, the next pixel in the bitmap will be drawn next time the loop repeats. If not, pixels will be skipped until the cumulative adding of the height value to the error term pushes it over the threshold. Before we can draw the next pixel in the bitmap column, however, we must advance tileptr to point at it: t i l e p t r+=32 0 ; } } }
That terminates all loops. The walls have now been drawn . The complete texture-mapped ray-cast draw_mazeO function appears in Listing 9-3.
The Textu re - M a pped Ray-Cast d raw_mazeo Fu nction
II
TEXTCAST . C P P
II II
F u n c t i on t o d r a w t e x t u re-ma pped wa l l s u s i ng
ray-ca s t i n g .
I I W r i t t e n by C h r i s t o p h e r La mp t on f o r II
Ga r d e n s o f
Ima g i na t i on
( Wa i t e G r oup P r e s s ) .
II # i n c l ud e < s t d i o . h> # i n c l ud e # i n c l ud e
" t ex t c a s t . h "
# i n c l ud e
" p cx . h "
# i n c l ud e
"di stance . h"
# i n c l ud e
" s lope . h "
II
C o n s t a n t d e f i n i t i on s :
c o n s t W A L L_H E I G H T = 6 4 ;
II
c o n s t V I E W E R_D I S T A N C E = 1 9 2 ;
I I Vi e w e r d i s t a n c e f r om s c r e e n
H e i g h t of wa l l
i n p i xe l s
c o n s t V I E WPORT_L E FT=O;
II
D i me n s i ons of v i ewport
c o n s t V I E W PORT_R I G H T =3 1 9 ; c o n s t V I E WPORT_TOP=O; const
V I E WPORT_BOT = 1 99;
c o n s t V I EWPORT_H E I G H T=V I E WPO R T_BOT-VI E W PO R T_TO P ; const
332
V I E W PORT_C E N T E R=VI E W PORT_TOP+V I E W PO R T_H E I G H T I 2 ;
CHAPTER N I N E:
RdlJ Cdsting
vo i d d r aw_ma z e ( map_type ma p , c h a r far * s c ree n , i n t xvi ew, i n t y v i ew, f l o a t v i ewi ng_a n g l e , i n t v i ewer_h e i g h t , c h a r f a r * t e x t ma p s ) II II II II
D r a w s a r a y- c a s t i ma g e i n t h e v i ewpo r t of t h e ma z e r e p r e s en t ed i n a r ray MA P [ J , as seen f r om pos i t i o n XVI EW, YV I E W by a v i ewe r Lo o k i ng at ang l e V I E WI NG_ANGLE w h e r e ang l e 0 i s due nor t h . ( Ang l e s a r e mea s u r ed i n r a d i a n s . )
{ I I Va r i a b l e d e c l a r a t i o n s : i n t sy,o f f s e t ; I I P i x e l y pos i t i o n and of f s et f l oat co l umn_a ng l e = a t a n ( ( f l o a t ) ( c o l umn -1 60 ) I V I EW E R_D I STAN C E ) ; i f ( co l umn_a n g l e==O . O ) c o l umn_a n g le=0 . 001 ; I I Ca l c u l a t e ang l e of ray r e l a t i v e to maze coor d i na t e s f l oat r a d i a n s =v i ewi ng_ang l e +co l umn_ang l e ; I I R o t a t e endpo i n t of r a y to v i e w i n g a ng l e : i nt x 2 - 1 024 * ( s i n ( r a d i a n s ) ) ; i n t y2 = 1 024 * ( c o s ( rad i a ns ) ) ; I I T r a n s l a t e r e l a t i ve t o v i ewer ' s po s i t i on : x2+=xv i ew ; y2+=yv i e w ; I I I n i t i a l i z e r a y a t v i ewer ' s po s i t i o n : f l oa t x=xv i ew ; f l oat y=yvi e w ; I I F i nd d i f f e r ence i n x , y coor d i n a t e s a l ong ra y : f l oat xd i f f =x2-xv i e w ; f l o a t yd i f f=y2-yv i e w ; I I C h e a t to a vo i d d i v i de-by- z e ro e r ro r : i f ( x d i f f ==O ) xd i f f =0 . 0001 ; I I G e t s l ope of ra y : f l oat s l ope = ydi f f l x d i f f ; I I C h e a t ( a g a i n ) to avo i d d i v i de-by- z e r o e r r or : i f ( s l o pe==O . O ) s l o pe= . 0001 ; I I C a s t ray f r om g r i d L i n e to g r i d L i n e : for < ; ; ) { I I I f r a y d i re c t i on po s i t i ve i n x, g e t n e x t x g r i d L i n e : i f ( x d i f f >O ) g r i d_x = ( ( i n t ) x & Ox f f c 0 ) +6 4 ; I I I f ray d i r e c t i o n n ega t i v e i n x , g e t e l s e g r i d_x = ( ( i n t ) x & O x f f c O ) - 1 ;
Last x g r i d L i ne : continued on next page
GARDE:NS O F I MAG I NATION continuedfrom previous page
II
If
if
( yd i f f>Q )
r a y d i r e c t i on po s i t i ve
II
I f r a y d i r e c t i on n e g a t i ve
i n y,
get next y g r i d
l i ne :
g r i d_ y= ( ( i n t ) y & O x f f c Q ) +64 ; i n y,
e l s e g r i d_y= ( ( i n t ) y & O x f f c O ) I I G e t x , y C o o r d i na t e s w h e r e
get
last y grid
l i ne :
- 1; ray c rosses x g r i d
l i ne :
I I G e t x , y c o o r d i na t e s w h e r e r a y c r o s s e s y g r i d
l i ne :
x c ro s s_x =g r i d_x ; x c r o s s_y=y+s l ope* ( g r i d_x-x ) ;
y c r o s s_x=x+ ( g r i d_y-y ) l s l o p e ; y c r o s s_y=g r i d_ y ; II
Get d i stance to x gri d
l i ne :
xd= x c ro s s_x- x ; yd= x c ro s s_y-y; xd i s t =sq r t ( x d *xd+yd*yd ) ; I I Get d i s tance to y g r i d
l i ne :
x d = y c ro s s_x- x ; yd=y c ro s s_y-y; yd i s t = sq r t ( x d *xd+yd*yd ) ; I I If x grid i f
l i ne i s c l o s e r . . .
( xdi s t V I E WPORT_BO T ) d h e i g h t -= ( bo t - V I EWPORT_BOT ) ; I I Poi n t to v i deo memo ry o f f s et for t o p of L i n e : o f f s e t = t op * 320 + co l umn; II I n i t i a l i z e v e r t i c a l e r ror t e rm for t e x t u r e ma p : i n t t ye r ro r = I MA G E_H E I G HT; I I Wh i c h g r a p h i c s t i l e a r e we us i n g ? i n t t i l e=ma p [ xma z e J [ yma z e J - 1 ; II F i n d o f f s et of t i l e and co l u mn i n b i t ma p : u n s i gned i n t t i l e p t r= ( t i l e i 5 ) *320* I MAGE_H E I G HT+ ( t i l e %5 ) * I MAGE_WI DTH+t ; II Loop t h r ough t h e p i x e l s i n t h e c u r r ent ver t i c a l I I L i n e, adva n c i n g O F F S E T to t h e nex t row of p i x e l s I I a f t e r e a c h p i xe l i s d rawn . continued on next page
GARDE:NS OF I MAG INATION continuedfrom previous page
f o r ( i n t h=O; h = I M A G E_H E I G H T ) { I I If s o , d ra w i t : s c r een [ o f f s e t J = t e x t ma p s [ t i l e p t r J ; I I Re s e t e r ro r t e rm t y e r ro r-=I MAGE_H E I G H T ; I I A n d a d v a n c e O F F S E T t o n e x t s c reen L i n e : o f f s e t +=320; } II I n c reme n t a l d i v i s i on : t ye r r or+=he i g h t ; I I Adva n c e T I L E P T R t o n e x t L i ne o f b i tma p : t i l e p t r+=320; } } }
To see t h i s code i n a c t i o n , go to the RAYCAST di rectory and type TEXTDEMO. You'll see a texture-mapped view of the maze walls as they look from the center of the room at an angle of 0 radians, as shown in Figure 9- 1 6. To see the view from other angles, type a number of radians from 0 to 6.2 after the name of the program. Figures 9- 1 7a through e show the maze from angles of 1 , 2 , 3 , 4 , and 5 radians. You can also use fractional radian values to get more precise views of the maze. The improvement over the earlier solid-color ray-casting program is striking. The texture-mapped walls look quite vivid and dramatic. We're well on our way to creating a topnotch ray-casting engine.
Figure 9-16
The maze viewed from an
angle of o radians. as depicted by the TEXTDEMO program
CHAPTER N I N E:
Figure 9-1 7a-e The maze as seen from
RdlJ Cdsting
Figure 9-1 7b
angles of 1 , 2, 3, 4, and 5 radians, respectively
Figure 9-1 7c
Figure 9-1 7d
Figure 9-1 7e
F l o o rca sti n g Truth to teil, we now have almost enough ray-casting code to write an arcade game along the lines of Wolfenstein 3D. All we need is some additional code for drawing bitmapped objects, such as treasure chests and Nazi guards, and we'd have a clone of the Wolf 3D engine. (Actually, this isn't quite true. The Wolfenstein 3 D
GARDENS O F IMAGI NATION
engine appears to use texture-mapped polygons to represent doors and sliding walls, but we'll ignore that detail for the moment.) We can now draw the walls of a texture-mapped maze, which is pretty much all that Wolfenstein 3D does to generate scenery. (The floors and ceilings are drawn in solid colors, but these can be laid down before the ray casting begins.) However, the technology of ray casting has proceeded apace since Wolfenstein 3D came on the scene. lt's no Ionger enough j ust to display texture-mapped walls. A ray-cast game needs to display texture-mapped floors and ceilings as weil. So we'll now develop a floorcasting version of our engine. The first version will drawn only texture-mapped floors and ceilings, then we'll create a second version that will combine the floors with the walls. Ready? Let's go. The program that calls our floorcasting drawmaze() function will need to maintain two arrays, one to represent the map of the floor, the other to represent the map of the ceiling. Each element in these arrays will represent the number of the b itmap in the IMAGES. PCX file to be mapped onto the corresponding segment of floor or ceiling. Since each square of the maze measures 64 by 64 in the fine coordinate system, there will be a perfect correspondence between the pixels in the bitmap and the points in the fine coordinate system, with one pixel per fine coordinate point. Here are the two arrays that we'll be using: m a p_type f l o r = { { 5,
5,
5,
5,
5,
5, 5,
5,
5,
5,
5,
5, 5,
5, 5, 5},
{ 5,
5,
5,
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{ 5,
5,
5,
5,
5,
5, 5,
5, 5, 5,
5, 5,
5,
5,
5,
5},
{ 5,
5,
5,
5,
5,
5, 5,
5,
5, 5,
5,
5,
5,
5,
5,
5},
{ 5,
5,
5,
{ 5,
5,
5,
8,
8, 8,
5,
5,
5,
5,
5, 5,
5,
5,
5,
5},
{ 5,
5,
5,
5,
8,
5,
5,
5,
5,
5,
5, 5,
5,
5,
5,
5}, 5},
5 , 8, 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 } , { 5 , 5 , 5 , 8, 8 , 8 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 } , { 5 , 5 , 8 , 8 , 8, 8 , 8 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 , 5 } , { 5,
5,
5,
{ 5,
5,
5,
5, 5, 5 , 5, 5 , 5 , 5, 5 , 5 , 5, 5, 5, 5}, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
{ 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5, 5,
5,
5,
5,
5},
{ 5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5, 5,
5,
5,
5,
5},
{ 5,
5,
5,
5,
5,
5,
5,
5, 5,
5,
5, 5,
5,
5, 5,
5},
{ 5,
5,
5,
5,
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5},
{ 5, } ,.
5,
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5}
5,
m a p_t y p e c e i l i n g = { { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3, 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3, 1 3 } ,
CHAPTER N I N E
RdLJ Cdst i ng
{ 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3 , 1 3}, { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3}, { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3, 1 3, 1 3, 1 3 , 1 3}, { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3, 1 3 , 1 3 , 1 3, 1 3 , 1 3, 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3 , 1 3, 1 3 } , { 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3 , 1 3, 1 3, 1 3 , 1 3, 1 3 , 1 3, 1 3, 1 3} , { 1 3, 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3 , 1 3, 1 3 , 1 3 , 1 3, 1 3 , 1 3, 1 3 , 1 3}, { 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3, 1 3, 1 3}, { 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3}, { 1 3, 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3, 1 3} } ,.
We've called the first array flor[} instead of floor[} so that the linker won't mistake it for a reference to the C++ floor() function when we attempt to pass a pointer to it in a parameter list. Once again, the prototype of the function, in FLOORCST.H, will need to be modified slightly from earlier versions: vo i d d r a w_ma z e ( ma p_type f l oo r , ma p_type c e i l i ng , c h a r f a r * s c r e e n , i n t x v i e w , i n t y v i ew, f l o a t v i e w i n g_a ng l e , i n t v i e w e r_h e i g h t , c h a r f a r * t e x tmaps ) ;
We've now added the floor and ceiling arrays to the parameter list and have dropped the wall array, since we have no need for it in this function. As before, the function is constructed as a large for() loop that iterates through all of the columns in the viewport. And we start out by calculating the angle of the r ay cast through the c u r r e n t col u m n relative to n o r t h , s t o r i n g the resulting angle i n the Soating point variable radians. At this point, however, the Soorcasting function takes off in a completely different direction. Here's what it will do: We will start at the bottommost pixel in the column, the one at the very bottom of the viewport, and cast a ray through that column. We'll determine where it strikes the Soor, in terms of the fine coordinate system, then determine the color of the pixel at that point. Then we'll draw that pixel in the viewport. We'll continue doing this for all of the pixels in the bottom half of the viewport. (Actually, we'll stop 5 pixels short of the center of the viewport, to give the impression that the more distant Soor pixels have faded into darkness. When we combine the Soorcasting code with the wallcasting code, we'll simply stop casting when we reach a wall.) We'll use a second for() loop to step through all of the pixels in the lower portion of the column, from the one at the bottom of the viewport to the one 5 pixels short of the vertical center: for
( i nt
row=V I EW P O R T_BOT;
row>V I EW PORT_C E N T E R+ S ; --row )
{
GARDENS O F I MAG I NATI ON
Th e Si m i l a r Tri a n g l e s M ethod Now we need t o determine what color the pixel i n that position should be, which means that we must cast a ray through that pixel to determine where it hits the floor. Bur how do we do this? Look at Figure 9- 1 8. lt shows the viewer looking through the video display at the floor of the maze. The line coming out of the viewer's eyes and striking the floor is labeled B. Another line runs from the viewer's feet to the position where the ray strikes the floor. We've labeled this A. Finally, the line that runs from the viewer's eyes to the floor is labeled C. You'll notice that these lines, taken together, form a rriangle. We know the length of one side of this triangle, side C (the one that runs from the viewer's eye to the floor) . (lt's conrained in the parameter viewer_height that was passed from the cal ling program . ) We want to know the length of A, from which we can calculate the Coordinates of the floor pixel struck by the ray. We could use trigonometry to calculate this length, but there's an easier way, one that lends itself more to optimization. It's called the similar triangles method Look at Figure 9- 1 9. The same triangle from Figure 9- 1 8 is depicted here, but now a second triangle has been nested inside of it. One edge of this triangle, running parallel to line C in the big triangle, is labeled c. This edge represents the height of the viewer's eye above the row through which we are casting. The line labeled a in the small triangle is the distance from the viewer to the display. lt runs parallel ro line A in the b ig triangle. And the line labeled b in the small triangle is the straight line distance from the viewer's eye to the row through which the ray has been cast. This line runs parallel to line B in the big triangle.
rKieo Display
Figure 9-18
A
Distonce Moze Floor
The viewer looking through the video dis play at the maze
CHAPTER N I N E
Rdy Cdsting
Because all of the lines in the small triangle are parallel to lines in the big triangle, we know that all of the angles between sides in the small triangle must be the same as the angles between sides in the big triangle, even though the lengths of the sides are different. In mathematical terms, two triangles in which all of the angles between the sides are the same but the lengths of the sides are different are called similar triangles. According to the Law of Similar Triangles, the ratio between the sides of a similar triangle are always the same no matter what the actual lengths of the sides are. If one side is 1 . 5 times the length of another in a small triangle, it will be 1 . 5 times the size of the other in a !arger version with the same angles. In algebraic terms, we would write: A/C
=
a/c
where the letters refer to the sides o f our similar triangles. We already know the lengths of three of these sides. C is the value of the parameter viewer_height, a is the value of the constant VIEWER_DI STANCE, and c can be easily calculated by subtracting VIEWPORT_CENTER (the row of the viewport with which the viewer's eyes are supposedly aligned) from row (the row we are casting the ray through) . We need to solve this equation for A. Following the standard algebraic rules, we can rewrite the equation as: A
=
a/c * C
Plugging in the values from our program, we get: d i s t a n ce_to_p i x e l
V I EW E R_D I STANC E / ( V I EWPO RT_C E N T E R - row )
*
v i e w e r_h e i g h t
Video Display
A Figure 9-1 9 The small triangle with sides a. b, c is mathematica lly similar t� the large triangle with sides A, B, C
GARDENS OF I MAGI NATION
I n the actual program, we'll code that slighdy differendy. First we'll perform the division and store the value in the floating point variable ratio, since the result of the division is the ratio between the sides: f l oa t ra t i o =( f l oa t ) v i e w e r_h e i g h t l ( row-V I EWPORT_C E N T E R ) ;
Then we'll multiply the result by VIEWER_DISTANCE to get the actual distance of the pixel from the viewer. We'll also perform the cosine adj ustment, as explained earlier in this chapter, to avoid a fish-eye effect: d i s t a n c e = r a t i o*VI E W E R_D I S TA N C E i cos ( c o l u mn_a ng l e ) ;
Now we'll rotate this distance around the origin to the angle that we stored earlier in the variable radians: i n t x = - d i s t a n c e * ( s i n ( ra d i a n s ) ) ; i n t y = d i s tance * ( cos ( rad i a ns ) ) ;
(This is the same calculation that we used during wallcasting, explained earlier in this chapter.) Then we'll translate the resulting coordinates relative to the viewer's position: x+=xv i e w ; y+=yv i ew;
I n order to find the pixel color for this position in the maze, we need to know which maze grid square it's part of: i n t xma z e = x I 6 4 ; i n t yma z e = y I 64;
We also need to know the column of texeure map that should be applied to rhis pixel. We'll find it by taking both the x and y coordinates positions modulo 64, and using the result to calculate the necessary index into rhe bitmap buffer relative to the upper-left corner of the bitmap: i n t t = ( ( i n t ) y & Ox 3 f ) * 320 + ( ( i n t ) x & Ox 3 f ) ;
Once again, we've used 320 as the bitmap width rather than IMAGE_WIDTH because the bitmaps are stored in a buffer 320 pixels wide. The number of the image tile that we'll be using is stored in the floor[] array. (We've called the array floor[] wirhin the function rather than flor[], because the context in which we'll be using it here makes it clear that we're not referring to the floorO function.) We can use the xmaze and ymaze values, as before, to obtain the element with the nurober of the rile: i n t t i l e= f l oo r [ xm a z e ] [ yma z e ] ;
CHAPTER N I N E
Rdy Cdsting
We'll calculate the offset of the upper-right corner of the bitmap exacdy as before: u n s i gned i n t t i l ep t r= ( t i l e / 5 ) *3 2 0 * I M A G E_H E I G H T+ ( t i l e% 5 ) * I M A G E_W I D T H + t ;
Similarly, we'll calculate the video memory offset of the current row in the current column using rhe standard calculation: o f f s e t = r ow*320+ c o l umn;
Finally, we'll copy the pixel from the bitmap to the viewporr: s c r e e n [ o f f s e t J = t e x t m a p s [ t i l e pt r J ; }
And that's all there i s to drawing the floor.
D rawi n g t h e C e i l i n g We'll draw the ceiling almost exactly as we drew rhe floor. The easiest way to draw the ceiling would be to draw it at the same time as we draw rhe floor, mirroring every floor pixel with a ceiling pixel in the upper half of the viewporr. That would be faster than what we're actually going to do, bur it would assume that the viewer's eye height is fixed midway between the floor and ceiling. This will often be the case - in fact, many ray casters work on rhis assumption - but we'll make the ray caster that we develop in this chapter a bit more versatile. That's why we've passed viewer_height as a parameter from the calling program, so that it can be varied dynamically if necessary. Bur this means we'll have to cast the ceiling separarely, recalculating all of rhe numbers from scratch. Instead of starring with the bottommost pixel in the viewporr and advancing until we're 5 pixels below rhe center, we'll starr with the topmost pixel in the viewporr and advance until we're 5 pixels above the center: for
( row=VI E W P O R T_TO P ;
row
" f loo r c s t . h " "pcx. h "
WALL_H E I GHT=64; II Hei g h t of wa l l i n p i x e l s V I E W E R_D I STA N C E=1 28; II V i e w e r d i s t a n c e f r om s c reen V I EWPORT_L E FT=O; II D i me n s i o ns of v i ewpo rt V I E WPORT_R I GHT= 3 1 9 ; V I E W PORT_TOP=O; V I EWPORT_BOT=1 99; V I E WPORT_H E I GHT=VI E W PO RT_BOT-V I E W PO RT_TOP; V I EWPORT_C E N T E R=VI EWPORT_TOP+V I EW PO RT_H E I GHT I 2 ; ZOOM=2;
vo i d d ra w_ma ze ( ma p_t ype f l o o r , m a p_type c e i l i ng , c h a r f a r * s c reen, i n t x v i ew, i n t y v i ew, f l o a t v i e w i ng_a n g l e , i n t vi ewer_h e i g h t , c h a r f a r * t ex t ma p s ) { i n t y_un i t , x_un i t ; I I Va r i a b l e s for a mount o f c h a nge II i n x a nd y i n t d i s t a n ce , o l d_d i s t a n c e , x d , y d , s x , s y , o f f s e t ; I I Loop t h r o u g h a l l co l umns o f p i x e l s i n vi ewpo r t : for ( i n t c o l umn=V I E WP ORT_L E F T ; c o l umnV I E WPORT_C E N T E R + S ; --row )
I I G e t ra t i o of v i ewe r ' s h e i g h t to p i x e l h e i g h t : f l oa t
ra t i o= ( f l oa t ) v i e w e r_he i g h t l ( row-V I EWPO R T_C E NT E R ) ;
I I G e t d i s t a n c e t o v i s i b l e p i xe l : d i s t a n c e = ra t i o*V I EW E R_D I S TAN C E i c o s ( c o l umn_a n g l e ) ;
I I Rotate d i s t ance t o ray ang l e : i nt x
- di stance *
( s i n ( ra d i a ns ) ) ;
i n t y = d i stance * ( co s ( radi a n s ) ) ;
I I T r a n s l a t e r e l a t i ve to v i e w e r c o o r d i n a t e s : x+=xv i ew; y+=yv i e w;
II G e t ma z e squa r e i n t e r s e c t e d by r a y : i n t x ma z e x I 64; i n t yma z e = y I 64;
II
F i nd r e l ev a n t
co l umn o f t e x t u r e ma p :
i n t t = ( ( i n t ) y & Ox3 f ) * 3 2 0 + ( ( i n t ) x & Ox3f ) ;
I I Wh i c h g r a p h i c s t i l e a r e w e u s i n g ? i n t t i l e= f l o o r [ xm a z e J [yma z e J ;
I I F i nd o f f s e t o f t i l e a n d c o l umn i n b i t ma p : u n s i g ned i n t t i l e p t r = ( t i l e i 5 ) *320* I MAGE_H E I G HT+ ( t i l e% 5 ) * I MA G E_W I DTH+t ;
I I Ca l c u l a t e vi deo o f f s e t of f l oo r p i xe l : o f f s e t= row*320+c o l umn ;
I I Draw p i xe l : s c r e e n [ o f f s e t J = t e x t m a ps [ t i l e p t r J ; }
I I S t e p t h rough c e i l i n g p i xe l s : for
( row=V I Ew PO R T_TO P ;
row O ) g r i d_ y= ( ( i n t ) y & Ox f f cQ ) +64; II If ray d i re c t i o n ne ga t i v e i n y , get l a s t y g r i d l i n e : e l se g r i d_ y= ( ( i n t ) y & Ox f f c Q ) - 1 ; I I G e t x , y coor d i n a t es w h e r e ray c r o s s e s x g r i d l i n e : x c r o s s_x =g r i d_x ; x c r o s s_y=y+ s l o p e* ( g r i d_x-x ) ; I I G e t x , y coor d i n a t e s w h e r e ray c r o s s e s y g r i d l i n e : y c r o s s_x =x+ ( g r i d_y-y ) l s l o p e ; y c r o s s_y= g r i d_ y ; I I G e t d i s t a n c e to x g r i d l i n e : x d= x c r o s s_x- x ; yd=x c r o s s_y-y; x d i s t =s q r t ( x d *xd+yd*yd ) ; I I G e t d i s t a n c e t o y g r i d l i ne : x d =y c ro s s_x-x; yd=y c ro s s_y-y; y d i s t =sq r t ( xd* xd+yd*yd ) ; I I I f x g r i d l i n e i s c l oser . . . i f < xd i s t V I EW PO R T_BOT ) dhei ght i he i g h t
{
( bot - V I E W PORT_BOT ) ; -=
C bot - VI EWPOR T_BOT ) *y r a t i o ;
continued on next page
GARDENS OF I MAG I NATION continuedfrom previous page
b o t = V I E W P O RT_BOT; } II
Po i n t t o v i deo memo r y o f f s e t
f o r t o p of
l i ne :
o f f s e t = t o p * 3 2 0 + c o l um n ; II
I n i t i a l i z e vert i ca l
i nt
e r r or t e rm for
t e x t u r e map :
t ye r ro r =64 ;
I I Wh i c h g r a ph i c s t i l e a r e we u s i n g ? i n t t i l e=ma p [ x m a z e J [ yma z e J - 1 ; II
F i nd o f f s e t of
t i l e a n d c o l umn i n b i tma p :
u n s i g n e d i n t t i l e p t r= ( t i l e i 5 ) * 3 2 0 * I M A G E_H E I GHT+ ( t i l e % 5 ) * I M A G E_W I DT H+ t ; II
Loop t h rough
II
l i n e , a dvan c i n g O F F S E T t o t h e n e x t
the pixels
I I a f t e r e a c h pi xe l for
( i n t h=O;
I I A r e we whi le II
i n t h e c u r rent vert i c a l row o f p i x e l s
i s dr awn .
h= I M A G E_H E I G H T )
I f so,
draw
{
it:
s c r e e n [ o f f s e t J = t e x t m a p s [ t i l e pt r J ; II
Reset e r ro r t e r m :
t ye r r o r - = I M A G E_H E I G H T ; I I A n d advan c e O F F S E T t o n e x t s c r e e n
l i ne :
o f f s e t +=320; } II
I n c rement a l d i v i s i on : t y e r r o r+=he i g h t ;
I I Adva n c e T I L E PT R t o n e x t
l i n e of
bi tma p :
t i l e pt r+=320; } II
S t e p th rough f l o o r p i x e l s
for
( i nt
I I Get f loat
row=bo t + 1 ;
row=O ) & & < xm a z e =Q ) && C ym a z e =O ) && < xma z e =O > && < ym a z e
"b lokcas t . h " "pcx . h "
I I Co n s t a n t def i n i t i o ns : II H e i g h t o f wa l l i n p i x e l s c o n s t W A L L_H E I GHT=64; const V I E W E R_D I S TANCE=1 92; I I V i e w e r d i s t a n c e f rom s c reen II D i me n s i ons o f v i ewpo r t const V I EWPORT_L E FT=O; co n s t V I EW PO RT_R I GHT=3 1 9 ; co n s t V I E W PORT_TOP=O; c o n s t V I E W PORT_BOT= 1 99; c o n s t V I E W PORT_H E I G H T=V I EWPORT_BOT-V I EWPORT_TOP; const V I EWPORT_C E N T E R =V I EWPORT_TOP+V I EW PO R T_H E I GH T I 2 ; c o n s t G R I D W I D TH=1 6 ; vo i d d r a w_ma z e ( ma p_type wa l l , ma p_t ype f l o o r , map_t ype f l o o r h e i g h t , c h a r f a r * s c r e e n , i n t x v i e w , i n t yvi ew, f l oa t v i e w i ng_a n g l e , i n t v i ewer_he i g h t , c h a r f a r * t e x t ma p s ) II II II II
D r a w s a r a y c a s t i ma g e i n t h e v i ewpo r t of t h e m a z e r e p r e s e n t ed i n a r r a y MA P [ J , a s seen f r om po s i t i o n X V I E W , YV I EW by a v i e w e r l ook i ng a t a n g l e V I E W I N G_A N G L E w h e r e a ng l e 0 i s due n o r t h . ( Ang l e s a r e measu red i n r a d i a n s . )
{ I I Va r i a b l e d e c l a r a t i o n s : I I P i xe l y po s i t i on a n d o f f s e t i nt sy,offset; f l oa t x d , yd; I I D i s t a n c e t o n e x t wa l l i n x and y I I Coord i na t e s of x and y g r i d l i n e s i n t g r i d_x, g r i d_ y ; f l oa t x c ross_x , x c r o s s_ y ; I I R a y i n t e r s e c t i on coord i na t e s f l o a t y c r o s s_x , y c r o s s_ y ; u n s i g n e d i n t x d i s t , yd i s t ; I I D i s t a n c e t o x and y g r i d l i n es II D i s t a n c e to wa l l a l ong ray i n t d i s t a n c e , rea l d i s t a n c e ; I I Co l umn i n t e x t u r e m a p i n t t m c o l um n ; f l o a t y r a t i o; i n t c u r r e n t h e i g h t ; I I He i g h t o f c u r r e n t f l oor b l o c k i n t new h e i g h t , w a l l h e i g h t ; i n t h e i g h t c h a ng e ; i n t t o p , bot , l a s t t op; f loat x,y; I I Loop t h ro u g h a l l c o l umns of p i xe l s i n v i ewpo r t : f o r ( i n t co l umn=VI EWPORT_LE FT; c o l umnV I EW PO R T_BOT )
II
l a s t t o p=top;
End o f o u t e r c a s t i n g w h i l e ( )
} I I End o f c o l umn f o r ( ) }
l a s t top=V I EWPORT_BO T ;
l oop
l oop
GARDE:NS OF I MAG I NATI O N
The B LO KD E M O Prog ra m To demoostrate this latest maze-drawing function, we'll write a short call ing program called BLOKDEMO that will use draw_mazeO to draw a view of a heightmapped maze. This calling program will use the three arrays shown earlier in this chapter. As in the demo p rograms in the last chapter, it will read a parameter from the command line that represents the number of radians relative to due north that the viewer should be facing. It will also check for a second parameter representing the height of the viewer above the floor, so that you can look at the maze from both high and low vantage points. This height should be i n the range 1 to 2 5 5 , though you can try other values to see what sort of results you can obtain. The text of the BLOKDEMO program appears in Listing 1 0-2.
Listi ng 1 0- 2 The B LO K D E M O . C P P prog ram II II II II II II II
BLOKDEM O . C P P C a l l s r a y- ca s t i n g f un c t i on t o d r a w v i ew of b l o c k-a l i gn e d h e i g h t f i e l d s . W r i t t e n by C h r i s t o p h e r Lampton for G a r d e n s of I ma g i na t i on ( Wa i t e G ro u p P r e s s >
# i n c l ude < s t d i o . h> # i n c l ude # i n c l ude < con i o . h> # i n c l ude < s t d l i b . h> # i n c l ude # i n c l ud e " s c r e e n .. h " # i n c l ud e " p cx . h " # i n c l ud e " b lokcas t . h " l l # i n c l u de " b i tma p . h " c o n s t NUM_I MAGE S=1 5 ; p c x_s t r u c t t e x t ma p s , h i g h ma p s ; map_t ype w a { 5, 5, 5, { 5, 5, 5, { 5, 5, 5, { 5, 5, 5, { 5, 5, 5, { 5 , 1 4, 1 4, {14, 5, 5,
ll={ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5,
5}, 5}, 5}, 5}, 5}, 5}, 5},
CHAPTE:R TE:N { 1 4 , 5, 5, { 1 4 , 5 , 5, { 5,14, 1 4, { 5, 5 , 5, { 5 , 5 , 5, { 5 , 5, 5, { 5, 5 , 5 , { 5 , 5, 5, { 5 , 5, 5, };
5, 5, 5, 5, 5, 5, 5 , 5 , 5, 5 , 5, 5 , 5 , 5, 5, 5 , 5, 5 , 5,12,12,12,12,12, 5,12,12,12,12,12, 5 , 1 2 , 1 2 , 1 2 , 1 2,1 2, 5 , 1 2 , 1 2 , 1 2 , 1 2,1 2, 5 , 1 2 , 1 2 , 1 2 , 1 2 , 1 2, 5 , 5, 5, 5 , 5 , 5,
5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5,
5}, 5}, 5}, 5}, 5}, 5}, 5}, 5}, 5}
map_t ype f l or={ { 5, 5, 5 , 5 , 5, 5, 5, 5 , 5 , { 5, 5, 5, 5 , 5, 5, 5 , 5 , 5 , { 5, 5, 5, 5 , 5, 5, 5 , 5 , 5 , { 5, 5, 5, 5, 5, 5, 5 , 5 , 5 , { 5, 5, 5, 5, 8 , 5, 5 , 5 , 5 , { 5 , 1 0 , 1 0, 5 , 5, 5 , 5 , 5, 5, { 5 , 1 0, 1 0, 5, 5 , 5 , 5, 5, 5, { 5 , 1 0, 1 0, 5, 5, 5, 5, 5 , 5, { 5, 5, 5, 5 , 5 , 5, 5 , 5 , 5 , { 5 , 5, 5, 5 , 5 , 5, 5 , 5 , 5 , { 5 , 5, 5, 5 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , { 5 , 5, 5, 5 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , { 5 , 5, 5, 5 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , { 5 , 5, 5, 5 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , { 5, 5 , 5 , 5 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , { 5 , 5, 5, 5 , 5 , 5 , 5 , 5, 5 , };
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5}, 5}, 5}, 5}, 5}, 5}, 5}, 5}, 5}, 5}, 5}, 5}, 5}, 5}, 5}, 5}
Heightmdpping
map_type f l oo rh ei g h t = { { 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 20 , 20,20, 20, 20, 2 0 , 2 0 , 2 0 , 2 0 , 2 0 } , {20,20,20,20,20,20,20, 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 } , {20,20,20,20, 20, 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 } , {20, 20, 20, 20, 20,20, 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 } , {20, 20, 20,20,2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 } , { 2 0 , 0 , 0,20,20,20, 2 0 , 2 0 , 2 0 , 2 0 , 4 0,60,80, 1 00, 1 2 0 , 1 20} , {20, 0, 0,20,20,20, 2 0 , 2 0 , 2 0 , 20 , 20, 20, 20, 20, 1 2 0, 1 2 0 } , { 2 0 , 0, 0,20,20,20, 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0, 20, 1 2 0, 1 2 0 } , {20,20,20,20,20,2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 4 0,60,80, 1 00, 1 2 0 , 1 20} , {20, 20, 20,20,2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0, 2 0 , 2 0 , 2 0 , 2 0 , 2 0 } , {20,20,20,20,60,60,60,60,60, 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 } , {20,20,20,20,60, 1 00 , 1 0 0, 1 00 , 6 0 , 2 0 , 2 0,20, 20, 40, 2 0 , 2 0 } , {20,20,20,20,60, 1 00 , 1 50, 1 00 , 6 0 , 2 0 , 2 0 , 2 0,40,40,20,20}, {20,20,20,20,60, 1 00 , 1 0 0 , 1 00 , 6 0 , 2 0 , 2 0 , 4 0 , 4 0, 40, 2 0 , 2 0 } , {20,20,20,20,60,60,6 0, 60,6 0, 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 } , {20,20,20,2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 2 0 , 20,20,20, 20,20,20, 20} }; f l oa t vi ewi ng_ang l e =O; i n t vi ewer_h e i g h t = 32 ; i n t xvi ew=6*64+48; continued on next page
GARDENS O F I MAG I NATI ON continutdfrom prroious page
i n t yvi ew=6*64; vo i d ma i n ( i nt a r g c , c h a r* a r g v [ ] ) { I I Read a rgume n t s f r om command l i n e i f p r e s e n t : i f ( a r gc>=2 ) v i e w i ng_a ng l e= a t o f ( a r gv [ 1 J ) ; i f ( a rgc >=3 ) v i ewer_h e i g h t =a t o f ( a rgv[ 2 J ) ; I I Load t ex t u r e m a p i ma g e s : i f ( l oadPCX ( " i ma g e s . p c x " , & t e x t ma p s ) ) e x i t ( 1 ) ; i f ( l o a d P C X ( " h i g h m a p s . p c x " , & h i g hma ps ) ) ex i t < 1 > ; I I Po i n t va r i a b l e a t v i deo memo r y : c h a r f a r * s c reen= ( c h a r f a r * ) MK_FP ( O xaOOO , O > ; I I S a v e pre v i ous v i deo mod e : i n t o l dmode=* ( i n t * ) MK_F P ( O x40,0x 49 ) ; I I S e t mode 1 3 h : s e t mod e ( Ox1 3 ) ; I I S e t t h e pa l e t t e : s e t pa l e t t e ( t e x tm a p s . pa l e t t e ) ; I I C l e a r t h e s c reen : c l s ( s c re en ) ; I I D r aw a r a y- c a s t v i e w of t h e ma z e : d r a w_ma z e (wa l l , f l o r , f l oo r h e i g h t , s c r e e n , x v i e w , yv i e w , v i ewi ng_ang l e , v i ewer_h e i g h t , t e x t m a p s . i ma g e ) ; I I Wa i t f o r u s e r t o h i t a key : w h i l e ( ! kb h i t ( ) ) ; I I R e l e a s e memo r y d e l e t e t e x t ma p s . i ma g e ; d e l e t e h i ghma p s . i ma g e ; I I R e s e t vi d e o mode a nd e x i t : s e tmode ( o ldmode ) ; }
To run the program, go to the HEIGHT directory and type BLOKDEMO. If you don't type any parameters on the command line, the view will default to 0 radians and a height of 32 units. You'll see the image depicted in Figure 1 0-6: a pair of staircases leading up to a platform. Want to see the platform from a higher angle? Hit a key to end the program and type BLOKDEMO 0 1 5 0. This will elevate your viewpoint to 1 5 0 height units above the Hoor (which is already 20 units tall) , as shown in Figure 1 0-7.
CHAPTER TE:N
Heightmdpping
Fig u re 1 0-6 The view due north from
Figure 1 0 - 7 T h e view from 1 50 height
the center of the heig htmapped maze
u n its above the floor
Remernher that swimming pool I menrioned earlier? It's righr behind you. Do an about face by typing BLOKDEMO out Figure
3 . 1 4 64 to rake a Iook at it (or just check
1 0-8). Another poinr of inrerest is the pyramid-like structure to the 4.7 1 92 (see Figure 1 0-9).
west. Take a Iook at it by typing BLOKDEMO
Now you're on your own. Experiment with various angles and heights. (Don't overlook that structure in the n o nhwest corner that Iooks vaguely like the Barplane.) When you ger rired of looking around, come back and read the rest of this chapter, where we'll discuss methods for making heightmapping even more versatile and useful.
H e i g htm a p ped C e i l i n g s I f you'd like to extend this system, an obvious place to starr is by heighrmapping the ceilings as weil as rhe Boors. Just as rhe Boor in a heightmapped maze can go up, so the ceiling can come down. This can be used to produce interesting effects, such as stalactite-like descending structures and skylighrs (which would simply be raised portions of ceiling with brightly colored graphics tiles mapped onro them) .
Figure 1 0-8 Not manv mazes have thei r
Fig u re 1 0-9 The pyra mid viewed from a
own swimming pools
lofty perch
G A R D E: N S OF IMAGI NATI ON
One problern this introduces is hidden surface removaL You must take care not to draw a distant portion of raised floor that would be blocked by a nearer section of lowered ceiling, or vice versa. This means that you'll need to draw the floor and ceiling rogether, checking each block of floor and the block of ceiling above it for height changes and drawing those changes before moving on to more distant blocks. You'll also need ro be careful that you don't lower the ceiling ro a Ievel below that of the floor (though lowering the two to the same Ievel, thus forming a wall, shouldn't be a problem) .
Ti l ed H e i g htm a ps Block-aligned heightmaps are simple to create, require relatively litde memory to store, and can be rendered on the display fairly rapidly, if not quite as rapidly as the single-height maze maps in the last chapter. But block-aligned heightmaps suffer from an obvious Iimitation: They only allow us to elevate square sections o f fl o o r. Thus the walls i n a m aze rendered with this technique are still o rthogonal - aligned with the m aze grid - and have the same minimum thickness as the maze cubes we used in earlier chapters. They allow us to create multilevel mazes, but they don't free us from the tyranny of the maze grid. The ideal heightmap would not be block-aligned but pixel-aligned - that is, it would allow every floor pixel to have a different height from the pixels araund it. With such a heightmap, we could create mazes containing three-dimensional structures in a wide variety of shapes. We could create slender walls aligned at unpredictable angles. We could create winding staircases and narrow passageways. Such a heightmap is theoretically possible. We could use a two-dimensional byte array in which each element corresponds to one pixel of the floor to srore the height values. You can probably anticipate some of the problems with this approach , though. The most glaring problern is the amount of memory that would be required to store such an array. Since it would contain a 1 -byte element for every point in the fine coordinate sytem, a 16 by 1 6 maze such as we've been using in our demos would require 64 x 1 6 by 64 x 1 6, or 1 million, bytes of storage . While this isn't entirely out of the question on current generation machines, most of which contain at least 4 to 8 megabytes of RAM, it does seem rather wasteful. And the size of the array would go up rapidly if the size of the maze were increased. A 32 by 32 maze would require 4 megabytes of heightmap storage, and a 64 by 64 array would require 16 megabytes. There's got to be a more efficient way of sroring the heightmap data. There is. We can tile heightmaps exacdy as we've been tiling texture maps. The floors of the mazes that we've created in this and the previous chapter have
C HAPTE:R TE:N
Heightmd p p i ng
been texture mapped at the Ievel of rhe fine coordinate system. Yet, by the simple expedient of repeating the same texture-map tiles over and over again across rhe fl.oor of the maze, we've reduced the amount of storage necessary for these texture maps to a small portion of that required to store a PCX image. We can do the same thing with heightm aps: Create a few heightmap riles and repeat them throughout the maze. The amount of storage required will be minimal, yet we'll be able to pull off heightmapping effects that would be impossible using block aligned heightmaps. Alas, there are orher problems presenred by rhe use of riled heighrmaps. Bur we'll discuss those problems in the course of developing a demo showing one possible way in which riled heightmaps might be used in a ray-casting engine.
c reati n g H e i g h t Ti l e s Before we can create a riled heightmapped version o f the draw_mazeO function, we need a method of creating height riles to use it with. Like the texture-map riles that we've been using, each heighr tile will be a 64 by 64 array of byre values. Typing such height data into an array could get tedious, since each array will have 4,096 entries. Bur there's a simpler way to create height tiles. We can create rhem exacdy as we create texrure-map tiles: with a paint program. How does one go about creating height riles with a paint program? In the HEIGHT directory, you'll find a PCX file named HIGHMAPS . PCX. Take a Iook at it using the PCXSHOW program from chapter 2 (see Figure 1 0- 1 0) . Like the PCX files containing our texrure-map riles, rhis file contains 1 5 heightmap tiles stored as 64 by 64 pixel images. The colors in which these riles have been painted aren't arbitrary. The palette number of each color represents the heighr of the floor pixel corresponding to that position in the heightmap. For instance, a pixel in a heightmap tile drawn in palette color 47 represents a fl.oor pixel that is 47 height units above the base floor Ievel. Because our ray-casting engine normally sees the
Figure 1 0-10 The heightmap ti/es in the HIGH MAPS PCX file
GARDENS OF IMAGI NAT I O N
pixel values in a bitmap as unsigned byte values, it won't even be necessary to translate the heightmaps into another format before using them. We'll simply store them as a PCX image and Iook up individual height values the same way we've been looking up individual pixel color values in our texture-map tiles. Wh ile the relative heights of pixels in the heightmap aren't obvious from looking at the PCX image, I've included slanted walls and even curved walls in this PCX file, to demonstrate the flexibility of tiled heightmapping.
T h e Ti l ed floo r h e i g h t l 1 Array We'll need to incorporate several arrays into the program to tell the ray-casting engine how to use these height tiles. The first, and most obvious, of these arrays will be the floorheight[} array. In our block-aligned heightmapping engine, we used the floorheight[} array to store the heights of the floor squares. In this tiled version, we'll use the floorheight[} array to store the numbers of the heightmap tiles to be used for the floor squares. A value of 5 in the floorheight[} array, for instance, means that the heightmap fo r the corresponding floor square is in height tile num ber 5 . Yo u'll note that the floorheight[} array serves the same purpose for heightmap tiles as our flor[} and wall[} arrays have served for texturemap tiles. Here's the floorheight[} array that we'll be using in our demonstration pro gram: m a p_t y p e f l oo r h e i g h t = { 0 , 0 , 0 , 0} , 0 , 0 , 0, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 } , { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, 0 , 0 , 0, 0 , 0 , 0 , 0 } , { 0 , 0 , 0 , 0,
0 , 0 , 0 , 0 , 0 , 0 , 0, 0,
{ 0 , 0 , 0 , 0,
{ 0 , 0 , 0 , 0 , 0, 3,
3, 3,
3,
3, 0 , 0, 0, 0 , 0, 0} ,
{ 0 , 0 , 0 , 0, 0 , 0 , 0 , 0 , 0 , 0 , { 0 , 1 , 0 , 0 , 0, 0 , 0 , 0 , 0 , 0 , { 0 , 1 , 1 , 0, 0, 0 , 0 , 0, 0, 0, { 0, 0 , 1 , 1 , 0, 0 , 0 , 0, 0, 0,
0 , 0, 0,
0 , 0, 0 } ,
0 , 0, 0 , 0 , 0 , 0 } , 0 , 0, 0, 0 , 0, 0}, 0 , 0, 5 , 1 0 , 0 , 0 } ,
{ 0,
0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, 6 , 1 1 , 0, 0 } ,
{ 0,
0 , 2 , 2 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, 0 , 0 , 0 , 0 } ,
{ 0,
2,
{ 0,
2 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, 0 , 0,
{ 0,
0 , 0 , 0,
0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, 0 , 0 , 0 , 0 } ,
{ 0,
0 , 0 , 0,
0, 0 , 0 , 0,
{ 0,
0 , 0 , 0 , 0, 0 , 0 , 0 , 0, 0 , 0, 0, 0 , 0 , 0 , 0} ,
{ 0, } ,.
2 , 0, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, 0 , 0 , 0 , 0 } ,
0 , 0 , 0 , 0, 0 , 0 , 0,
0, 0 } ,
0 , 0 , 0 , 0, 0 , 0 , 0, 0 } , 0 , 0 , 0 , 0,
0,
0 , 0, 0}
T h e floorba s e n Array We'll also create an array called floorbase[}, the purpose of which may not be readily ap parent. Essentially, the floorbase[} array will work exactly like the
CHAPTE:R TE:N
Heightmdpping
floorheight[} array in the block-aligned heightmapped engine. The numbers in the floorbase[} array will specify the heights of the individual maze squares. In the last program, we calculated the height of the Boor from an assumed base of 0. Now we'll give each Boor square an individual base value, as specified in the Boor base array. The actual heights of the individual Boor pixels will be calculated as the sum of the value for that square in the jloorbase[} array and the value for that pixel in the heightmap tile corresponding to that square. What's the point of maintaining a floorbase[} array? lt will allow us to reuse height tiles at various height I evels through o u t the maze. De sp i t e the vers a t i l i ty of the t i l e d heightmapping approach, most of the squares in a maze will still be Bat. But some Bat squares will be at low Ievels within the maze while others may be on top of towering balconies. There's no point in creating several Bat height tiles when we can simply raise the Boor base and use the same Bat tile at each Ievel. Similarly, tiles that represent walls rising from a Bat Boor can be used i n both low-lying portians of the maze and elevated portians of the maze, simply by raising the Boor base underneath the tiles. The Boorbase array that we'll be using in our demo program Iooks like this: map_type f l oorba se= { { 0 , 0, 0 , 0 , 0, 0, { 0 , 0 , 0 , 0 , 0, 0, { 0 , 0 , 0, 0, 0, 0, { 0 , 0 , 0, 0, 0 , 0 , { 0 , 0, 0, 0, 0, 0, { 0, 0 , 0 , 0 , 0, 0, { 0,20, 0, 0, 0, 0, { 0, 40,20, 0, 0, 0, { 0, 40,40,20, 0 , 0, { 0 , 40, 20, 0 , 0, 0, { 0 , 2 0, 0, 0, 0, 0, { 0, 0, 0, 0, 0, 0, { 0, 0, 0, 0 , 0 , 0, { 0, 0, 0, 0, 0, 0, { 0, 0, 0, 0, 0 , 0, { 0 , 0, 0, 0 , 0, 0, }.
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, 0}, Q}, 0}, 0}, 0}, 0}
,
The Ti le d fl o r l J a n d wa l l [ ] Arrays Finally, we'll maintain the flor[} and wall[} arrays from earlier versions of our raycasting engine. The flor[} array will store indices into a PCX file filled with texture maps, as it did in our earlier programs, but the meaning of the wall[} array will change. Previously, the wall[} array was used to store pointers to the texture-map tiles that were to be mapped onto the corresponding sections of wall. Now that we have the ability to create walls in any shape and oriented at any
GARDE:NS OF I MAG INATION
angle, texture mapping is considerably more difficult. When the walls were guaranteed to be aligned with the maze grids, we could find the appropriate column of the texture map by taking the x or y coordinate at which the ray struck the wall modulo 64. But how do we find the appropriate column of the texture map to map onto a diagonal wall? Or a curved wall? In this chapter we'll take the easy way out and drop the texture-mapped walls altogether, drawing the walls in solid colors instead. The number in the wall[} array for a given square will indicate what color the wall is to be drawn in. This allows us to perform a kind of rough-and-ready shading on straight and diagonal walls, but it doesn't work very well on curved walls, as you'll see in a moment. After we demoostrate the tiled heightmapping engine, 1'11 suggest some alternate methods that might be used for texture mapping heightmapped walls. Now let's develop a tiled heightmapping version of the draw_maze() function.
A Ti l e d H e i g htm a p p i n g E n g i n e Because height changes in a tiled heightmap are aligned at the Ievel of individual floor pixels rather than at the Ievel of the maze grid, we can no Ionger cast for walls by casting rays against x and y grid lines. The best way to cast for height changes would, in fact, be to test every pixel along the path of the ray to see if a height change has occurred, perhaps using a variation on Bresenham's algorithm to determine the path that the ray takes. Such a program wouldn't be terribly difficult to write, but it would be excruciatingly slow in execution. An alternative method would be to cast for height changes at the same time as we cast for the color of the floor pixels. This would not only be relatively fast, but it would be easy to write. We could drop the wallcasting code altogerher and write a tight floorcasting loop instead. The disadvantage of this method is that it can miss changes in floor height altogether, especially when the ray is more than a few maze squares away from the viewer and the angle of the ray becomes shallow. For the most part, this won't matter. If a wall or raised section of floor is thick enough, we'll pick it up a few floor pixels past the actual height change. The fact that the ray penetrates a slight distance into the wall won't be obvious on the screen. There's a second drawback to this method as well, but we'll talk about that later in the chapter. We'll use the second method in this chapter, not because it's perfect but because i t's relatively fast. A few years from now, about the time that Intel introduces the Octium microprocessor, the average PC should be fast enough to make the first method feasible. Here's the prototype for this latest version of the draw_maze() function: vo i d d r a w_m a z e ( ma p_t ype wa l l , ma p_type f l oor, map_type f l oo r h e i g h t , m a p_type f l oo r ba s e ,
CHAPTER TEN
Heightmdpping
c h a r f a r * s c reen, i n t x v i ew, i n t yvi e w , f l o a t v i ewi ng_a ng l e , i n t v i ewer_h e i g h t , c h a r f a r * t extmaps , c h a r f a r * h i g h ma p s )
This version passes the address of the buffer containing the heightmaps in the pointer highmaps and a pointer to the array of floorbase heights in floorbase. We'll place this prototype in the file TILEDEMO.H in the HEIGHT directory. The function will go in the TILEDEMO.CPP file in the same directory. This version of the draw_maze() function, like all of the other ray-casting versions, is structured as a large for() loop that iterates through all of the colurnns in the viewport. At the beginning of the loop, we calculate the viewing angle for the current column exacdy as in earlier versions. Then we calculate the viewer's height much as we did in the previous function, by determining which maze square the viewer is standing on and retrieving the corresponding value from the
Jloorheight[}: i n t xma z e = xvi ew/64; i n t yma z e = yvi ew/64; i n t f l oort i l e=f l o o r h e i g h t [xma z e J [yma z e J ;
This time, however, we use a somewhat different method o f retrieving the height of the pixel that the viewer is standing on, employing the same method that we've used in the past for finding the color of a ßoor pixel in a texture-map tile. Remernher that the heightmaps, like the texture maps, are 64 by 64 tiles in a 320 by 200 PCX image, arranged in three rows of five maps each. We can find the number of the row by dividing the tile number (in Jloortile) by 5 , get the size of a row by multiplying 320 (the screen width) by 64 (IMAGE_HEIGHT), and then multiply the tile number by the row size. To get the position of the tile, we take the tile number modulo 5 , then derive the row offset by multiplying this number by 64 (IMAGE_WIDTH). Finally, we obtain the actual position wirhin the tile by taking yview modulo 64 (IMAGE_HEIGHT) , multiplying it by 320 (the screen width) , and adding in xv{ew modulo 64 (IMAGE_WIDTH) . Whew! Here's the resulting instruction: i n t c u r r en t h e i g h t=h i g hmap s [ ( f l o o r t i l e / 5 ) *320*IMAGE_H E I G HT + ( f l oo r t i l e % 5 ) * 1 MA G E_W I DTH + ( yv i e w % I MA G E_H E I G H T ) *320 + ( x v i ew%IMAGE_W I DT H ) J ;
We'll cast for the ßoor pixels exacdy as we did i n the ray-casting engine we developed in the last chapter. However, we'll include code in the floorcasting loop to abort the casting when the edge of the map is reached: i f ( ( xmaze= G R I D W I D T H ) I l < ymaze=G R I D W I D T H ) ) break;
GARDE:NS OF I MAG I NATI O N
As in the first program in this chapter, we can no longer depend on there being a wall to stop us at the edge of the maze. We'll determine the height of floor pixels much as we determine the color of floor pixels. First we'll calculate a value t that represents the column in the height tile corresponding to the current floor pixel: i n t t = ( ( i n t ) y & O x 3 f ) * 320 + ( ( i n t ) x & Ox 3 f ) ;
Then we'll store the number of the height tile for the current floor pixel in the variable tile: i n t t i l e= f l oo r h e i g h t [xma z e J [ yma z e J ;
Finally, we'll calculate the offset o f the relevant byte of height data wirhin the PCX image comaining the heightmaps: u n s i gned i n t t i l e p t r = ( t i l e / 5 ) * 320* IM AGE_H E I G HT+ ( t i l e % 5 ) * I M A G E_W I DT H + t ;
To get the actual height of the floor pixel, we need to add the height value fro m the heightmap file and the floorbase[} array value for the maze square containing the floor pixel: i n t newhe i g ht= h i g hmap s [ t i l e p t r J + f l o orba s e [ xma z e ] [yma z e J ;
I f the floor level has gone down, the wall i s facing away from us, s o n o wall needs to be drawn. All we need to do is reset the floor height and keep casting: i f ( n ew h e i g h t < c u r r e n t h e i g h t ) c u r rent h e i g h t= newhe i g h t ;
The other possibilities, o f course, are that the floor level has gone u p o r that it has not changed at all. In the latter case, we do nothing and cominue casting. If the floor has gone up, then we prepare for drawing the wall: else { i f ( n ew h e i g h t > c u r r e n t h e i g h t ) {
We still need to reset the floor height: c u r r e n t h e i g h t =new h e i g h t ;
We can use the similar triangles method t o find the screen row corresponding to the top of the wall: ra t i o = ( f loa t ) V I E W E R_D I STANC E / r ea l_d i s t a n c e; i n t a = r a t i o * ( v i e w e r_h e i g h t - c u r r e n t h e i g h t ) ; i n t newrow V I EWPORT_C E N T E R + a ; =
We can then calculate the offset i n video memory to begin drawing the wall at and draw pixels from there to the screen row corresponding to the bottom of the wall:
CHAPTER TEN
Heightmd p p i n g
o f f s e t = row*320+co l umn; for
( i nt
i = row;
i > newrow; - - i )
{
s c r e e n ( o f f s e t J =wa l l [ x m a z e J [ ym a z e J ; o f f s e t -=320; }
Note that the color for the wall is taken from the wall array entry corresponding to the current maze square. Once the wall is drawn, we can reset the row variable to the screen row direcdy above the top of the wall: row=new row; }
Whether or not there was a change in Boor height, we m ust draw the Boor pixel at the current positio n. However, we must first check to see that the Boor pixel is visible - that is, that it is below the viewer's eye level, which is presumed to be at V1EWPORT_CENTER: if
( row > V I E W PO RT_C E NT E R )
{
The rest of the Boorcasting - which is the rest of this function - proceeds exacdy as it did in the ray-casting engine in the last chapter.
The Ti led H e i g htm a p p ed d raw_m a z eo Fu ncti o n The complete text o f the tiled heightmapped version of the draw_maze function appears in Lisring 1 0-3.
Listing 1 0-3 The ti l ed heig htma pped d raw_mazeo fu nction II I I T I LE C AST . C P P II
D r a w s a t i l ed h e i g h tmapped t h r ee-d i me n s i o na l m a z e
I I W r i t t en b y C h r i s t o p h e r Lampton II
f o r Ga r d e n s of
I ma g i n a t i o n ( Wa i t e G roup P r e s s )
# i n c l ude < s t d i o . h> # i n c l ude # i n c l ude " t i L e c a s t . h " # i n c l ude " p c x . h " con s t WALL_H E I GHT=64;
I I H e i g h t of wa l l
c o n s t V I E W E R_D I STAN C E=1 2 8 ;
II V i ewe r d i s t a n c e f r om s c r e e n
i n p i xe l s
con s t VI E W PO RT_LE FT=O;
II
D i men s i on s o f v i ewpo r t continued on next page
GARDENS O F I MAG I NATION continuedfrom previous page
c o n s t V I E W PORT_R I GHT=3 1 9 ; c o n s t V I EW PORT_TOP=O; c o n s t V I EW PORT_BOT= 1 99; c o n s t V I E W PORT_H E I G HT=V I EW PORT_BOT-V I EW PORT_TO P ; const
V I E W P O RT_C E N T E R =V I EW PORT_TOP+V I EW PO R T_H E I GH T I 2 ;
const
Z OOM=2 ; c o n s t G R I D W I DT H = 1 6 ; c o n s t W A L L C O L O R= 1 8 ; vo i d d r a w_ma ze ( m a p_type wa l l , m a p_t ype f l o o r , ma p_type h i g h t i l e , map_t y p e f l oorba s e , char
f a r * s c r e e n , i n t x v i e w , i n t yv i ew,
f l oa t v i ew i n g_a n g l e , i n t v i e w e r_he i g h t , c h a r f a r * t e x t ma p s , c h a r
f a r * h i g h ma p s )
{ i n t y_u n i t , x_un i t ;
II Variables II
f o r a mo u n t o f c h a n g e
i n x a nd y
i n t d i s t a n c e , rea l_d i s t a n c e , o l d_d i s t a n c e , xd , y d , s x , sy ; unsi gned II
i nt o f f s e t ;
Loop t h r o u g h a l l
for
( i nt
II
c o l um n s o f p i x e l s
c o l umn=VI EWPORT_L E F T ;
C a l c u l a t e h o r i z o n t a l a ng l e of
II
center
f l oa t
i n v i e w po r t :
c o l umn
II
C a l c u l a t e a ng l e of
f l oat
co l umn_a ng l e=0 . 0001 ; r a y r e l a t i ve
to m a z e coord i n a t e s
r a d i a n s =v i e w i n g_a n g l e+ c o l umn_a n g l e ;
I I W h i c h s q u a r e i s t h e v i e w e r s t a nd i n g o n ? x v i ewl64;
i nt xmaze
i n t yma ze = yv i e w l 6 4 ; II
Get
i nt
po i n t e r t o f l oor h e i g h t ma p :
f l oo r t i l e= h i g h t i l e [ xma z e J [ yma z e J ;
I I How h i g h i nt
is
t h e f l o o r u n d e r t h e v i ewe r ?
c u r r en t h e i g h t = h i g hma p s [ ( f l o o r t i l e i 5 ) *320* I MAGE_H E I GHT + ( f l o o r t i l e % 5 ) * 1 MA G E_W I DT H + ( yv i e w% I MA G E_H E I G HT ) *3 2 0 + ( xv i e w % I M A G E_W I D TH ) J ;
II
F i r s t s c r e e n row t o d ra w :
int II
row=V I EWPORT_BO T ; Cast a
for
I I Get
r a y a c ro s s t h e f l oor : { r a t i o of v i e w e r ' s h e i g h t
to p i x e l
he i g h t :
{
C HAPTER TEN f l oat if
s c r een_h e i g h t = row-V I EWPORT_C E N T E R ;
( s c reen_h e i gh t ==O . Q )
f l oa t II
Heighlmd pping
s c r een_h e i g h t = . 0000 1 ;
r a t i o= ( f l oa t ) ( v i e w e r_h e i g h t - c u r r e n t h e i g h t ) l s c r een_h e i g h t ;
Get d i s t a n c e t o p i x e l :
rea l_d i s t a n c e= r a t i o*V I EW E R_D I STAN C E ; if
( r e a l_d i s t a n c e>o l d_d i s t a n c e+S )
d i s t a n c e+=S ;
d i s t a n c e = r e a l_d i s t a n c e l c os ( c o l umn_a ng l e ) ; I I Rot a t e d i s ta n c e t o ray ang l e : - di stance *
i nt x
i n t y = di stan ce *
( s i n ( ra d i a n s ) ) ;
( c os( radi an s ) ) ;
I I T r a n s l a t e re l a t i ve to v i e w e r coo rd i na t e s : x+=x v i ew; y+=y vi ew; II
Get ma z e
i n t xma z e
squa r e i n t e r s e c t e d by
ray :
x I 64;
i n t yma z e = y I 6 4 ; II
I f we ' ve r e a c h e d t h e edge o f
if
( ( xm a z e =G R I DW I DTH )
the map, q u i t :
I I ( yma z e =G R I D W I D TH ) ) break; II
F i n d r e l ev a n t c o l umn o f
i nt
t = ( ( i n t ) y & Ox3 f )
t e x t u r e a n d h e i g h t maps :
* 320 + ( ( i n t ) x & O x 3 f ) ;
I I W h i c h h e i g h t t i l e a r e w e u s i ng ? i n t t i l e=h i g h t i l e [ x ma z e J [ yma z e J ; II
F i n d o f f s et o f t i l e and co l umn
i n b i tma p :
uns i gned i n t t i l ep t r= ( t i l e i 5 ) *320 * I MA G E_H E I G HT+ ( t i l e % 5 ) * I MAGE_W I DTH+ t ; I I G e t h e i g h t o f p i xe l : i n t newhe i g h t = h i g hmaps [ t i l e pt r ] + f l o o r ba s e [ x ma z e J [ yma z e J ; II
Ha s t h e f l o o r
if
( n ewhe i g h t < c u r r e n t h e i g h t )
L e ve l
gone down? c u r r en t h e i g h t =new h e i g h t ;
e l se { II
H a s t he f l oor
if
( newhe i g h t > c u r r e n t h e i g h t ) II
I f so,
set
L e ve l gone up? {
new f l o o r h e i g h t :
c u r r e n t h e i g h t =new h e i g h t ; II
I f s o , ca l c u l a t e new s c reen po s i t i on :
ra t i o =
( f l oa t ) V I EW E R_D I STAN C E i r ea l_d i s t a n c e ; continued on next page
GARDENS OF I MAGI NATION continunewrow;
-- i )
{
s c r e e n [ o f f s e t J=wa l l [ xma z e J [ yma z e J ; o f f s e t -=320; } II
Set
s c r e e n r o w to n e w po s i t i on :
row=n e w r o w ; } II
If
if
( r ow > V I E W P O R T_C E N T E R )
vi ewer i s
L oo k i ng d o w n on f l o o r , d r a w f l oor p i x e l : {
I I W h i c h g r a p h i c s t i l e a r e we u s i ng ? t i l e=f l oo r [ xma z e J [ ym a z e J ; II
F i nd o f f s e t o f
t i l e a nd c o l umn i n b i t ma p :
t i l e pt r = ( t i l e i 5 ) *3 2 0 * I M A G E_H E I G HT+ ( t i l e % 5 ) * I MA G E_W I DTH+ t ; I I C a l c u l a t e v i deo o f f s e t o f
f l oor p i x e l :
o f f s e t = row*320+ c o l um n ; I I D ra w p i x e l : s c r e e n [ o f f s e t J = t e x t m a p s [ t i l ep t r J ; I I Adva n c e t o n e x t s c r e e n
L i ne :
row--; } } } } }
T h e T I L E D E M O . C P P P ro g ra m To demonstrate this new ray-casting function, we'll call it from a short program called TILEDEMO .CPP. The complete text of this program is in Listing 1 0-4.
II
T I LE D E M O . C P P
II II
Ca l l s
r a y- c a s t i n g f u n c t i o n to d r a w v i e w o f
C HAPTER TEN II
Heightmd p p i n g
t i l ed h e i g h t-mapped ma z e .
II I I W r i t t e n by C h r i s t o p h e r L a m p t o n f o r I I Ga rdens o f
I ma g i n a t i on
# i n c l ude
< s t d i o . h>
#i n c l ude
# i n c l ude
# i n c l ude
< s t d l i b . h>
# i n c l ude
# i n c l ude
'' s c r e e n . h "
# i n c l ude
" pcx . h "
# i n c l ude
" t i lecast . h"
( W a i t e G roup P r e s s )
const N U M_I MA G E S = 1 5 ; p c x_s t r u c t
t e x t ma p s , h i g h m a p s ;
map_type wa L L = { { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, { 0 , 0 , 0 , 0 , 0 , 0 , 0,
0,
0,
0,
0, 0 , 0 , 0,
0, 0 , 0,
0, 0},
0, 0 , 0 , 0 , 0},
{ 0, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 } , { 0, 0, 0 , 0, 0 , 0, 0, 0, 0 , 0 , 0, 0 , 0 , 0 , 0, 0}, { 0, 0, 0, 0, 0 , 0 , 0, 0, 0 , 0 , 0, 0, 0 , 0,
0,
Q},
{ 0,28, 0, 0, 0 , 0, 0, 0, 0 , 0 , 0, 0 , 0 , 0,
0,
Q},
0, 0 , 0 , 0,
0,
0},
0 , 0, 1 8 , 1 8 , 0 ,
0},
0,
0},
{ 0 , 2 8 , 28,
0,
0, 0,
0,
0, 0 , 0,
{ 0 , 2 8 , 2 8 , 28, 0 , 0 , 0 , 0 , 0 , 0 , { 0 , 2 2 , 2 2 , 2 2 , 0 , 0 , 0,
0,
0,
0,
0,
0 , 1 8 , 1 8,
{ 0, 1 7, 1 7, 1 7, 0, 0, 0, 0, 0 , 0,
0, 0, 0 , 0, 0,
0},
0, 0, 0 , 0 , 0,
0 , 0 , 0 , 0 , 0,
0},
{ 0 , 1 7, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
0, 0 , 0 , 0, 0,
0},
0 , 0, 0 , 0 , 0 , 0 , 0 , 0 ,
0, 0, 0 , 0, 0,
0},
{ 0,
0, 0, 0, 0, 0 , 0 , 0, 0 , 0,
0, 0, 0 , 0, 0,
0},
{ 0,
0, 0, 0, 0, 0 , 0 , 0, 0 , 0,
0 , 0 , 0 , 0 , 0,
0},
{ 0, 1 7 , 1 7 , { 0 , 0,
{ 0, } ,.
0,
0,
0 , 0 , 0 , 0 , 0 , 0,
0, 0 , 0 , 0 , 0 , 0 , 0, 0, 0}
m a p_type f l o r ={ { 0 , 0 , 0 , 0 , 0,
0, 0, 0, 0 , 0,
{ 0, 0 , 0, 0, 0, 0, 0, 0, 0,
0,
0, 0 , 0 , 0,
0},
0, 0, 0 , 0, 0,
0},
0,
{ 0, 0 , 0, 0, 0, 0, 0, 0, 0 , 0 , 0 , 0 , 0, 0, 0, 0}, { 0, 0 , 0, 0 , 0 , 3, 3, 3, 3,
3,
{ 0, 0, 0, 0, 0, 0, 0, 0, 0 , 0 , { 0,
0, 0, 0, 0, 0, 0}, 0, 0, 0, 0, 0, 0}, 0, 0, 0, 0, 0},
1 , 0, 0 , 0, 0, 0, 0, 0 , 0,
0,
0,
0,
0, 0, 0, 0, 0},
1 , 0, 0, 0, 0, 0, 0,
0,
0,
{ 0,12,
1 , 0, 0 , 0 , 0, 0, 0,
{ 0, 1 2 , 1 2 ,
5 , 1 0, 0 , 0},
{ 0, 1 2 , 1 2 , 1 2 , 0, 0, 0, 0, 0, 0, 0 , 0 , 6, 1 1 , 0 , 0}, 0,
0,
0, 0, 0, 0, 0},
{ 0 , 1 2 , 2 , 0 , 0 , 0, 0, 0, 0 , 0,
0,
0, 0, 0 , 0 , 0},
{ 0, 2 , 0 , 0 , 0, 0 , 0, 0, 0 , 0, 0,
0, 0 , 0 , 0, 0},
{ 0, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
0, 0 , 0 , 0, 0},
{ 0 , 1 2 , 1 2 , 2 , 0 , 0 , 0 , 0,
{ 0, 0 , 0 , 0 , 0 , 0,
0,
0,
0,
0, 0, 0, 0 , 0 , 0 , 0 , 0}, continued on next page
�
G A R D E N S O F I MAGI NATI ON continuedfrom previous page
0,
0,
0 , 0,
0,
{ 0, } .
0,
0 , 0,
0 , 0 , 0 , 0,
{
0,
0, 0, 0 , 0, 0 , 0, 0 , 0 , 0 , 0}, 0, 0,
0 , 0 , 0 , 0 , 0 , 0}
,
m a p_t y p e f l o o r h e i g h t = { { 0,
0,
0 , 0 , 0 , 0,
{ 0,
0,
0 , 0,
0 , 0 , 0, 0, 0 , 0,
0,
0,
0, 0,
0,
{ 0, 0,
0 , 0,
0, 3, 3,
{
0 , 0, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0}, 0, 0 , 0 , 0 , 0 , 0},
0, 0 , 0 , 0 , 0 , 0 , 0 , 3, 3 ,
0,
0, 0 ,
0} ,
3, 0, 0, 0, 0, 0,
0},
0, 0, 0 , 0 , 0 , 0, 0 , 0,
{ 0,
0,
{ 0,
1 , 0,
0,
0 , 0,
0, 0 ,
{ 0,
1,
1,
0,
0,
0, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
{ 0,
0,
1 ,
1 , 0 , 0 , 0 , 0 , 0 , 0, 0 , 0 ,
{ 0,
0,
0, 0, 0,
0 , 0 , 0 , 0 , 0, 0 , 0 , 6 , 1 1 , 0 , 0},
{ 0,
0,
2,
2, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0,
2,
2, 0, 0,
{ 0,
2 , 0 , 0 , 0 , 0,
{ 0,
0,
{ 0, 0, { 0, { 0, } ,.
0,
0, 0, 0, 0, 0, 0, 0, 0},
0, 0, 0, 0, 0, 0, 0, 0 , 0,
0, 0 , 0 , 0 } ,
0 , 0,
0,
0,
0},
5, 1 0, 0, 0},
0, 0, 0, 0}, 0,
0, 0 , 0},
0, 0, 0, 0, 0, 0, 0,
0 , 0, 0 , 0 } ,
0 , 0 , 0 , 0 , 0, 0 , 0 , 0 , 0 , 0 ,
0, 0 , 0, 0},
0 , 0, 0 ,
0 , 0 , 0 , 0, 0,
0,
0,
0,
0,
0,
0,
0, 0 , 0 ,
0,
0 , 0 , 0 , 0 , 0 , 0 , 0,
0 , 0 , 0,
0, 0, 0, 0}
0, 0 , 0 } ,
m a p_t y p e f l oo r ba s e = { 0 , 0 , 0,
0,
0, 0,
0 , 0, 0, 0 ,
0, 0, 0,
{ 0 , 0 , 0 , 0,
0,
0,
0,
0,
0 , 0, 0, 0 ,
0, 0 , 0 , 0},
0 , 0,
0,
0 , 0 , 0,
0 , 0 , 0, 0 ,
0,
0,
0 , 0,
0, 0, 0, 0,
0, 0, 0 , 0},
0,
0, 0,
0,
0, 0 , 0 , 0},
{ 0, 0, { 0, 0,
{ 0 , 0 , 0 , 0 , 0, { 0 , 0,
0 , 0,
{ 0, 0,
0 , 0, 0,
0 , 0 , 0 , 0 , 0 , 0 , 0,
{ 0,20,
0 , 0,
0,
0,
{ 0 , 40 , 20 , 0,
0,
0, 0,
0, 0, 0 ,
0, 0, 0, 0,
0},
0, 0, 0},
0, 0 , 0 , 0}, 0, 0, 0 , 0},
0,
0 , 0 , 0,
0,
0, 0 , 0 , 0, 0, 0 , 0},
{ 0 , 4 0 , 40 , 2 0 , 0 ,
0 , 0, 0,
0,
0 , 0,
{ 0 , 40 , 2 0 , 0 , 0 ,
0 , 0, 0,
0 , 0, 0, 0 , 0 , 0 , 0 , 0},
0 , 0 , 0 , 0 , 0 , 0,
0 , 0 , 0, 0 , 0 , 0 , 0 , 0},
{ 0,20, { 0,
0,
0 , 0 , 0,
0 , 0 , 0 , 0,
0,
0, 0, 0,
0,
0,
0, 0 , 0 } ,
0, 0, 0},
{ 0 , 0 , 0 , 0 , 0,
0 , 0,
0,
0 , 0, 0, 0 , 0 , 0 , 0 , 0 } ,
{ 0 , 0 , 0 , 0, 0,
0 , 0,
0,
0 , 0 , 0 , 0 , 0 , 0 , 0,
{ 0 , 0 , 0 , 0 , 0,
0, 0, 0,
{ 0, 0, 0, 0, 0,
0, 0,
0,
0, 0, 0, 0,
0},
0, 0, 0 , 0},
0, 0 , 0 , 0 , 0 , 0 , 0 , 0 }
}; f l o a t v i e w i ng_a ng l e = O ; i n t v i e w e r_h e i g h t = 1 5 0 ; i n t x v i ew=8*64+32 ; i n t y v i ew=7*6 4 ; vo i d ma i n ( i n t a r g c , c h a r* a r g v [ J ) { II
R e a d a rg u me n t s f r om c ommand
if
( a r g c>=2 )
v i e w i ng_a n g l e=a t o f ( a rgv [ 1 J ) ;
if
( a rg c>=3 )
v i e w e r_he i g h t = a t o f ( a rgv [ 2 J ) ;
L i ne
i f p r e s en t :
CHAPTER TEN
Heightmdpping
I I L o a d t e x t u r e map i ma g e s : if
( l o a d PC X ( " i ma g e s 2 . p c x " , & t e x t m a p s ) )
if
C l o a d P C X ( " h i g h m a p s . p c x " , & h i ghmaps ) )
exi t ( 1 ) ; ex i t ( 1 ) ;
I I Poi n t va r i a b l e a t v i deo memo r y : c h a r f a r * s c r een= C c h a r f a r * ) MK_F P C Ox a O OD , D > ; I I S a ve p r ev i o u s v i deo mod e : i n t o l dmode=* ( i n t * ) MK_F P C 0 x 4 0 , D x 49 ) ; I I S e t mode 1 3 h : s e tmode ( Ox 1 3 ) ; I I Set
t h e pa l e t t e :
se t pa l e t t e ( t e x tmaps . pa l e t t e ) ; II
C l e a r t h e s c re e n :
c l s ( s c r ee n ) ; II
Draw a
r a y c a s t v i ew o f t h e ma z e :
d r a w_ma z e ( wa l l , f l o r , f l o o r h e i g h t , f l oo r b a s e , s c r e e n , x v i e w , yv i ew , v i e w i ng_a ng l e , v i e w e r_h e i g h t , t e x t m a p s . i ma g e , h i g hm a p s . i ma g e ) ; I I Wa i t f o r u s e r t o h i t a
key :
wh i l e ( ! k bh i t C ) ) ; I I Re l ea s e memo ry de l e t e t e x t m a p s . i ma g e ; de l e t e h i g h m a p s . i m a g e ; I I R e s e t vi deo mode a n d e x i t : se tmode ( o l dmode ) ; }
To run the program, go to the HEIGHT directory and type TILEDEMO. You'll see the circular formation in Figure 1 0- 1 1 . Type TILEDEMO 3. 1 4 and yo u'll see the we dge-shaped s tructure in Figure 1 0 - 1 2 . And if you type TILEDEMO 1 . 57, you'll see the series of circular posts in Figure 1 0- 1 3. The texture-map tiles for the floor are taken from the files I MAGES2. PCX in the HEIGHT directory. They were created specifically to go with the height tiles in HIGHMAPS.PCX. Type TILEDEMO 3 . 1 4 250 to get a look at the wedge-shaped structure from a considerable height, and you'll see another flaw in this method of casting for height changes (see Figure 1 0- 1 4) . The front of the wedge-shaped structure, which should be partially obscured by the bottom of the viewport, will move back until it's flush with the bottom. Why does it do this? Because we didn't cast for height changes between the viewer and the bottom of the viewport. The
GARDENS O F IMAG I N AT I O N
Figure 1 0- 1 1 A circu lar formation made
Figure 10-1 2 A wedge-shaped structure
from height tiles
Figure 1 0-1 3 A row of circular posts
Figure 1 0-1 4 Height changes between the viewer and the bottom of the viewport are missed by TILE D E M O
program assumes that no such changes occur. We didn't cast for such height changes because that would slow down the code considerably, and would make the function considerably more complicated to write. Similarly, if any height changes are obscured by a closer wall, they will be missed by the floorcasting routine. One solution to this problern might be to perform spot checks for height changes below the bottom of the viewport and behind walls, but slender walls (or the corners of walls) might still be missed.
H y b rid H e i g ht m a p p i n g M et h o d s Which, i f any, o f the above methods are programs like Doom using? Probably neither. lt's clear from the visual effects that it achieves that Doom is not using block-aligned heightmapping, a method that can provide some quick and dirty heightmapping tricks but is too limited ro depict walls that aren't aligned at 90 degrees to the maze grid or that are less than a maze square in thickness. And it probably does not use tiled heightmaps, which have flaws that only the CPU intensive solutions suggested in the last paragraph would solve.
CHAPTER TEN
Heig htmd p p i ng
There are, however, heightmapping algorithms that combine the elements of both techniques. There's not enough space in this book to cover them, but you might want to explore them on your own. Basically, these hybrid methods would specify height changes on a block-aligned basis, so rhat it would only be necessary to check for height changes at block boundaries. B ur the height changes themselves would not necessarily have to take place at those boundaries. While only one height change might be allowed per block, that height change could slash across the middle of the block at an arbitrary angle rather than along the edges of the block. Each entry in the walls[J array could contain a flag indicating that a height change takes place in that block. If one does, the entry would also conrain the slope at which that change is arienred relative to the maze grid and the coordinates at which it crosses the edges of the block (or a pointer to a separate structure that contains this information) . A more ambitious system would allow each block to conrain a poinrer to a linked list that could conrain an unlimited number of height changes, all of which occur in that block. S uch a system could simulate a tiled heightmap system, but would run much faster. Is this what the Doom programmers have clone? I have no idea. However, it should be possible to duplicate most if not all of the floor and wall effects in Doom by using such a system.
An i mated H e i g htma p s Once you've developed a heightmapping system, there's no reason that you should stop there. There are a number of special effects that can be achieved by animating the heightmaps. For instance, you could raise and lower heightmaps dynamically to create an elevator. In a block-aligned heightmap, this is only a matter of changing the number in a single position in the floorheight[} array, assuming that the elevator occupies a single maze square. (For larger elevators, just change the numbers in several different positions.) By increasing the floor height on every frame, a section of floor will appear to detach itself and rise toward the ceiling atop a thick column. If the player character is standing on that block, he or she will automatically be lifted along with it. (This will be handled automatically by the ray-casting code in this chapter, which always checks the height of the floor pixels beneath the viewer's feet before casting the image.) Reducing the number in the height array corresponding to a maze square will cause the floor of that square to go down, even below the height of surrounding squares if those squares are higher than the base level. Changing the floor level more rapidly than this creates a trapdoor over a hidden pit. When the unsuspecting player character steps on rhat square, lower it by a few dozen height levels - and the viewer will seem to plunge in after it. You
G A R D ENS OF IMAGI NATION
can then lower his or her hit points to represent darnage from the fall, if that strikes you as appropriate. And you can use texture mapping to write messages on the walls of the pit that the player cannot see until he or she takes the plunge, perhaps representing important clues toward completing the game. I f you're using a tiled heightmapping system, raising or lowering the floor need not be any more complicated than with a block-aligned system. To raise a section of floor, you can change the Ievel in the jloorbase[} array rather than altering one of the height tiles. If you want your elevator to have nonaligned walls, you can maintain a sequence of height tiles that represent the floor's height at various stages in the process. And if this requires too many height tiles for the available m e m o ry in the co m p u te r, you can actually modify the height tile itself dynamically. Once you get into dynamically modifying height tiles, even more bizarre effects become possible. For instance, the floor can morph from one shape to another, going from flat to pyramidal, or sinking to form a bowl-like pit with sloping edges. Use your imagination. With heightmaps, the sky's the Iimit!
ight is the soul of ray casting. By following beams of light from the viewer's eye through an array representing a maze, we can produce remarkably realistic visual effects. So far, however, we've only modeled the way in which light moves through the maze. We've made no attempt to cap ture the way in which light intensities vary across the surfaces in the maze. This may not sound terribly important, bur by giving our ray-casting engine the ability to dynamically alter the brightness of surfaces according to the ways in which rays of light strike them - a process known as lightsourcing we can greatly enhance the realism of the images that it produces. Our murky mazes will really Iook murky, with shadows filling distant hallways and corners of large rooms. Distant surfaces can be made darker than nearer ones, a visual cue that will make our mazes look even more three-dimensional than they already do. Pools of light can be placed around lamps and torches. Flashes of light can accompany the explosion of a fireball. And lightsourcing can also be used ro create visual effects that at first glance seem to have little to do with light, such as filling the maze with fog or even water. Best of all, lightsourcing is fairly easy to add to a ray-casting engine and the cost in added CPU cycles is small. -
G A R D ENS OF I MAG I NATION
C a sti n g Sa m e Li g ht o n Li g ht Light is a form of radiation. That doesn't mean that you should don a radiation suit the next time you head to the beach (though some SPF-30 sunblock might not hurt) . It simply means that light radiates outward in all directions from the source that's producing it, as in Figure 1 1 - 1 . We can imagine that the wavefront of photons leaving a light source at any given instant in time is an expanding sphere, growing larger at the speed of light (see Figure 1 1 -2). If there's nothing standing in its way, this sphere of light can expand for a very long time. When astronomers turn their telescopes on dis tant quasars, they're catching a tiny portion of an expanding light sphere that left its source more than ten billion years ago. That's a long time to spend traveling in the icy vacuum of space. If these panides of light happen to be heading in yo ur direction and you happen to be looking toward the light source when they reach you, some of them will strike your eyes. If enough of them strike at one time, they will activate photosensitive receptors and trigger a visual signal in your brain. This visual signal tells you that you're looking toward a light source. You will, quite literally, see the light. How bright the light appears depends mostly on three factors: how much light the light source is producing, how much of the light gets absorbed or knocked aside on its way to your eyes, and how far away you are from the source. We'll talk about all of these factors in the course of this chapter, but for now we'll concentrate on the third: distance.
Fig u re 1 1 -1 Light spreads out i n a l l d i rections from its sou rce
Figure 1 1 -2 The photans leaving a light source at anv given instant form an expanding sphere of light araund the source
C HAPTE:R ElEVEN
Lightso u rcing
As the sphere of light expands, the panides making up the sphere grow fanher apart. When the sphere reaches your eye, fewer panides hit your eye and the visual signal in your brain grows weaker, making the light seem dimmer. This is why stars, the brightest light sources that you can see with your naked eyes, Iook a lot dimmer than the feeble streedamps that routinely drown out their light. Most of the l ight that reaches our eyes doesn't come direcdy from a light source. It is reflected off the surfaces of other objects. The amount of light reaching these surfaces also depends on how dose they are to the light source. The doser a surface is to a light source, the more light it reflects and the brighter it appears to be. The farther away it is from a light source, the darker the surface appears. This effect won't be obvious when you're outside on a sunny day, since most of the objects that you'll see are roughly equidistant from their primary light source: the sun. Bur it's immediately apparent in a room that is illuminated by a single light source, such as a lamp. Light will seem to pool in the vicinity of the lamp, but will fall off into dark shadows in distant corners. What does this have to do with maze games? A lot of games take place in dark, dank dungeons, which are presumably illuminated only by a torch carried in the hand of the player character. Presenting such mazes in full illumination, as we have clone up until now, isn't really realistic. In the real world, the player would find that surfaces in his or her immediate vicinity are more brighdy illuminated than surfaces in the distance. In a !arge room or a long corridor, distant walls may be so dimly lit as to not be visible at all. To the player, the light of the torch would seem to form a circular pool of brighrness that dims rapidly into the distance (see Figure 1 1 -3).
Figure 1 1 -3
The light of the torch forms
a circular pool araund the player, dimming into the distance
GARDENS OF I MAGI NATI O N
We can simulate this effect in aur ray-casting engine by making distant walls darker and by making the pixels in bath the flaar and ceiling grow dimmer as they recede from the viewer. This addition won't even take a lot of extra code. lt will, hawever, require at least some understanding af how the intensity of light decreases with distance.
Th e I nverse S q u a re Law A mament ago, I campared the wavefront af photons leaving a light source to an expanding sphere with the light saurce at its center. The density of photans an the "surface" of this sphere determines how bright the light source will appear to an abserver. Because the photans spread out as the sphere expands, the light saurce appears dimmer with distance. A little thought tells us that there's a simple rule gaverning the rate at which the light dims. Figure 1 1 -4a depicts a light sphere with a radius af r. If you were standing at a distance a f r fro m the l ight source producing this sphere, your eyes would intercept phatons from the surface of this sphere. The small square on the surface af the sphere represents the sectian of the sphere that strikes, and emers, your eyes. (I've exaggerated the size af this square to make it easier to see.) Figure 1 1 -4b represents a light sphere with a radius af 2 x r, twice that of the sphere in Figure 1 1 -4a. The tiny square has expanded to a square with twice the height and twice the widrh (and therefare four times the area) of the square in Figure 1 1 -4a. H owever, if you were standing at a distance of 2 x r from the light source, your eyes - which haven't grawn any !arger - wauld anly intercept rhe photons in
Radius r
� Araa a
Figure 1 1 -4a
An expanding l i g ht sphere.
Figure 1 1 -4b
The same sphere at twice
The square patch is the area intercepted
the distance. The square patch has
by vour eves
expanded to four times its previous area
CHAPTER ELEVEN
Lig htsou rcing
the smaller dotted square inside of the large square. This square contains only one-fourth as many phorons as the original, since the photons from the original square have now spread out to form the !arger square. Thus the light source would appear only one-fourth as bright. The same thing would happen if you were standing 3 x r from the light source, except that the small square would only be 1 19th as large as the original square and thus the light would seem 9 times dim mer. At a distance of 4 x r, the small square would be only 1 1 1 6 as large as the original square and the light would seem 1 6 times dimmer. At 5 x r, the small square would be I /25th as !arge as the original square and the light 25 times dimmer. And so forth. Asrute readers will notice that the light grows dimmer as the square of the distance increases. This is known as the inverse square law and represents the way that the intensity of all forms of radiation falls off with distance, not just light. As a general rule, we can say that if a light has an apparent brighrness of B at a distance of D, it will have an apparent brighrness of 1 / ()C) at a distance ofX x D. There's a catch to this rule, however. lt only applies to a light source that has no size - that is, it assumes that the light is emanating from a single point in space. In the real world, light sources tend to be !arger than this, which causes the light to fall off more slowly with distance. So we can't use the inverse square law as a realistic model for the way light intensity changes. What will we do?
T h e Lig htso u rci n g For m u l a My own solution was to pull a copy of Computer Graphics by Donald Hearn and M. Pauline Baker (Prentice Hall, 1 986) off my bookshelf and look to see if it contained any formulas relating to light intensity. What I found was this:
where
I is the intensity of the light on the surface, kd is the reflectivity of the surface, I P is the intensity of the light source, d is the distance from the light source to the surface, d0 is a fudge factor that avoids divide-by-0 errors when the light source and the surface are very close, N is a vector representing the surface normal of the surface, and L is a vector representing the direction of the ray of light striking the surface. Got that? Good. Now forget it, because we're not going to be using that formula. We don't need the business with the surface normal and the light ray vector because the light source is always originating at the viewer (who will be carrying the torch) and the light always reflects straight back from the surface to
G A R D E N S OF IMAGI NATION
the viewer. Thus the two factors cancel each other out. We don't need the fudge factor because we already have a fudge factor preventing the distance from getting too small . And we don't need the reflectivity factor because we're going to assume that all surfaces in the maze are equally reflective. I n practice, I find that this simpler formula produces perfectly reasonable results: i n t e n s i t y_r a t i o = i n t e n s i t y_o f_sou r c e I d i s t a n c e_f rom_so u r c e * MULT I P L I E R
where intensity_ratio i s a floating point number i n the range 0 t o 1 that represents how bright the surface appears relative to how it would appear in full light (with 1 representing full intensity and 0 representing black) , and MULTIPLIER is a constant that p revents the l ight i n tensity from falling off too rapidly with distance, so that even nearby walls would be invisible. I recommend a value of 3 for this constant, but feel free to try other values. There's nothing sacred about this formula. I use it because it works. Bur if you find one that works better, you should use it in preference to this one (and I wouldn't mind if you Iet me use it too). There's no point in spending too much time worrying about whether our l ightsourcing equations produce accurate results, though. What matters is that those results Iook realistic to the viewer, who probably won't be examining the video display with a light meter. Thus we can get away with a Iot of simplification. Before we can use this formula, we need to know what numbers to assign to the variables. Obviously, the distance_ from_source factor in this formula will be the distance of the light source in pixel units from the coordinates xview,yview at which the viewer is standing, since our engine already calculates that distance and makes it readily available in the variable distance. Bur how are we going to measure the intensity of the light source? Surprisingly, we can measure it using any method we want to, as long as we use that method consistently throughout the program. The method that we'll be using in this chapter will represent the intensity of l ight sources with a number in the range 0 to 32, with 32 representing the maximum possible intensity and 0 representing complete darkness. We'll measure the brightness of surfaces the same way, with a brightness of 32 indicating that the pixels appear exactly as they would if no lightsourcing were applied (that is, as they've appeared in the last two chapters) and 0 indicating that all pixels have the same color: black. Values between 0 and 32 will indicate degrees of brightness. For instance, an intensity value of 1 6 will indicate that the pixels on the surface appear half as bright as they do at full intensity. To perform lightsourcing on a pixel, we must calculate the distance to the p ixel , apply the lightsourcing formula to get the value of intensity_ratio, and multiply intensity_ ratio by the brightness of the pixel to determine the actual color that we are to place on the screen.
CHAPTE:R ElEVEN
Lig htsou rcing
Bur how do we know what the brighmess of the pixel is? When we copy a pixel from the texture-map buffer to the screen, all we know is the palette number of the pixel. We don't even know what color it is (which can vary from palette to palette) much less how bright it is. You'll recall that the VGA color registers contain three numbers in the range 0 to 63 for every color in the palette. These numbers represent the red, green, and blue intensi ties of the color. For i nstance, a medium gray shade might be represented by the numbers 32,32,32, meaning that red, green, and blue are all at about half their maximum intensities. A bright red color might be represented by the numbers 63,0,0, indicating that red is at full intensity while green and blue are at minimum intensity. To change the brighmess of the red, green, or blue component of a color, we must increase or decrease the number representing that component. If we increase the number representing the green component of a color, for instance, we make that component brighter. If we decrease the number, we make that component darker. However, if we raise and lower these numbers indiscriminately, we can actually change the color. lncreasing the green component of a color may make the green component brighter, bur it also makes the color itself seem more green. To change the brighmess of a color withour changing its hue, we must be careful to increase or decrease all three color components together. Furthermore, we must take care to preserve the relationship between these components, because it is that relationship that establishes the hue. For instance, all colors in which the three color components are equal are shades of gray. We can increase or decrease the intensity of the color by increasing or decreasing the color components. Bur if we want the color to stay gray, we must be sure the three components are always equal. This isn't hard to do. We j ust have to multiply all three components by the same lightsourcing factor. The factor that we'll be using is the intensity_ratio component from the formula above. You'll recall that this number is a fraction in the range 0 to 1. To find the lightsourced RGB components of a pixel, we need merely multiply this fraction by rhe non-lightsourced RGB values for the pixel. Suppose, for instance, that we wish to place a pixel on the display that has non lightsourced RGB components of 48,32, 1 6. And suppose that, after solving the lightsourcing equation for the distance between the viewer and the pixel, we determine rhat intensity_ratio has a value of 0.5 (meaning that the pixel should appear half as bright as in full light). To determine the lightsourced value of the pixel, we simply m ultiply each of the RGB components by 0 . 5 , producing lightsourced RGB values of 24, 1 6,8. What could be easier? A Iot of rhings, acrually. While all of this sounds fairly simple in theory, the way that colors are handled by the VGA adapter makes lightsourcing a great deal more difficult than it should be. For one thing, it's rare that multiplying RG B
GARDENS OF I MAGI NATI ON
values by intensity_ratio will produce integral values. More likely, it will produce three fractional values, such as 1 7.9,50.23 1 ,39.8. This is a minor problem, since i t's fairly easy ro round such numbers to the nearest integer, and the difference will generally be unnoticeable to the viewer. Far more troublesome is the fact that, despite being able to produce 64 x 64 x 64 (or 262, 1 44) different colors, the VGA adapter in mode 1 3h only allows us to place 256 of them on the screen at one time. So j ust knowing what color needs to be placed on the display to represent the lightsourced value of a pixel isn't enough. We must then find out if that color is present in the 2 56-color screen palette. If it isn't (and it usually won't be) , we need to find the closest approximation of that color and use it instead. This can be quite a time-consuming process. Performing these operations while casting rays through the maze could slow our ray-casting engine to a crawl. Fortunately, there's no need to perform these calculations on the fly. We can perform them in advance, save the results in a couple of arrays, and pass those arrays to the ray-casting engine, which can simply look up relevant values as it requires them. The first of these arrays will be a two-dimensional array - that is, an array of arrays - in which there are 32 entries for every color in the 2 56-color palette. This may sound like rather a large array, but it will only take up 8 kilobytes of our precious computer memory. The advantages we gain from this array will be well worth the sacrifice. The 32 entries for each palette color will contain the palette numbers of other palette colors that come closest to approximating the way that that color would look at each of the 32 light levels. (Actually, there are 33 light levels, counting 0, so this array will really contain 33 entries for each of the 2 5 6 palette colors.) This is a potentially confusing concept, so I'll pause here for a moment to explain it in more detail. Suppose that palette color 1 0 is a shade of gray with RGB values of 32,32,32. Since we're using an intensity scale that runs from 0 to 32, it's not difficult ro see what the ideal list of lightsourced colors for this color would look like. At an intensity level of 0, it would have RGB values of 0,0,0. (Actually, every color has RGB values of 0,0,0 - pure black at intensity 0,0,0.) At intensity 1 , it would have RGB values of 1 , 1 , 1 . At intensity 2, it would have RGB values of 2,2,2. And so on, all the way up to intensity 32, where it would have RG B values of 32,32,32. Entry 1 0 in the lightsourcing array would consist of the numbers of the 32 colors that represent lightsourced versions of color 10. Ideally, the first entry in the list (corresponding to a light intensity of 0) would be the number of the color with RGB values of 0,0,0. Since that color is pure black, it almost certainly will be in the list, since almost every palette contains black. If black is color 0 in the palette, the first entry in the array for color 1 0 will be 0. If the color with RGB
C HAPTER ELEVEN
Lig htsourcing
values of 1 , 1 , 1 is also in the palette, its number will be entry 2 in the !ist. If it isn't, tht;n entry 2 will contain the n u mber of the color that most closely approximates this color. For instance, if the color with RGB values 2, 1 , 1 is in the palette, its number may appear i n entry 2 instead. It's not quite the right color, but it's close enough. Enrry 3 will contain the color that comes closest to havi ng RG B values of 2 , 2 , 2 , entry 4 will contain the color that com es closest to having RGB values of 3,3,3, and so forth. The last entry will represent the palette color that best represents the color at intensity 32. Since this is full brightness, the last entry will always point to the color itself. For color 1 0, the last entry will be 1 0 . Once we've created these tables, finding the lightsourced color o f a pixel will be a simple matter of calculating an intensity number in the range 0 to 32 that represents the intensity of the light falling on that surface and looking up the equivalent entry in the table of light source values for that color. We can calculate the i ntensity n umber by solving the l ightsourcing formula for the value of intemity_ratio, then multiplying intemity_ratio by 32. All clear? For example, if the lightsourcing tables are contained in the two-dimensional array lightsource[}[}, where the number in the first pair of brackets represents the palette number of the color and the n umber in the second pair of brackets represents the light intensity, we can find the lightsourced value of a pixel like this: f l oat i n tens i ty_ra t i o = i n t e n s i ty_of_so u r c e I d i s t a n c e * MULTI P L I E R ; i f C i n t e n s i ty_ra t i o ) > 1 . 0 ) i n t e n s i ty_ra t i o 1 . 0; i n t i n t e n s i t y = i n t e n s i t y_r a t i o*32 ; i n t L i g h t s our ced_co l o r = L i g h t s ou r c e [ c o l o r J [ i n t e n s i t y_rat i o *32 J ;
where color i s the palette color o f the non-lightsourced pixel, distance i s the distance to the pixel, and lightsourced_color is the palette color that will actually be placed on the video display. You'll notice that we truncate the value of intensity_ratio, so that it can never become ! arger than 1 . 0 and increase the maximum l igh t i n tensity val u e . Surprisingly, intemity_ratio n o t only can go over 1 . 0 , b u t it frequendy will, depending on the values given to light_intensity and MULTIPLIER. Setting MULTIPLIER to a value greater than 1 (or light_intensity to a value greater than 32) has the effect of producing a circle of ful l brightness araund the player character, which not only looks realistic but will save the player considerable eyestrain. Before we can create a lightsourced version of the draw_mazeO function, we'll need to create a lightsourcing table and save it in a two-dimensional array. This is a task that can take up a lot of CPU time, since it involves searching ehe enrire palette 32 times for each of the 256 palette colors to find lightsourced equivalents
GARDE:NS OF I MAG INATION
for rhose colors. So we'll write a special uriliry program that will create rhe table and save it to a disk file. Our ray-casting program can then read the data during its initialization. We'll call rhis uriliry program MAKELITE.CPP. Ir will use the loadPCXQ function to read a PCX file from the disk and will create lightsourcing tables for that palette. Thus, if we are going to use the tiles and palette i n rhe file IMAG E S . PCX i n our ray-casting program , and rherefore want to base rhe lightsourcing tables on rhat file, we can invoke MAKELITE by typing: MAKE L I T E I MAG E S . P C X
at rhe DOS prompt. This means that we'll need to include code at the beginning of rhe mainO ftmction of the program to make sure that there's a file name from the command line: i f < a r g c=3 ) r e d t a r g e t = a t o f ( a r g v [ 2 J ) ; i f ( a r g c>=4 ) b l u e t a r g e t = a t o f ( a r g v [ 3 J ) ; i f ( a r g c>=5 ) g r e en t a r g e t = a t o f ( a r g v [ 4 J ) ;
The process that follows will be fairly time-consuming, especially on slower machines. (Ir takes abour 1 5 seconds on my 40-mHz 486.) So that the user won't think rhe computer has locked up, we'll print periodic messages on rhe display
C HAPTER E lEVEN
L i g htso u rc i ng
explaining what the program is doing. Since the first thing we'll do is load the PCX file and extract its palette, that's what we'll indicate in the first message: p r i nt f < " Loa d i n g pa l e t t e . \ n " ) ;
We'll link our well-worn PCX routines to the program, so all we need to do to Ioad the file is to call the loadPCXQ ftmction using the command line parameter argv[l} as the file name: i f ( l oadP C X ( a r gv [ 1 J , & l i t epa l ) ) e x i t ( 1 ) ;
This Ioads the file into litepal, which will need to be declared earlier i n the program as a PCX_struct. (You'll recall that the PCX_struct type is defined in the file PCX.H, which we'll need to include.) As usual, we check for a valid load, exiting the program if loadPCX tells us that an error has occurred. For purposes of clarity, we'll copy the palette from the palette field of the litepal structure into three arrays of integer variables, which we'll call redlj, greenlj, and blue[}. Each of these arrays will have 256 entries, one for each palette color. The individual entries will contain the red, green, and blue values for the equivalent palette color. Strictly speaking, this step is unnecessary, since we already have access to these values in the litepal structure. But the use of the three arrays will make the code that follows clearer. And this is hardly a time-critical program, which will suffer from the extra CPU cycles. We'll do the copying with a forO loop: for ( i n t c o l o r =O; c o l or< PALETT E S I Z E ; c o l o r ++) { re d [ co lo r J = l i t e pa l . p a l e t t e [ c o l o r* 3 J ; g r e e n [ co l o r J = l i tepa l . p a l e t t e [ c o l o r *3+1 J ; b l u e [ c o l o r J = l i t e pa l . p a l e t t e [ c o l o r *3+2 J ; }
The constant PALETTESIZE will be set equal to 256, representing the number of colors in the mode 1 3h palette. This is also clone for purposes of clarity, since it's unlikely that this program will ever be rewritten to handle palettes of a different size. Now it's time to calculate the palette tables. We'll print a message to the user so that they'll know what the program is up to: p r i n t f < " C a l c u l a t i ng pa l e t t e t a b l e s " ) ;
We'll perform the palette calculations within a set of three nested forO loops. The outer loop will iterate through all 256 colors in the palette. The middle loop will irerate through all 33 intensity Ievels of that color. And the innermost loop will once again irerate through all 256 palette colors, searching for the closest match for the current color at the current light Ievel. Here's the beginning of the outer loop: for ( c o l o r=O; c o l o r ,.pcx . h "
const MAX L I GHT=3 2 ; c o n s t PALET TES I Z E =256; pcx_s t r u c t L i t e pa l ; i n t red [ PALETT E S I Z E J , g r e e n [ PALET T E S I Z E J , b l u e [ PALET T E S I Z E J ; u n s i gned c h a r l i teso u r c e [ MAXLI GHT+1 J [ PALETTES I Z E J ; f l oa t red s c o r e , g reen s c o r e , b l u e s c o r e ; F I L E * h a nd l e ; vo i d ma i n ( i n t a r g c , c h a r * a r gv [ J ) { I I Set de f a u l t ta r g e t to b l a c k : i n t re d t a rget=O ; i nt greenta rget=O; i n t b l u eta rget=O; II C h e c k for f i l e name on the command L i n e : i f ( a r gc< 2 ) { p r i n t f < " You m u s t t ype a f i l e name . \ n " ) ; exi t ( 1 > ; } II if if if
C h e c k for < a rgc >=3 ) ( a rg c>=4 ) ( a rg c>=5 )
t a r g e t va l u e s on command L i n e : redta rget=atof ( a rgv[2 J ) ; b l ueta rget=atof ( a rgv[3J ) ; g r een t a r g e t = a t o f ( a rg v [ 4 J ) ;
continued on next page
GARDENS OF I MAGI NATION continutdfrom prroious pagt
I I L e t u s e r know w h a t ' s g o i n g on : p r i n t f < " Loa d i ng pa l e t t e . \ n " ) ; I I Load t h e P C X f i l e : i f ( L oadPCX ( a rg v [ 1 J , & L i t e pa l ) ) e x i t ( 1 ) ; I I Copy pa l e t t e t o RGB a r ra y s : f o r ( i n t c o l o r = O ; c o l o r ; f l oa t s c o r e= r e ds c o r e+g r e e n s cor e+b l u e s c o r e ; I I I s t h i s be t t e r ( i . e . i f ( s c o r e=2 )
if
( a rgc >=3 )
v i e w i ng_a n g l e= a t o f ( a r gv [ 1 J ) ; i n t en s i t y= a t o f ( a r g v [ 2 J ) ;
if
( a rg c>=4 )
a m b i e n t_L eve l =a t o f < a r g v [ 3 J ) ;
The first line checks for the viewing angle parameter and reads i t off the command line if it's found. The second line loads a value for the intensity of the light source (which we'll set to 32 - full brighrness - by default) . The third line reads the ambient lighting level . We mentioned ambient lighting in the chapter on ray tracing. Ambient light is light that appears to come from all directions at once, filling shadows with a certain minimum light level. We'll add the ambient light level to all pixels on top of the light received from the player's "torch," but will not let the overall light level exceed 32. An ambient level of 0 will make the room appear to be lit only by the torch. A level of 32 will make all surfaces appear the way they would in full light (that is, the way they did in the ray-casting chapter) . You can tweak the ambient value to Ievels in between these two to find the level you like best. Even in the darkest dungeon a small ambient Ievel is desirable; otherwise the screen will turn pitch black if the player's torch should go out. Of course, that may be the effect that you're looking for. Finally, the call to our lightsourced version of the draw_maze() function will look like this: d r a w_ma ze ( wa l l s , f l o r , c e i l i ng , s c r e e n , xv i e w , yv i ew , v i ew i ng_a n g l e , vi e w e r_h e i g h t , a m b i ent_L eve l , t ex t m a p s . i ma g e , L i t e t a b l e , L i t e l eve l ) ;
This is similar to the way in which the ray-cast version in chapter 9 was called, with the familiar pointers to the walls[}, fior[}, and ceiling[} arrays, but we've added pointers to the litetable and litelevel arrays .
The LITE D E M O . C P P Pro g ra m The complete text of the LITEDEMO.CPP program appears in Listing 1 1 -2.
Listing 1 1 -2 The LITE D E M O . C P P prog ra m II II
L I TE D E MO . C P P continu�d on next page
GARDE:NS O F I MAG I N AT I O N continutdfrom previous page
r a y-c a s t i ng f u n c t i on
II
Ca L L s
II
v i ew o f ma z e .
to d ra w
L i g h t so u r c e d
II I I W r i t t e n by C h r i s t o p h e r L a m p t o n f o r II
Ga rdens of
I ma g i na t i on
# i n c l ud e
# i n c l ud e
# i n c l ud e
# i n c l ud e
< s t d l i b . h>
# i n c l ud e
# i n c l ud e
" sc reen . h "
# i n c l ud e
" pc x . h "
# i n c l ud e
" L i t esorc . h "
( Wa i t e G roup P r e s s )
c on s t G R I D S I Z E = 1 6 ; c on s t MAX D I STAN C E=64*G R I D S I Z E ; c on s t N U M_I MAG E S = 1 5 ; c on s t
f l o a t MULT I P L I E R= 3 ;
p c x_s t r u c t
textmaps;
F I LE *hand l e; unsi gned char
l i t e t a b l e [ MA X L I G H T + 1 J [ PA LE T T E S I Z E J ;
un s i g n e d c h a r * L i t e l e ve l ; ma p_t ype w a L l s= { {
5,
{ 7,
5,
5, 5, 5,
5 , 0,
5, 0,
5,
5,
5,
5,
5,
5, 5,
5,
{ 7, 0, 0 , 0, 0 , 0 , 0 , 0 , 3 , 0 , 0 , 0, 0, { {
5, 5, 5},
0, 0 , 2 , 0 , 0 , 0, 0, 0 , 0, 0 , 5 } , 0, 0,
7 , 0 , o , 0, 0 , 0 , 0 , 0 , 0 , 6 , 0 , 0, 0 , 0, 0 , 7, 0, 0, 0, 0 , 0 , 0 , 0 , 0 , 0 , 5 , 4, 0 , 0, 0,
5}, 5}, 5},
0 , 0, 0 , 0 , 0 , 0 , 0 , 0 , 1 1 , 1 1 , 0 , 0 , 0,
5},
0 , 1 1 , 1 0 , 0 , 0,
5},
0, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, 1 0, 0, 0,
5},
{ 1 1 , 7 , 7 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 1 1 , 1 0, 0, 0,
5},
{ 1 2,
0,
{ 1 1 , 0 , 0 , 0 , 0, { 1 1 , 0, 0, { 1 2 , 0,
0 , 0 , 0,
0, 0 , 0,
0,
0,
0, 0 , 0 , 0 , 0 , 1 1 , 1 1 , 0 , 0 , 0 ,
5},
1 , 0 , 0, 0 , 0, 0,
5},
5,
1 , 0,
{ 7 , 0 , 0 , 0, 0 , 0 , 0 ,
1 , 0,
1 , 0 , 0, o, 0, 0,
{ 1 2 , 7 , 7 , 0, 0,
0,
{ 7 , 0 , 0 , 0, o, 0 , 0, { 7,
0,
{ 7, 5, } .
0 , 0,
1 , 0,
1 , 0 , 0, 0 , 0 , 0, 1 1 } ,
6,
0,
0,
1 , 0,
1 , 0 , 0 , 0 , 0 , 0 , 1 0} ,
5,
5,
5 , 1 5 , 1 5, 1 5,
0 , o , 0,
{ 7 , 6 , 6 , 6, 5,
5,
5},
1 , o, 0 , o, o , 0, o, 0, 1 2} ,
5, 5,
5,
5,
5,
5}
,
m a p_type f l or = { {
5,
5,
5, 5, 5, 5, 5,
5,
5,
5,
5, 5,
5,
5, 5,
5},
{
5,
5,
5,
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5, 5,
5},
{
5,
5,
5,
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5, 5,
5},
{
5,
5,
5,
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{
5,
5,
5 , 5 , 8,
5,
5,
5,
5,
5,
5,
5,
5,
5, 5,
5},
{
5,
5,
5 , 8,
8,
5,
5,
5,
5,
5,
5,
5,
5, 5,
5},
{
5,
5, 8, 8, 8, 8, 8,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{
5,
5,
57
5,
5,
5,
5 , 8,
5,
5, 5,
5},
8,
5 , 8 , 8,
8,
C HAPTER ElEVEN { 5,
5,
5,
5, 8,
5,
5,
5,
5,
5,
5,
5,
5,
5, 5,
5},
{ 5,
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{ 5,
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{ 5, 5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
5},
{ 5,
5,
5, 5, 5, 5, 5, 5 , 5 , 5, 5, 5, 5, 5, 5, 5},
{ 5,
5,
5,
5, 5,
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
{ 5,
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5, 5},
{ 5, } ,.
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
Lig hrso u rc i ng
5}
map_type c e i l i ng={ { 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3, 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3} , { 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3, 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3} , { 1 3, 1 3, 1 3 , 1 3, 1 3 , 1 3, 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3} , { 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3, 1 3, 1 3, 1 3} , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3, 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 2 , 1 3 , 1 3, 1 3, 1 3, 1 3} , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 } }; f l oa t v i e w i ng_a n g l e=3 . 1 4 ; f l oa t
i n t e n s i ty=MAX L I G H T ;
i n t v i ewe r_h e i g h t =32 ; i n t ambi e n t_L eve l =O ; i n t xv i e w=8*64+3 2 ; i n t yv i ew=8*64+32; voi d ma i n ( i n t a r g c , c h a r * a r g v [ J ) { II
R e a d a r gume n t s f rom command
if
( a r g c>=2 )
L i ne i f p r e s e n t :
if
( a rg c>=3 )
i n t e n s i t y=a t o f ( a r g v [ 2 J ) ;
if
( a rg c>=4 )
amb i e n t_ L eve l = a t o f ( a r g v [ 3 J ) ;
v i e w i ng_a n g l e= a t o f ( a r g v [ 1 J ) ;
I I Loa d t e x t u r e ma p i ma g e s : if
< L o a d P C X ( " i ma g e s . p c x " , & t ex t m a p s ) )
I I Load if
ex i t ( 1 ) ;
L i g h t sou r c i ng t a b l e s :
( ( ha n d l e=f open ( " L i t e so r c . da t " , " r b " ) ) ==NU L L )
{
p e r r o r ( " E r ro r " ) ; ex i t ; } f r ea d ( l i t e t a b l e , MA X L I G H T+1 , PA L E TT E S I Z E , h a nd l e ) ; f c l o s e ( ha nd l e ) ;
continued on next page
GAR DE:NS O F IMAG I NATIO N continued.from previous page
II
I n i t i a l i z e a r r a y of
l i gh t
L e ve l s :
l i t e l e ve l =new u n s i gned c h a r [ MA X D I STA N C E J ; II
Ca l cu l a t e
II
d i s t a n ce s :
for
( i nt
f l oa t if
l i ght
i nt en s i t i e s f o r a l l pos s i b l e
di s t a n c e =1 ; d i s t a n c e 1 . 0 )
r a t i o= 1 . 0 ;
l i t e l eve l [ d i s t a n c e J = ra t i o*MA X L I GH T ; i n t dummy= O ; } II
Po i n t
char II
va r i a b l e a t
v i deo memo r y :
f a r * s c r e e n= < c h a r
f a r * ) MK_F P ( O x a 00 0 , 0 ) ;
S a v e pre v i o u s v i deo mod e :
i n t o l dmode=* ( i n t * ) MK_F P ( O x 4 0 , 0 x 49 ) ; II
S e t mode
13h:
s e t mode ( Ox 1 3 ) ; II
Set
t h e pa l e t t e :
s e t p a l e t t e ( t e x t m a p s . pa l e t t e ) ; II
C l e a r t h e s c reen :
c l s ( s c r e en ) ; II
D raw a
r a y- c a s t
v i ew of
t he maze
d r a w_m a z e ( w a l l s , f l o r , c e i l i n g , s c r e e n , x v i e w , yv i ew , v i e w i ng_a n g l e , v i e w e r_he i g h t , a m b i e n t_l e v e l , t e x t m a p s . i m a g e , l i t e t a b l e , l i t e l eve l ) ; I I Wa i t whi le
for user t o h i t a
key :
( ! kbh i t ( ) ) ;
I I R e l ea s e m e m o r y d e l e t e t e xt m a p s . i ma g e ; de l e t e II
l i t e l eve l ;
R e s e t vi deo mode a n d e x i t :
s e t mo d e ( o l dmode ) ; }
Lig htso u rc i n g i n Th ree L i n e s o r Less The prototype for the function, which will go in the file LITESORC.H, Iooks like this: vo i d d r a w_ma z e ( ma p_t y p e m a p , m a p_t ype f l o o r , ma p_type c e i l i n g, char
far
* s c r e e n , i n t xv i ew , i nt yv i e w,
f l o a t v i ew i ng_a n g l e , i n t v i ewer_h e i g h t , i nt amb i ent_le v e l , u n s i gned c h a r f a r * t e x t m a p s ,
CHAPTER ELEVEN u n s i gned
Lig htsourc i ng
c h a r [MAXLI GHT+1 J [ PA L E T T E S I Z E J ,
u n s i gned c h a r * L i t e l eve l ) ;
The function itself is in the file LITESORC.CPP. It is nearly identical to the ray-cast version of the draw_mazeO function in chapter 9. All that has changed, other than the prototypes, are the lines that actually place dara on the display. lnstead of simply copying the pixel dara from the texture-map buffer ro video RAM , we'll do somerhing s lighdy more complicared. We'll use the distance variable (which contains rhe distance, in floor pixel units, from the viewer to the pixel being drawn) as an index inro the litelevel array to find the light level for thar disrance, then add the arnbient term ro rhar value: i nt
L e ve l = L i t e l eve l [ d i s t a n c e ]+ambi e n t_Leve l ;
Since the arnbient term may rake this value over 32, we'll chop ir off in thar Ievel: if
( L eve l>MA XL I G H T )
l eve l =MAX L I G HT;
The consrant MAXLIGHT is assigned a value of 32 in the LITESORC.H file, should we be overcome wirh an urge ro change the number oflighr levels. Now rhar we have rhe l ight leve l , we can use ir as an i n dex in to rhe litesource[}[} array ro find rhe lightsourced color for the current pixel. We'll use rhe non-lighrsourced color fo r rhe pixel, which is at offset tileptr in the textmapsD array as rhe second index for rhis rwo-dimensional array. Then we'll copy the resulring value into video RAM: s c ree n [ o f f s e t J = L i t e s ou r c e [ L e ve L J [ t e x tmap s [ t i l ep t r J J ;
The draw_mazeO funcrion writes pixels to the display i n three different places, once when drawing the walls, once when drawing rhe floors, and once when drawing the ceiling. We'll use these sarne rhree lines of code in all rhree places. And rhose, believe it or not, are all the changes that are necessary to add lighrsourcing to the core of our ray-casring engine. By placing all of the tough work in the calling module and in the MAKELITE program, we've actually left very little extra work for draw_mazeO to do.
The Lig hts o u rced d raw_m a z e o F u n ction The lightsourced version o f rhe draw_mazeO function appears i n Listing 1 1 -3.
I I L I T E S O R C . CPP II
contimud on next page
GARDENS O F I MAG I NATION continuedfrom previous page
II
F un c t i on
II
c e i l i ng s us i n g r a y c a s t i ng .
to d r a w
l i g h t s o u r c ed wa l l s ,
f l o o r s a nd
I I W r i t t e n by C h r i s t o p h e r Lampton f o r II
G A R D E N S O F I MAG I NA T I O N
( Wa i t e G r oup P r e s s ) .
II # i n c l ud e < s t d i o . h> # i n c l ud e # i n c l ud e
" l i tesorc . h"
# i n c l ud e " p c x . h " II
C on s t a n t de f i n i t i on s :
c o n s t W A L L_H E I G H T = 6 4 ;
I I H e i g h t o f wa l l
const
I I V i ew e r d i s t a n c e f rom s c r e e n
V I E W E R_D I S TAN C E = 1 9 2 ;
II
c o n s t V I E W P O R T_LE FT=O ;
in pi xe l s
D i me n s i o n s of v i ewpo r t
c o n s t V I E W P O R T_R I G H T =3 1 9 ; c o n s t V I EWPORT_TO P=O; c o n s t V I E W PORT_BOT= 1 99 ; c o n s t V I E W PORT_H E I G HT=V I EWPO R T_BOT-V I EWPORT_TO P ; c o n s t V I E W PORT_C EN T E R= V I E W P O R T_TOP+V I EWPORT_H E I G H T I 2 ; c o n s t G R I D S I Z E= 1 6 ; c o n s t MAX D I STANC E=64* G R I D S I Z E ; v o i d d r a w_ma z e ( ma p_t ype m a p , m a p_t ype f l oo r , ma p_type c e i l i n g , char
f a r * s c r e en , i n t x v i ew, i n t yv i ew ,
f l o a t v i ew i n g_a n g l e , i n t v i ewe r_h e i g h t , i n t a m b i e n t_ l ev e l , un s i gned c h a r f a r * t e x t m a p s , uns i gned c h a r
l i t e sour c e [ MA X L I GHT+ 1 J [ PA LE T T E S I Z E J ,
uns i gned c h a r * l i t e leve l ) II
D ra w s a
II
i n a r ra y M A P [ J ,
r a y- c a s t
i ma g e
i n t h e v i ewpo r t of t h e m a z e
I I v i ewe r
l oo k i ng a t a n g l e V I EW I NG_ANG L E w h e r e a n g l e 0
II
( Ang l e s a r e mea su red
nort h .
r e p r e s e n t ed
a s s e e n f rom po s i t i on XVI EW, Y V I EW by a in
i s due
radi a ns . )
{ I I Va r i a b l e d e c l a r a t i on s : II
P i xe l
x d , yd ;
II
D i s t a n c e to n e x t w a l l
i n t g r i d_x , g r i d_ y ;
II
Coord i na t e s o f
f l o a t x c r o s s_x , x c r o s s_ y ;
II
R a y i n te r s e c t i on coo rd i n a t e s
u n s i g n e d i n t x d i s t , yd i s t ;
II
D i s t a n c e to x a n d y g r i d
i n t x ma z e , yma z e ;
I I M a p l o ca t i o n of
i n t d i s t a n c e;
II
D i s t a n c e to wa l l a l ong
i nt
II
C o l umn i n t e x t u r e map
int
sy,of fset;
f l oa t
y po s i t i on a nd o f f s e t X
i n x and y
a nd y g r i d
l i nes
f l oa t y c r o s s_x , y c r o s s_ y ;
t m c o l um n ;
l i ne s
ray co l l i s i on ray
f l oa t y ra t i o ; II for
Loop t h r ough a l l ( i nt II
c o l um n s of p i x e l s i n v i ewpo r t :
c o l umn=VI E W P O R T_L E F T ;
c o l umn O )
ray d i re c t i o n po s i t i v e i n y,
II
If
g r i d_y= ( ( i n t ) y & O x f f c O ) +64 ;
r a y d i r e c t i on nega t i ve i n y,
e l s e g r i d_y= ( ( i n t ) y & O x f f c O )
get
Last y grid
L i ne :
- 1;
I I Get x , y c o o r d i n a t e s w h e r e r a y c ro s s e s x g r i d L i n e : x c r o s s_x=g r i d_x ; x c r o s s_y=y+ s l o pe * ( g r i d_x-x ) ; I I Get x , y coo r d i n a t e s w h e r e r a y c ro s s e s y g r i d
L i ne : continutd on nexr pagt
GARDENS OF I MAG I NATION continuedfrom previous page
y c r o s s_x=x+ ( g r i d_y-y ) l s l o p e ; y c r o s s_y=g r i d_y; I I Get di stance
to x g r i d
L i ne :
xd=x c ro s s_x- x ; yd=x c ross_y-y; x d i s t =s q r t ( xd* xd+yd*yd ) ; I I Get d i s t a n c e t o y g r i d
L i ne :
xd=y c ro s s_x- x ; yd=y c ro s s_ y-y; yd i s t =sq r t ( xd*xd+yd*yd ) ; I I I f x gri d i f
L i ne
( xdi s t V I EWPO R T_BOT )
{
d h e i g h t -=
( bo t - V I EWPO RT_BOT ) ;
i h e i g h t -=
( bo t - V I EWPORT_BOT ) *y ra t i o ;
bot=V I EWPO RT_BOT; } II P o i n t t o v i deo memo ry o f f s e t
f o r top o f
L i ne :
o f f s e t = top * 320 + c o l um n ; II
I n i t i a l i z e ver t i ca l
e r ro r t e rm f o r t e x t u r e map :
i n t t y e r r o r=64; II Wh i c h g ra ph i c s t i l e a r e w e u s i ng ? i n t t i l e =ma p [ xma z e J [yma z e J -1 ; II
F i nd o f f s e t of
t i l e a nd c o l umn i n b i t ma p :
u n s i gned i n t t i l e p t r = ( t i l e 1 5 ) *3 2 0 * I M A G E_H E I G H T+ ( t i l e % 5 ) * I MAGE_W I DT H + t ; I I Loop t h r o u g h II
t h e p i xe l s i n t h e c u r r e n t v e r t i c a l
L i ne , adva n c i ng O F F S E T t o t h e n e x t
I I a f t e r e a c h p i xe l for
( i n t h=O;
h = I M A G E_H E I G H T ) If
i nt if
so,
{
draw i t :
l ev e l = l i t e l ev e l [ d i s t a n c e ] + a m b i e n t_ l e v e l ; ( l e ve l >MAX L I G H T )
l ev e l =MAX L I GH T ;
s c r ee n [ o f f s e t J = l i t e sou r c e [ l eve l J [ t e x tma ps [ t i l ep t r J J ; II
Reset error term:
t y e r r o r- = I M A G E_H E I G H T ; I I A n d a d va n c e O F F S E T t o n e x t
s c reen
l i ne :
o f f s e t +=320; } II
I n c r emen t a l d i v i s i on :
t ye r ror+=he i g h t ; I I Adva n c e T I L E PT R t o n e x t
l i ne o f b i t ma p :
t i l e p t r+=320; } II
S t ep t h ro u g h f l oor p i x e l s :
for
( i nt
I I Get
row=bo t + 1 ; r a t i o of
rowMAX L I G H T ) l e ve l =MAX L I GH T ; s c r e e n [ o f f s e t J = l i t e sou r c e [ l e v e l J [ t e x tmaps [ t i l ep t r J J ; } } }
GARDENS OF IMAG I NATI ON
To run the program, go to the LIGHT directory and type LITEDEMO. You'll see the familiar view of the ray-cast maze from chapter 9, but this time much of it will be shrouded in gloomy shadows (see Figure 1 1 - 5 ) . Try fooling around with the intensity and am bient_ level parameters to see what e ffect they have. (Remember that the first parameter on the command line is the angle, followed by the intensity and ambient parameters, in that order.) Type LITEDEMO 0 0 to see the maze in no light at all. Whoops, the screen is completely b i ack! Not much reason to include a screen shot of that one. And LITEDEMO 0 32, where the ambient is turned up all the way, Iooks exactly like the screen shots at the end of chapter 9. But LITEDEMO 1 6 0 shows a spookier and darker dungeon (refer to Figure 1 1 -6) . And LITEDEMO 4 shows a dungeon made up of little besides shades of dark gray (Figure 1 1 -7) . A little bit of ambient light can go a long way. Try LITEDEMO 32 4 to see the maze with a bright torch and some dim ambient lighting. But LITEDEMO 32 1 6 , which raises the ambient Ievel halfway to full brightness, doesn't Iook much different from the maze in full light.
Alternate Li g hts o u rc i n g Effects You don't have to use lightsourcing in your programs. Many popular maze games, such as Wolfenstein 3D, use no lightsourcing effects at all and still manage to
Fig u re 1 1 - s A gloomy maze produced by LITED E M O . EXE
Figure 1 1 -7 Almost in total darkness
Figure 1 1 -6 Turn ing down the intensity
C HAPTE:R ElEVEN
L i g htsourcing
enthrall players. But it's a nice special effect to use when you want to add something a l i t d e extra to your c o d e . The best t h i n g about u s i n g t h e MAKELITE program t o generate lightsourcing tables i s that you can use it with almost any palette whatsoever, not j ust with special palettes designed to work with lightsourced graphics. Of course, some palettes work better with this lightsourcing method than others. Since all the palette colors converge on black and dark gray at low light levels, it helps if there are several dark gray and brown colors in the palette. Otherwise, severe banding can occur in very low light, all colors being converted to one or two very dark colors by the lightsourcing rounnes. Lightsourcing can be used with some special effects that you might not have thought had anything to do with light levels. The game Shadowcaster, from Origin Systems, uses lightsourcing to pull off some unexpected effects, such as scenes filled with fog (see Figure 1 1 -8) and and underwater scenes (Figure 1 1 -9). It's not hard to figure out that these must have been clone with lightsourcing tables similar to the ones we've learned to create in this chapter. However, where the lightsourcing tables that we created for our demo program are based on a target color of pure black (with RGB values of 0,0,0) , these fog and underwater effects must be based on l ightsourcing tables with target values of white (63,63,63) and blue (0,0,63) . You can easily create a fog table with the MAKELITE utility, by going to the LIGHT directory and typing: MAKE L I T E 63 63 63
Once the table is complete, run LITEDEMO again. This time the maze will be enshrouded with mist, as i n Figure 1 1 - 1 0 . To try and dupl icate t h e underwater effect, type: MAKE L I T E 0 0 63
Figure 1 1 -8 A scene from Origin's
Figure 1 1 -9 Another scene from
Shadowcaster. demonstrating how
Shadowcaster. with underwater lighting
lig htsourcing can create fog effects
effects
GARDENS OF IMAGI NATION
Fig u re 1 1 -1 0 A m ist-enshrouded maze created by using l ig htsource target values of 63,63 ,63
This effect is a bit less successful. Rather than appearing to be underwater, the maze appears to be enshrouded in blue fog this time. Typing LITEDEMO 8 0, which decreases the circle of light around the player, helps a bit, but too much detail is lost in the distance. To get a better underwater effect, you might take a tip from Shadowcaster and design a set of underwater texture-map tiles in which the color blue already predomi nates, with swirling lines that suggest li ght refracted through a wavy surface.
Lig htm a p p i n g In real life it's rare to find a room that is illuminated entirely by a light source at the viewer's position, but neither are all scenes lit entirely by ambient light. Just as often, lighting comes from multiple light sources scattered about in various positions, some of them bright and some of them not so bright, some of them stationary and some of them moving. Your living room, for instance, may be illuminated by two or more table lamps, and possibly by an overhead lamp as wel l . And even when these sources have been turned off, light may pour in from windows or nearby rooms. All of these sources of illumination may periodically change from dim to bright and back again and may even move around in the course of the day. (The light streaming through the window, for instance, may enter at one angle in the morning and another in the evening.) If our ray-casting engine is to accurately model the kind of illumination found in the real world, it will have to model the distribution of variable local light sources throughout the maze. And the best way I know to model this distribution is through the use of lightmaps. Just as a heightmap specifies the floor height at various points throughout the maze, a lightmap specifies lighting intensities. This can be clone for every pixel in the maze or it can be clone on the level of the maze grid. The first system, which
CHAPTE:R ElEVEN
Lig htsou rci ng
is the simpler of the two, could be called block-aligned lightmapping, by analogy to the block-aligned heightmaps we talked about in chapter 1 0. And the lightmaps themselves look a lot like heightmaps. Here are the lightmaps we'll use in our demo program : map_type f l o o r l i t e s ={ { 0 , 0 , 0 , 0, o,
0, 0, 0 , 0,
0 , 0,
{ 0, 0, 0, 0, 0, 0, 0, 0,
0, o,
0, 0 , 0 , 0 , 0},
{ 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
{ 3 2 , 0 , 0 , 0,
0, 0,
0,
0,
0,
{ 0, 0 , 0,
0,
0,
0,
0,
0, 0 , 0 } ,
0, 0 , 0 , 0 } ,
0,
0, 0,
0,
0, 0, 0, 0, 0},
0, 0,
0 , 0,
0,
0 , 0, 0,
0, 0 , 0,
{ 0 , 0 , 0,
0,
0, 0 } ,
0, 0, o, 0, 0, 0, 0, 0, 0, 0}, 0, 0 , 0,
{ 0, 0 , 0 , 1 6, 1 6 ,
0 , 0 , 0,
{ 0, 0 , 0 , 1 6, 1 6 ,
0, 0, 0, 0, 0, 0,
{ 0,32, 0, 0, 0,
0,
{32,32,32, 0, 0,
0 , 0 , 0,
0, 0,
0, o, 0, 0 , 0}, 0, 0 , 0 } ,
0,
0,
0,
0,
0, 0, 0, 0},
0, 0, 0,
0,
0,
0, 0,
0,
0,
0},
{ 0, 3 2 , 0, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , Q } , { 0, 0 , 0 , 0 , 0 , 0 , 0, 0, 0 , 0, 0 , 0 , 0, 0, 0 , 0 } , { 0, 0 , o , 0 , 0 ,
0 , 0 , 3 2 , 3 2 , 3 2 , 0 , 0 , 0, 0 , 0 , 0 } ,
{ 0, 0 , 0 , 0 , 0 ,
0, 0,32,32,32, 0, 0, 0, 0, 0, 0},
{ 0, 0 , 0 , 0 , 0 ,
0, 0,32,32,32, 0, 0, 0 , 0 , 0 , 0}, 0, 0 , 0, 0, 0 , 0}
{ 0, 0 , 0 , 0 , 0 , 0 , 0, 0 , 3 2 , 0, }; map_type c e i l i n g l i t e s ={
0, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 } ,
0 , 0,
0,
0,
{ 0, 0 , 0 , 0,
0,
0,
{ 0, 0 , 0 , 0,
0 , 0,
0, 0 , 0 , 0 , 0 , o,
{ 0 , 0 , 0 , 0,
0,
0, 0 , 0,
{ 0, 0 , 0 , 0,
0 , 0,
{ 0,
0,
{ 0, 0 , 0 , o,
0,
0, 0 , 0 , 0 , 0 , 0,
0 , 0 , 0,
{ 0, 0 , 0 , 3 2 , 3 2 , 0 , 0,
0, 0,
0, 0 , 0 , 0 } ,
0, 0, 0,
0, 0,
0},
0, 0, 0, 0, 0},
0, 0 , 0 , 0 , 0 , 0 , 0 , 0, 0,
0,
0, 0 , 0 , 0 , 0 , 0 , 0 } , 0, 0},
0, 0 , 0 , 0 , 0 , 0 , 0 } ,
{ 0, 0 , 0 , 3 2 , 3 2 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 } , { 0, 0, 0, 0, 0 , 0, 0, 0, 0 , 0 , 0 , 0 , 0 , 0, 0, 0 } , { 0 , 32 , 32 , 0,
0 , 0 , 0, 0 , 0 , 0 , 0 ,
{ 0,
0, 0, 0, 0, 0},
{ 0,
0,
0,
0,
0,
0,
{ 0,
0,
0,
0,
0,
0,
0 , 0 , 0 , 0, 0 , 0 , 0 , 0 , o, 0 } , 0 , 0, 0 , 0, o , 0 , 0 , 0 , 0 , 0 } , 0, 0 , 32 , 3 2 , 0 ,
0, 0, 0, 0, 0},
{ 0,
0,
0,
0,
0,
0,
0, 0,32, 0, 0,
0 , 0 , 0 , 0,
{ 0,
0,
0,
{ 0,
0,
0,
0,
0,
0,
0, 0 , 3 2 , 0 , 0 ,
0, 0, 0, 0, Q}
0,
0,
0,
0,
0,
0},
o, 0, 0, 0 , 0 , 3 2 , 0 , 0 , 0 , 0 , 0 , 0 , 0 } ,
};
As you can probably tell from the names floorlites and ceilinglites, the first of these is a map of light intensities for the floor and the second is a map of light imensities for the ceiling. The floorlites[} array will also contain light intensities for the walls, since we'll be using block-aligned walls that cannot coexist in the same maze squares with visible floor tiles, so by combining the wall and floor maps we'll avoid some potential redundancy. Pixels in wall, floor, and ceiling tiles
GARDE:NS OF IMAG I NATI ON
will have the jloorlites[} value for the current maze square added to the intensity value of all other light sources falling on them (with a top intensity value of 32, as before) . In my continuing attempt to set a new world record for parameter passing, the prototype for our lightmapped version of the draw_mazeO function looks like this: vo i d d r a w_ma z e ( ma p_type m a p , m a p_type f l o o r , ma p_t ype c e i l i ng , m a p_type f l o o r l i t e s , m a p_type c e i l i ng l i t e s , char
f a r * s c r e e n , i n t x v i ew, i n t yv i e w ,
f l o a t v i ew i ng_a ng l e , i n t v i ewer_h e i g h t , i n t a m b i e n t_L e v e l , un s i gned c h a r f a r * t e x t m a p s , u n s i g n e d c h a r [ M A X L I G H T + 1 ] [ PA L E T T E S I Z E ] , u n s i g n e d c h a r * L i t e l eve l ) ;
This prototype will be placed in the file LITEMAP. H . The function itself is in the file LITEMAP. CPP. This time, we'll use the lightsourced version of the draw_mazeO function that we developed a few pages ago as a template for the new function. However, only three lines in the entire function need to be changed, the three lines that calculate the light level before a lightsourced pixel is added to the wall, ßoor, and ceiling, respectively. In the earlier function, this line looked up a color value for the current distance in the litelevel[} array and added the ambient term to it to find the light level of the pixel. The only additional step we need to take is to add the jloorlites[} value for the maze square to the intensity for ßoor and wall pixels: int
L ev e l = L i t e l e ve l [ d i s t a n c e ]+ambi e n t_L eve l + f l o o r l i t e s [ xm a z e ] [ yma z e J ;
And we can add the ceilinglites[} value for the maze square to the intensity for ceiling pixels: i nt
L ev e l = L i t e l e ve l [ d i s t a n c e ] +ambi e n t_L eve l + c e i l i ng l i t e s [ xm a z e ] [yma z e J ;
if
( l e v e l >M AX L I G HT )
l eve l =MAX L I G HT ;
s c r e e n [ o f f s e t ] = L i t e so u r c e [ L e ve l ] [ t ex t m a ps [ t i l e pt r ] ] ;
Th e L i g htma p ped d raw_ma z eo F u n ction Although i t seems almost redundant t o include it, the text o f the lightmapped version of the draw_mazeO function appears in Listing 1 1 -4.
Listing 1 1 -4 The l i g htmapped d raw_maze f u n ct i o n II II
L I T E MA P . C P P
C HAPTE:R ELEVEN II II II II II
Lig htsou rc i ng
F un c t i on to d ra w t e x t u re-mapped wa l l s , f l oors a nd cei l i n g s u s i ng ray c a s t i n g . W r i t t en by C h r i s t opher Lampton for GARDENS OF IMAGINATION ( W a i t e G r oup P r e s s )
# i n c l u de # i n c l ude # i n c l u de # i n c l ude
" l i te map . h " " pc x . h "
I I Con s t a n t de f i n i t i o ns : const WALL_H E I GHT=64; I I H e i g h t of wa l l i n p i x e l s II V i ewe r d i s t a n c e f rom s c r e e n const V I E W E R_DI STA N C E = 1 92; I I D i m e n s i ons o f vi ewpo r t con s t V I EWPO RT_L E FT=O; const V I E WPORT_R I G HT= 3 1 9 ; const V I EWPORT_TOP=O; c o n s t V I E W PORT_BOT= 1 99 ; const VI EWPORT_H E I GHT= V I EWPO RT_BOT- V I E W PORT_T OP; const V I E WPORT_C E N T E R=V I EWPORT_TOP+V I EWPORT_H E I G H T I 2 ; cons t G R I D S I Z E = 1 6; const MAX DI STANC E=64 *G R I D S I Z E ; vo i d draw_ma z e (map_type map, ma p_t ype f l oo r , map_t ype c e i l i n g, map_type f l oor l i t e s , ma p_t ype c e i l i n g l i t e s , c h a r f a r * s c r een, i n t x v i ew, i n t yv i ew , f l oa t v i ewi n g_a ng l e , i n t v i ewer_he i g h t , i n t ambi ent_lev e l , u n s i gned c h a r f a r * t ex t m a p s , u n s i gned c h a r l i t e s o u r c e [MAXLIGHT+ 1 J [ PA L E TT E S I Z E J , uns i g ned c h a r * l i t e l eve l ) II II II II
Draws a r a y- c a s t i ma g e i n t h e v i ewpo r t of t h e ma ze r e p r e s e n t ed i n a r r a y MA P [ J , a s seen f rom po s i t i on XVI EW, Y V I E W by a v i ewer l oo k i n g a t a n g l e V I EW I N G_AN G L E w h e r e ang l e 0 i s due no r t h . ( A ng l e s a r e measu red i n rad i a ns . )
{ I I Va r i a b l e d e c l a r a t i on s : I I P i xe l y po s i t i o n an d off set i n t sy,offset; I I D i s t a n c e t o n e x t wa l l in x and y f l oat xd,yd; II Coord i na t e s of x and y g r i d l i ne s i n t g r i d_x , g r i d_y; f l oa t x c ross_x , x c ross_ y ; I I Ray i n t e r s e c t i o n coor d i na t e s f l oa t y c r oss_x , y c r o s s_ y ; u n s i gned i n t x d i s t , yd i s t ; I I D i s t a n c e t o x a n d y g r i d l i n es i n t xma z e , ym a z e ; II M a p l o ca t i on of r a y co l l i s i on i nt d i s t ance; I I D i s t a n c e to wa l l a l o ng ray i n t tmco l umn; I I C o l umn i n t e x t u r e map f l o a t yr a t i o ; I I Loop t h r ough a l l c o l umns of p i xe l s i n v i ewpo r t : f o r ( i n t c o l umn=V I E WPORT_L E F T; c o l umn O ) g r i d_x = ( ( i n t ) x & Ox f f c 0 ) +64; II I f r a y d i r e c t i on nega t i ve i n x, g e t L a s t x g r i d L i n e : e l s e g r i d_x= ( ( i n t ) x & O x f f c O ) - 1 ; I I I f r a y d i r e c t i on po s i t i ve i n y , g e t next y g r i d L i n e : i f ( yd i f f > O ) g r i d_y= ( ( i n t ) y & Ox f f c O ) +64; I I I f ray d i r e c t i on n e ga t i ve i n y, get L a s t y g r i d L i ne : e l s e g r i d_y= ( ( i n t ) y & Ox f f c O ) - 1 ; I I G e t x , y coord i na t e s w h e r e r a y c r o s s e s x g r i d L i n e : x c r o s s_x=g r i d_x ; x c r o s s_y=y+s l o pe* ( g r i d_x - x ) ; I I G e t x , y coord i n a t e s w h e r e ray c r o s s e s y g r i d L i n e :
CHAPTE:R ElEVEN
Lightso u rc i ng
y c ro s s_x=x+ ( g r i d_y-y ) l s l op e ; y c r o s s_ y= g r i d_ y; II
Get d i s t a n c e t o x g r i d
L i ne :
xd=x c ro s s_x-x ; yd=x c ro s s_y-y; xd i s t =s q r t ( xd* xd+yd*yd ) ; I I Get d i s t ance t o y g r i d
L i ne
xd=yc r o s s_x- x ; yd=yc r o s s_y-y; yd i s t = sq r t ( x d *xd+yd*yd ) ; II
If x grid
if
( x d i s t V I E W P O R T_BOT ) d h e i g h t -=
{
( bot - V I E W PORT_BOT ) ;
i h e i g h t -= ( bo t - V I E W P O RT_BOT ) *y r a t i o ; b o t = V I EW PO RT_BOT; } II
Po i n t to v i deo memo r y o f f s e t
for
top of
line:
o f f s e t = t o p * 3 2 0 + c o l um n ; II
I n i t i a l i z e ve r t i c a l
i nt
e r ro r t e rm f o r t e x t u re map :
t ye r ro r=64;
II W h i c h g ra ph i c s t i l e a r e w e u s i n g ? i nt II
t i l e=ma p [ x ma z e J [ yma z e J - 1 ; F i nd offset
of
t i l e and c o l umn i n b i tma p :.
u n s i g n e d i n t t i l e p t r = ( t i l e i 5 ) * 320* I MAGE_H E I G H T+ ( t i l e % 5 ) * I M A G E_W I D T H + t ; II
L o o p t h roug h t h e p i x e l s
II
l i ne , a d va n c i ng O F F S ET t o t h e n e x t
II after each pixel for II
( i n t h =O ; A r e we
i n t he c u r rent vert i ca l
i s d ra w n .
h = I M A G E_H E I G HT ) II
I f so,
int
Lightsou rcing
{
d raw i t :
l eve l = l i t e l eve l [ d i s t a n c e ]+ambi e n t_l eve l + f l oo r l i t e s [ xm a z e ] [ yma z e J ;
if
( l eve l > M AX L I G H T )
l eve l=MA X L I G H T ;
s c r een [ o f f s e t J = l i t e s o u r c e [ l eve l J [ t e x t m a ps [ t i l e p t r J J ; I I Reset e r ro r t e r m : tye r ro r-= I MA G E_H E I G H T ; I I A n d adva n c e O F F S ET to n e x t
s c reen l i n e :
o f f s e t +=320; } II
I n c r eme n t a l d i v i s i on :
tye r r o r+=he i g h t ; I I Adva n c e T I LE P T R t o n e x t
l i n e o f b i t ma p :
t i l e p t r+=320; } II S t ep t h ro u g h f l o o r p i x e l s : for ( i nt I I Get f l oat
row=bo t + 1 ;
row > ; I I T r a n s l a t e re l a t i v e to v i ewe r coord i n a t e s : x+=xv i ew ; y+=yv i e w ; I I G e t m a z e squa re i n t e r s e c t e d b y r a y : i n t xma z e x I 64; i n t yma z e = y I 6 4 ; I I F i nd r e l evant co l umn of t e x t u r e ma p : i n t t = ( ( i n t ) y & Ox 3 f ) * 320 + ( ( i n t ) x & Ox3f ) ; I I W h i c h g r a ph i c s t i l e a r e w e u s i n g ? i n t t i l e = c e i l i n g [ xma z e ] [ yma z e J ; I I F i nd o f f s e t o f t i l e a nd c o l umn i n b i tma p : u n s i gned i n t t i l ep t r = ( t i l e i 5 ) * 320* I MAGE_H E I GHT+ ( t i l e%5 ) * I MAGE_W I DTH+t ; I I C a l c u l a t e v i deo o f f s e t of f l oor p i xe l : o f f s e t = row*320+co l um n ; I I D r aw p i x e l : i n t l e ve l = l i t e l eve l [ d i s t a n c e ] +amb i e nt_l eve l + c e i l i ng l i t e s [ xm a z e ] [ yma z e J ; i f ( l eve l > MA X L I G H T ) l e ve l =MAX L I G H T ; s c reen[ o f f s e t J = l i t e so u r c e [ l e v e l ] [ t e x t maps [ t i l e p t r J J ; } } }
C HAPTER ElEVEN
L i g htso u rcing
The MA P D E M O . C P P Prog ra m The text o f MAPDEMO.CPP, a short demonstration p rogram that calls the lightmapped version of the draw_mazeO function to draw a lightmapped view of a maze, appears in Listing 1 1 - 5 .
Listing 1 1 - 5 The MAP D E M O . C P P p ro g ra m I I M A P D E MO . C PP II I I Ca l l s r a y-ca s t i ng fun c t i on t o d r a w
L i g h t-mapped
II v i ew o f ma z e . II I I W r i t t e n b y C h r i s t o p h e r Lamp t o n f o r I I Ga r d e n s o f
I m a g i n a t i o n ( Wa i t e G roup P r e s s )
# i nc l ude
< s t d i o . h>
# i n c l ude
# i n c l ud e
< c on i o . h>
# i n c l ud e
< s t d l i b . h>
# i n c l ud e
# i n c l ud e
" s c reen . h "
# i n c lude
"pcx . h "
# i n c l ude
" L i t e ma p . h "
cons t G R I D S I Z E = 1 6 ; c o n s t MAX D I S T A N C E=64* G R I D S I Z E ; c o n s t NUM_I M A G E S = 1 5 ; c o n s t f l o a t M U LT I PLI E R = 3 ; p c x_s t r u c t
t e x t ma p s ;
F I LE *hand l e ; u n s i gned c h a r
L i t e t a b l e [ MA X L I GHT+1 J [ PA L E T T E S I Z E J ;
u n s i gne d c h a r * L i t e l e ve l ; I I M a p o f wa l l
textures :
map_type wa l l s={ 5,
5, 5, 5,
{ 5,
5, 5,
{ 5,
5 , 0 , 5 , 0 , 0 , 0 , 2 , 0 , 0,
5,
5,
5,
5,
5},
0, 0, 0 , 0 , 0 ,
5},
5,
5,
5,
5,
{ 5 , 0 , 0, 0 , 0 , 0, 0 , 0 , 3 , 0 , 0 , 0 , 0 , 0, 0 , 5 } , { 5,
0, 0, 0, 0 , 0 ,
{ 5,
0,
0, 0, 0,
6,
0,
0,
0 , 0 , 0, 0, 0 , 0,
{ 1 2 , 0, 0,
0,
0,
{ 1 1 , 0, 0, 0,
0,
5, 4 ,
0,
0, 0 , 5 } ,
0 , 0,
0, 5},
0,
0, 0, 0, 0 , 1 1 , 1 1 , 0 , 0 , 0 , 5 } ,
0, 0,
0, 0, 0, 0, 0 , 1 1 , 1 0 , 0 , 0 , 5 } , 0, 0, 0, 0 , 0 , 0 , 1 0 , 0 , 0,
5},
5 , 0 , 0 , 0 , 0, 0 , 0 , 0 , 0, 1 1 , 1 0 , 0 , 0 ,
5},
{ 1 1 , 0, 0 , 0, 0, {1 1 , 5,
0,
0,
{ 1 1 , 0 , 0 , 0, 0 , 0 , 0 , 0, 0 , 0 , 1 1 , 1 1 , 0 , 0 , 0 , 5 } , 0,
5,
1 , 0 , 1 , 0, 0 , 0 ,
0, 0, 0,
0,
1 , 0, 1 , 0, 0, 0, 0, 0, 5},
{ 1 2 , 5 , 5 , 0 , 0, { 5 , 0 , 0,
0, 0 , 5 } , continued on nextpage
ffi1
GARDENS OF I MAGI NATION continu=2 ) vi ewi ng_ang l e = a t o f C a r gv[ 1 J ) ; C a r g c >=3 ) i n t e n s i ty= a t o f C a r gv [ 2 J ) ; C a r g c >=4 ) a mb i e n t_L eve l =a t o f C a rgv[ 3 J ) ;
I I Load t e x t u r e map i ma g e s : i f C l oadPCX C " i ma g e s 3 . p c x " , & t extmaps ) ) e x i t C 1 ) ; I I Load L i g h t s ou r c i n g t a b l e s : i f C C hand l e=fope n C " L i t e s o r c . d a t " , " r b" ) ) ==NU L L ) {
continued on next page
G A R D E N S OF I MAG I NATION continuedfrom previous page
p e r r o r ( " E r ro r " ) ; ex i t ; } f r e a d ( l i t e t a b l e , MA X L I GHT+1 , PA L E T T E S I Z E , h a nd l e ) ; f c l o s e ( h a nd l e ) ; I I A l l o c a t e memo r y f o r a r ra y o f L i g h t L e ve l s : L i t e l ev e l =n e w u n s i g n e d c h a r [ MA X D I STA N C E J ; I I C a l c u l a t e L i g h t L eve l s b y d i s t a n c e : f o r ( i n t d i s t a n c e = 1 ; d i s t a n c e 1 . 0 ) r a t i o= 1 . 0 ; L i t e l e ve l [ d i s t a n c e ] = r a t i o*MA X L I GHT ; } I I Poi n t va r i a b l e a t v i deo memory : c h a r f a r * s c reen= ( c h a r f a r * ) MK_F P ( O xa000,0 ) ; I I S a v e p revi o u s v i deo mode : i n t o l dmode=* ( i n t * ) MK_F P ( Ox 4 0 , 0 x 49 ) ; I I S e t mode 1 3 h : s e tmode ( Ox 1 3 ) ; I I S e t t he pa l e t t e : s e t pa l e t t e ( t e x t m a ps . p a l e t t e ) ; I I C l e a r t h e s c reen : c l s ( s c r een > ; I I D ra w a r a y- c a s t v i ew o f t h e ma z e : d r a w�a z e ( wa l l s , f l o r , c e i l i n g , f loor l i t e s , c e i l i n g l i t e s , s c r e en , x v i e w , yv i ew , v i ewi n g_a ng l e , v i ewe r_h e i g h t , ambi e n t_Leve l , t e x t maps . i ma g e , L i t e t a b l e , L i t e l eve l ) ; I I W a i t for u s e r t o h i t a key : w h i l e ( ! k bh i t ( ) ) ; I I R e l ea s e memo r y d e l e t e t e xtma p s . i ma g e ; d e l e t e L i te l e ve l ; I I R e s e t vi d e o mode a nd e x i t : s e t mo d e ( o l dmode ) ;
To run the program, go to the LIGHT directory and type MAPDEMO. The result is shown in Figure 1 1 - 1 1 . Several portions of floors, walls, and ceilings have been illuminated, an effect that becomes more obvious in the distance, where the light from the player's torch becomes less intense. As in the previous demo, you can type in three command line arguments, represenring the angle of
CHAPTER ELEVEN
Lightsou rcing
view, the intensity of the light carried by the viewer, and the ambient lighting Ievel. If you type MAPDEMO 3 . 1 4 0 0 you'll see the same view as above, but with the player's light turned completely off. Now the lightmapping is the only source of illumination, an effect that is quite dramatic but not entirely realistic. In the real world, at least some light would spill over from the illuminated areas into the rest of the scene. We can simulate this effect by raising the ambient Ievel ever so slightly, with the instruction MAPDEMO 3. 1 4 0 4 (see Figure 1 1 - 1 2) . O r w e can raise t h e i n tensity o f t h e torch inst ead, with t h e i nstruction MAPDEMO 3. 1 4 4 0, as shown in Figure 1 1 - 1 3. Play around with different angles, source intensiries, and ambient Ievels for a while and see what sort of effects you can produce.
Ti l e d Lig htm a ps The limitations of block-aligned lightmaps are pretty much the same as the limitations of block-aligned heightmaps. Changes in light intensity can only occur ar block boundaries and musr therefore have squared off edges, an effect that isn'r always realistic. J ust as tiled heightmaps helped us solve some o f the problems o f block-aligned heightmapping, tiled lightmaps can help u s solve the
Figure 1 1 -1 1 Lightmap tiles in a maze
Figure 1 1 -1 2 Lightmap tiles glowing in
otherwise lit by the viewer's torch
the middle of a pitch -dark maze
Figure 1 1 -1 3 Lightmap tiles in a slightly better illumi nated maze
GARDENS OF I MAG I NATI ON
problems of block-aligned lightmapping. And tiled lightmaps are easier to use than tiled heightmaps, since we don't have to worry about raised pixels blocking our view of height changes that fal l partially in their shadow. By now, you can probably figure out how tiled lightmaps work without any prompting from me. There's a PCX file containing multiple lightmap tiles in the LIGHT directory under the name LITEMAPS. PCX. It consists of a nine-part map forming a large circular pool of light (the middle tile of which doubles as a fully illuminated square of light) , a single tile representing a small circular pool of light, and a tile with a diagonal slash of light. Each pixel in the lightmaps has a color value in the range 0 to 3 3 , representing the amount to be added to the intensity val ues of the corresponding screen pixels, the total value not to exceed 32. We'll still pass the jloorlites[} and ceilinglites[} arrays to the draw_maze() functio n . And - fo r yet another world record - we' ll pass an addi tional parameter called litemaps that points to the light tiles themselves. The new prototype, in the file TLMAP. H , Iooks like this: vo i d d r a w_ma z e ( ma p_t ype m a p , m a p_t ype f l oo r , ma p_t ype c e i l i n g , map_t ype f l oor l i t e s , m a p_type c e i l i n g l i t e s , c h a r f a r * s c r e en , i n t x v i ew, i n t yvi ew, f l oa t v i e w i ng_a n g l e , i n t v i ewer_he i g h t , i n t a m b i ent_L eve l , u n s i gned c h a r f a r *t extmaps, u n s i g n e d c h a r f a r * L i t ema p s , u n s i g n e d c h a r [ M A X L I GHT+ 1 J [ PALETT E S I Z E J , u n s i g n e d c h a r * L i t e l e ve l ) ;
Before we place a pixel o n the display, we've been calculating a value called tileptr to point to the offset in the texture-map array that contains the color value for that pixel. Now we'll use exacdy the same method that we used to calculate tileptr to calculate a value that we'll call liteptr, which will point to the value in the lightmap array that represents the light intensity of the current pixel: t i l e= f l oo r l i t e s [ xma z e J [ ym a z e J ; u n s i gned i n t l i t e p t r = ( t i l e / 5 ) * 320* I M A G E_H E I GHT+ ( t i l e % 5 ) * I M A G E_W I DTH+ t ;
Then we can add this value t o the intensity value for the pixel: i n t L ev e l = L i t e l eve l [ d i s t a n c e ] +amb i e n t_ L eve l + L i temap s [ L i t ept r J ;
And that's all the code that we need to add to use tiled lightmaps.
Th e Ti l e d Lig htma p p i n g d raw_m a z e o Fu ncti o n The complete text o f the tiled lightmapping version of draw_maze() appears in Listing 1 1 -6.
C HAPTER ElEVEN
Lig htsourcing
Listing 1 1 -6 The t i l ed l i g h tm a p p i n g d raw_maze function II II II II II II II
LITE MAP . C PP Fu n c t i o n to d r a w t e x t u re-mapped wa l l s , f l oo r s an d c e i l i n gs u s i n g ray c a s t i n g . W r i t t en by C h r i s t o p h e r Lampton f o r GARDENS O F IMAGI NATION ( W a i t e G r oup P r e s s ) .
# i n c l ude # i n c l ude # i n c l u de # i n c l u de
< s t d i o . h>
" t l map . h " "pcx . h "
II Co n s t a n t de f i n i t i o ns : I I H e i g h t of wa l l i n p i x e l s const WALL_H E I G H T=64; II V i ew e r d i s t a n c e f r om s c r een const V I E W E R_DI STAN C E = 1 92; const V I EWPO RT_L E F T=O; I I D i m e n s i ons of v i ewpo r t const V I E WPORT_R I G HT=3 1 9 ; const V I E W PORT_TOP=O; const V I E W PORT_BO T= 1 9 9; const VI EWPORT_H E I GHT=VI EWPORT_BO T-VI EWPO RT_TOP; const VI EWPORT_C E N T E R =VI EWPORT_TOP+V I EWPO RT_H E I G H T I 2 ; const G R I D S I Z E = 1 6 ; const MAX DI STANC E=64 *GR I D S I Z E ; vo i d draw_ma z e ( map_ty pe map,map_type f l oor,map_type c e i l i ng , map_type f l oor l i t e s ,map_type c e i l i n g l i t e s , c h a r f a r * s c r e e n , i n t x v i ew, i n t yv i ew , f l oat vi ewi nq_a ng l e , i n t vi ewer_h e i g h t , i n t ambi ent_l eve l , u n s i gned c h a r f a r * t e x t ma ps, u n s i gned c h a r far * l i t emaps, u n s i gned c h a r l i t e so u r c e [MAX L I G H T+ 1 J [ PALETTE S I Z E J , u n s i g n e d c h a r * l i t e l eve l ) II II II II
Draws a ray- c a s t i ma g e i n t h e v i ewpo rt of t h e ma z e r e p r e s e n t e d i n a r ray MA P [ J , a s se en f rom pos i t i on XVI EW, Y V I E W b y a vi ewer lo oki ng a t ang l e V I E W I N G_ANGLE w h e r e a n g l e 0 i s due no r t h . ( A ng l e s a r e measu red i n ra d i a n s . )
{ II Va r i a b l e dec l a r a t i o ns : I I P i x e l y pos i t i on and o f f s e t i n t sy, o f f s e t ; II D i s t a n c e to next wa l l in x and y f l oa t xd,yd; II Coord i na t e s of x and y g r i d l i ne s i n t g r i d_x, g r i d_y; f l oat x c r oss_x , x c ross_y; I I R a y i n t e r s e c t i o n Coor d i n a t e s continued on next page
GARDENS OF I MAGI NATION continuedfrom previous page
f l oa t y c ross_x , y c r o s s_ y ; u n s i g n e d i n t xd i s t , yd i s t ; I I D i s t a n c e to x a n d y g r i d l i ne s i n t xma z e ,yma z e ; I I Map l o c a t i o n o f ray co l l i s i o n i nt d i s t a nce; I I D i s t a n c e to wa l l a l ong ray I I C o l umn i n t e x t u r e map i n t t m c o l um n ; f l o a t y ra t i o ; I I Loop t h rough a l l c o l um n s o f p i xe l s i n vi ewpo r t : f o r ( i n t co l umn=V I E WP ORT_L E F T ; c o l u mnO ) g r i d_x = < < i n t ) x & Ox f f c 0 ) +64; II If ray d i re c t i o n ne ga t i v e i n x , g e t L a s t x g r i d L i n e : e l s e g r i d_x = ( ( i n t ) x & Ox f f c O ) 1; -
I I I f ray d i re c t i o n po s i t i v e i n y , g e t next y g r i d L i n e : i f ( y d i f f >O ) g r i d_y= ( ( i n t ) y & Ox f f c O ) +6 4;
CHAPTE:R ELEVEN II I f ray d i re c t i o n neg a t i v e i n y, g e t e l s e g r i d_y= ( ( i n t ) y & O x f f c Q ) - 1 ;
Lig htsourcing
Last y g r i d L i n e :
I I Get x , y coo r d i n a t e s w h e r e ray c r osses x g r i d L i n e : x c r o s s_x=g r i d_x ; x c r oss_y=y+s l o pe * ( g r i d_x-x ) ; I I Get x , y co o r d i n a t e s w h e r e ray c r o s s e s y g r i d L i n e : y c r o s s_x=x+ ( g r i d_y-y ) l s l ope; yc ross_y=g r i d_ y; II Get d i s t a n c e t o x g r i d L i n e : xd= x c r o s s_x-x; yd=x c r o s s_y-y; x d i s t = sq r t ( xd*xd+yd*yd ) ; I I Get d i s t a n c e t o y g r i d L i n e : xd=y c r o s s_x-x; yd=yc ross_y-y; yd i s t = sq r t ( xd*xd+yd*yd ) ; I I I f x g r i d L i ne i s c l o s e r . . . i f ( x d i s t V I E W PORT_BOT )
{
d h e i g h t -= ( bo t - V I E W PORT_BOT ) ; i h e i g h t -=
( bo t - V I EWPO RT_BOT ) *y r a t i o ;
b o t = V I EW PORT_BO T ; } II
P o i n t t o v i deo memo ry o f f s e t
f o r top of
L i ne :
o f f s e t = t o p * 320 + c o l um n ; II
I n i t i a l i z e ve r t i c a l
i nt
error
t e rm f o r t e x t u r e ma p :
t y e r r o r=64 ;
I I W h i c h g ra p h i c s t i l e a re we u s i n g ? i nt II
t i l e =m a p [ xma z e ] [ yma z e J - 1 ; F i n d o f f s e t of
u n s i g n ed i n t
t i l e and c o l umn
* I MAGE_W I D T H + t ; II
Whi ch
i n b i tma p :
t i l e p t r = ( t i l e i 5 ) * 3 2 0 * I MAGE_H E IGHT + ( t i l e% 5 )
L i ght
t i l e a r e we u s i ng ?
t i l e =f l oo r l i t e s [ x ma z e ] [ yma z e J ;
C HAPTER ElEVEN II
F i nd o f f s e t o f
u n s i gned
i nt
t i l e and c o l umn
in
Lig htsourcing
bi tmap :
l i t e p t r = ( t i l e 1 5 ) * 3 2 0 * I MAGE_H E I G H T+ ( t i l e % 5 )
* I M AGE_W I D TH+ t ; II
Loop t h ro u g h
II
l i n e , a dva n c i ng O F F S E T t o t h e n e x t
the pi xe l s
I I a f t e r ea c h p i x e l for
( i n t h=O;
I I A r e we whi le II
i n t h e c u r r ent v e r t i c a l row o f p i x e l s
i s d r awn .
h = I MAGE_H E I G H T )
{
I f s o , d ra w i t :
i nt
l e ve l = l i t e l eve l [ d i s t a n c e J +ambi ent_l eve l + l i t emap s [ l i t e p t r J ;
if
( l eve l>MAX L I G H T )
l e v e l =MAX L I G H T ;
s c r e en [ o f f s e t J = l i t e s o u r c e [ l eve l J [ t e x tmap s [ t i l ep t r J J ; I I R e s e t e r ro r t e rm : t ye r r o r- = I M A G E_H E I G H T ; I I A n d adva n c e O F F S E T t o n e x t s c reen
l i ne :
o f f s e t +=320; } II
I n c reme n t a l d i v i s i on :
t y e r r o r+=he i g h t ; I I Advan c e T I L E P T R * L I T E PTR t o n e x t
l i n e o f b i tma p :
t i l e pt r+=320; l i t ept r+=320; } I I S t e p t h rough for
( i nt
I I Get f loat
f l oor p i x e l s :
row=bo t + 1 ;
row=V I E W PORT_T OP;
--ro w ) to pixel
{ he i g h t :
r a t i o= ( f l o a t ) ( WALL_H E I GHT-v i e wer_h e i g h t ) l ( 1 00-row ) ;
G e t d i s t a n c e t o v i s i b l e p i xe l :
d i s t a n c e = r a t i o *V I EW E R_D I S TANC E i c o s ( c o l umn_a n g l e ) ; II
R o t a t e d i s t a n c e t o ray ang l e :
i nt x
- di stance *
i nt y = di stance * I I T ra n s l a t e
( s i n ( r a d i a ns ) ) ;
( c o s ( radi ans ) ) ;
r e l a t i v e to v i e w e r c o o r d i n a t e s :
x+= x v i ew ; y+=yv i e w ; II
Get maze
i n t x ma z e
squa r e
i n t e r s e c t ed by r a y :
x I 64 ;
i n t yma z e = y I 64 ; II i nt
F i nd re l ev a n t t =
c o l umn of t e x t u r e map :
( ( i n t ) y & Ox 3f )
I I W h i ch g r a ph i c s
* 320 + ( ( i n t ) x & Ox3f ) ;
t i l e a r e we u s i ng ?
CHAPTER ELEVEN
Lig htsou rc i n g
i n t t i l e= c e i l i n g [ xma z e J [ yma z e J ; II
F i nd o f f s e t o f t i l e a n d c o l umn
u n s i gned i n t
i n b i t ma p :
t i l e pt r= ( t i l e i 5 ) * 320* I MAGE_H E I G HT + ( t i l e % 5 )
* I MAGE_W I D TH+ t ; I I Wh i c h
L i g ht
t i l e a r e w e u s i ng ?
t i l e= c e i l i ng l i t e s [ x m a z e J [ yma z e J ; II
F i nd offset of
u n s i gned i nt
t i l e and c o l umn
i n bi tm a p :
l i t e p t r = ( t i l e i 5 ) * 320*IMAGE_H E I G HT+ ( t i l e % 5 )
* I MAGE_W I D T H + t ; II
Ca l c u l a t e v i deo o f f s e t
of
f l oor p i x e l :
o f f s e t = r ow*320+co l u mn ; II
Draw p i xe l :
i nt
L e ve l = L i t e l e ve l [d i s t a n c e ] +ambi ent_L eve l + L i t emaps [ L i t e pt r J ;
if
( L eve l >MAX L I G H T )
l eve l =MAX L I GHT;
s c r e e n [ o f f s e t J = L i t e s o u r c e [ L eve L J [ t e x t m a p s [ t i l ept r J J ; } } }
The TLM D E M O . C P P P ro g ra m To demonstrate the tiled lightmapping version of the draw_mazeO function, we'll use the simple program in Listing 1 1 -7.
Listing 1 1 -7 The TLM D E M O . C P P prog ra m II
T LMDEMO . C P P
II II
Ca l l s
ray-ca s t i ng f u n c t i on t o d r a w t i l e d
L i g h t-mapped
I I v i ew o f ma z e . II I I W r i t t en by C h r i s t o p h e r L a m p t o n f o r I I Ga rdens of
Im ag i n a t i o n ( Wa i t e G r o u p P r e s s )
# i n c l ude
# i n c l ude
# i n c l ude
< c on i o . h>
# i n c l ude
< s t d l i b . h>
# i n c l ude
# i n c l ude
" s c reen . h "
# i n c l ude
"pcx . h ., continrud on next page
GAR DENS O F I MAG I NATI O N continuedfrom previous page
# i n c l ud e
" t lmap . h "
c o n s t G R I D S I Z E =1 6 ; c o n s t M A X D I STANC E=64*G R I D S I Z E ; c o n s t N U M_I MA G E S = 1 5 ; const
f l oa t M U LT I P L I E R = 3 ;
p c x_s t r u c t
tex tmaps , L i temaps;
F I LE *hand l e ; un s i g n e d c h a r
l i t e t a b l e [ M A X L I G H T + 1 ] [ P A L E TT E S I Z E J ;
un s i g n e d c h a r * L i t e l eve l ; I I M a p o f wa l l
t e x t u re s :
m a p_t ype wa l l s= { {
5,
5,
{
5,
5 , 0 , 5 , 0 , 0 , 0,
{
5 , 0 , 0 , 0,
{
5,
{
5 , 0,
5, 5,
5,
5,
5,
5,
5,
2 , 0 , 0,
5,
5,
5},
0 , 0 , 0 , 0, 0,
5},
5,
5,
5,
0,
3,
0,
0, 0,
5},
0 , 0 , 0, 0 , 0 , 0 , 0,
0,
6 , 0 , 0 , 0, 0 , 0,
5},
0,
0,
0,
0,
0,
5, 0,
0,
0,
0,
0, 0 , 0,
5},
{ 1 2 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, 0 , 1 1 , 1 1 , 0 , 0 , 0,
5},
{ 1 1 , 0 , 0 , 0 , 0, 0 , 0 , 0 , 0, 0,
5},
0 , 0,
0, 0,
5,
0,
5,
4,
0 , 1 1 , 1 0 , 0 , 0,
0 , 0 , 0, 1 0 , 0 , 0,
5},
5 , 0 , 0, 0 , 0 , 0, 0 , 0 , 0 , 1 1 , 1 0 , 0 , 0,
5},
{ 1 1 , 0 , 0 , 0 , 0, 0, {1 1 ,
0,
0,
0,
{ 1 1 , 0 , 0 , 0 , 0, 0 , 0 , 0, 0 , 0 , 1 1 , 1 1 , {12,
5,
0 , 0 , 0,
5},
5,
1 , 0,
1 , 0, 0,
0, 0, 0,
5},
0, 0 , 0 , 0 , 0 ,
1 , 0,
1 , 0, 0,
0, 0, 0,
5},
5 , 0,
0,
0,
{
5 , 0,
{
5 , 0 , 0 , 0 , 0 , 0 , 0,
1,
0,
0, 0 , 0 , 0 , 0, 0, 1 2 } ,
{
5 , 0,
1,
0,
1 , 0 , 0, 0 , 0, 0, 1 1 } ,
{
5 , 6 , 6, 6,
6,
0, 0, 0,
0,
1 , 0 , 0, 0 , 0 , 0, 1 0 } ,
{
5,
5,
5,
5,
0 , 0 , 0, 5, 5,
0,
0,
5, 1 1 , 1 1 , 1 1 ,
5,
5,
5,
5,
5,
5}
} ,. I I M a p o f f l oor t e x t u r e s : m a p_t ype f l o r = { {
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5, 8,
5,
5,
5,
5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5, 5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5, 5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5,
5},
{
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5, 5,
5,
5,
5,
5},
{ } .
5,
5,
5, 5,
5,
5,
5,
5,
5,
5,
5, 5,
5,
5,
5, 5}
,
I I M a p of
cei l i ng textures :
C HAPTER ELEVEN
L i g htsourc i n g
map_type c e i l i ng = { { 1 3 , 1 3 , 1 3, 1 3, 1 3, 1 3, 1 3 , 1 3, 1 3 , 1 3, 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3}, { 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3, 1 3, 1 3, 1 3, 1 3 , 1 3, 1 3, 1 3 , 1 3, 1 3 , 1 3}, { 1 3, 1 3, 1 3 , 1 3 , 1 3 , 1 3, 1 3, 1 3, 1 3, 1 3 , 1 3, 1 3, 1 3, 1 3, 1 3 , 1 3 } , { 1 3, 1 3, 1 3 , 1 3 , 1 3 , 1 3, 1 3, 1 3, 1 3, 1 3 , 1 3, 1 3, 1 3, 1 3 , 1 3 , 1 3 } , { 1 3, 1 3, 1 3 , 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 2 , 1 3, 1 3, 1 3, 1 3 , 1 3} , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3, 1 3, 1 3, 1 3 , 1 3 } , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3, 1 3, 1 3, 1 3, 1 3 , 1 3}, { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3, 1 3 , 1 3}, { 1 3, 1 3, 1 3, 1 3, 1 3 , 1 3 , 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3, 1 3 , 1 3}, { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3} , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3} , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3} , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3 , 1 3, 1 3} , { 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3 , 1 3 , 1 3} , { 1 3, 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3 , 1 3, 1 3 , 1 3, 1 3, 1 3 , 1 3 , 1 3 , 1 3, 1 3} } ,. I I M a p of f l oor
L i g h t i ng :
ma p_type f l oor L i t e s = { 1 , 2 , 3, 0, 0, 0, 0,
0, 0 , 0 , 0 , 0 } ,
{ 0, 0, 0, 0, 2 , 7, 1 2 , 0 , 0 , 0 , 0,
0, 0 , 0 , 0 , 0 } ,
{ 0, 0, 0, 0 ,
0,
0,
0,
0},
{ 0, 0, 0 , 0, 3, 8 , 1 3 , 0 , 0 , 0, 0,
0,
{ 0, 0, 0 , 0, 0, 0, 0 , 0, 0, 0 , 0 , { 0, 0, 0, 0, 0, 0, 0 , 0 , 0 , 0, 0,
0, 0 , 0 , 0,
0},
0, 0, 0 , 0,
0},
{ 0, 0, o, 0, 0, 0, 0 , 0, 0, 0, 0, 0 , 0 , 0 , 0 , 0 } , { 5 , 0 , 0, 0 , 0, 0, 0 , 0 , 0 , 0, 0 , 0 , 0 , 0 , 0 , 0 } , 0,
0,
0},
{ 0, 0, 0 , 0, 2 , 7, 1 2 , 0 , 0 , 0, 0, { 0, 0, 0, 0, 3, 8, 1 3, 0, 0 , 0, o,
0, 0, 0 , 0,
0},
0, 0, 0 , 0,
0},
{ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, { 0, 0, 0 , 0, 0, 0, 0 , 0, 0 , 0, 0,
0, 0 , 0, 0,
0},
{ 0, 0 , 0, 0,
1,
6 , 1 1 , 0 , 0 , 0 , 0,
0,
0,
0, 0 , 0 , 0 , 0 } ,
{ 0 , 0, 0, 0, 0, 0, 0, 0, 0 , 0 , 0, 0, 0 , 0, 0, { 0 , 0, 0 , 0, 0, 0, 0, 0 , 0 , 0, 0, 0, 0, 0, 0,
0}, 0},
{ 0, 0 , 0, 0 , 0, 0, 0, 7, 7, 0, 0, 0, 0 , 0, 0, 0}, { 0, 0, 0, 0, 0, 0, 0, 7, 4, 0, 0, 0 , 0 , 0, 0 , 0 } }. ,
I I M a p of c e i l i ng
L i g h t i ng :
ma p_type c e i l i n g l i t e s = { 0, 0, 0 , 0 , 0 , 0,
0},
{ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, 0 , 0 , 0 , 0 , { 0, 0, o, 0, 0, 0, 0 , 0 , 0 , 0, 0, 0 , 0 , 0, 0,
0},
{ 0 , 0 , 0 , 0 , 0,
0,
0,
0,
0,
0},
{ 0, 0, 0, 0, 0, 0, 0, 0, 0 , 0, 0, { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0,
0, 0, 0, 0 , 0 } ,
{ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0,
0, 0 , 0, 0 , 0},
0, 0 , 0, 0,
0},
0, o , o , 0, o,
0, 0 , 0 , o , 0 } ,
{ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0,
0, 0 , 0, 0 , 0 } ,
5 , 0 , 0 , 0 , 0,
0, 0 , 0, 0 , 0 } ,
{ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0,
0, 0 , 0 , 0 , 0},
{ 0 , 0 , 0,
0,
0, 0,
{ 0, 0, 0, 0, 0 , 0 , { 0, { 0,
0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, 0 , 0 , 0 , 0 , 0 } , 0, 0, 0 , 0 , 0, 0 , 0 , 0 , 0 , 0 , 0, 0 , 0 , 0 , 0 } ,
continued on nextpage
G A R D E N S OF I MAG I NAT I O N continued.from previous page
{ 0, 0, 0, 0, 0, 0, 0, {
0,
0,
{ 0, 0, { 0, } .
0,
0, 0 , 0 , 0 , 0,
0,
0, 0,
0},
0 , 0 , 0 , 0 , 0, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0,
0},
0, 0 , 0, 0 , 0,
7, 0 , 0,
0 , 0,
0 , 0 , 0,
0},
0 , 0 , 0, 0,
0, 0,
0, 0 ,
0 , 0 , 0, 0}
0,
0,
,
f l o a t v i e w i ng_a n g l e =O; f loat
i n t e n s i t y=MAX L I G H T ;
i n t v i e w e r_he i g h t = 3 2 ; i nt
a m b i e n t_L eve l =O ;
i nt
x v i ew=8*6 4 + 3 2 ;
i n t y v i ew=8*64+32; i nt
L i g h t t ype=1 ;
v o i d ma i n ( i n t a r g c , c h a r * a r g v [ J ) { II
R e a d a r g ume n t s f r om comma n d
if
( a r g c>=2 ) v i e w i ng_a ng l e= a t o f ( a r g v [ 1 J ) ;
L i ne
i f present :
if
( a r g c>=3 )
i n t e n s i t y=a t o f ( a r g v [ 2 J ) ;
if
( a r g c>=4 )
ambi e n t_ L e v e l = a t o f ( a rg v [ 3 J ) ;
II
Load t e x t u r e m a p i ma g e s :
if
< L oa d P C X ( " i ma g e s 3 . p c x " , & t e x t m a ps ) )
II
Load
if
< L oa d PCX ( " L i t emaps . p c x " , & L i t emaps ) )
II
Load
if
( ( h a n d l e = fope n ( " L i t e s o r c . da t " , " r b " ) ) ==NU L L )
ex i t ( 1 ) ;
L i gh t map t i l e s :
L i g h t sou r c i ng
ex i t ( 1 ) ;
tab les : {
p e r r o r ( " E r ro r " ) ; exi t ; } f r e a d ( l i t e t a b l e , MA X L I GHT+1 , P A L E T T E S I Z E , h a nd l e ) ; f c l o s e ( h a nd l e ) ; I I A l l o c a t e memo ry f o r a r r a y o f
L i gh t
L e ve l s :
L i t e l ev e l =new u n s i g n e d c h a r [ MAX D I STAN C E J ; II
Ca l cu late
for
L i ght
L ev e l s b y d i s t a n c e :
( i n t di s t a n c e = 1 ;
f l oat if
d i s t a n c e 1 . 0 )
r a t i o= 1 . 0 ;
L i t e l e ve l [ d i s t a n c e J = ra t i o*MAX L I G H T ; } II
P o i n t va r i a b l e a t v i deo memo r y :
char
f a r * s c r e e n = ( c h a r f a r * ) M K_F P ( O x a 000,0 ) ;
I I S a v e p r e v i ous v i deo mod e : i n t o l dmode=* < i n t * ) M K_F P ( O x 4 0 , 0 x 49 ) ;
{
CHAPTER ElEVEN II
Set mode
Lightsou rcing
1 3h :
se tmode ( Ox 1 3 ) ; II
Set t h e pa l e t t e :
s e t p a l e t t e ( t e x tmaps . p a l e t t e ) ; II
C l ea r t h e s c reen :
c l s ( s c r ee n ) ; II
Draw a
ray- c a s t
v i ew o f
t h e ma z e :
d r a w_ma z e ( wa l l s , f l o r , c e i l i n g , f l oo r l i t e s , c e i l i ng l i t e s , s c r ee n , xv i e w , yv i e w , v i e w i ng_a n g l e , v i e w e r_h e i g h t , ambi ent_L eve l , t e x tmaps . i ma g e , L i tem a p s . i ma g e , L i t e t a b l e , L i t e l e ve l ) ; I I Wa i t whi le
for user t o h i t a
key :
( ! kb h i t ( ) ) ;
I I Re l e a s e memo ry d e l e t e t e x t maps . i ma g e ; d e l et e
L i t emaps . i ma g e ;
de l e t e
L i t e l eve l ;
I I R e s e t vi deo mode and e x i t : se tmode ( o l dmode ) ; }
Yo u can use this program by going ro the LIGHT directory and typing TLMD EMO. The result, with a large circular pool of light visible in the distance, is shown i n Figure 1 1 - 1 4 . As before, you can pass the program command line parameters representing the viewing angle, the intensity of the viewer's torch, and the ambient lighting level. Look around, experiment a little, and see what works and what doesn't.
Figure 1 1 -1 4 A screenshot from the TLMDEMO program
GARDENS O F I MAG I NATI O N
U s i n g Lig htm a p s Lightmapping can be used to create the illusion of bright overhead light sources in a dungeon, or of light spilling out of another room. Emire areas of the maze can be mapped to their maximum intensity, to give the impression that those areas are in full daylight. Animated characters, which should be drawn using the same light intensity values that have been assigned to the ßoor squares (or pixels) upon which they are standing, will appear lighted or shadowed depending on where they are standing. They'll even appear silhouetted if they are standing in a dimly lit area with a bright lightmap directly behind them, a very dramatic effect. Where lightmapping comes into its own, though, is in animation. The most impressive effects of all can be achieved by changing the lightmapping from frame to frame, which is a simple matter of writing new values into the jloorlite[} and ceilingliteO arrays. Lights in the maze can be made to blink on and off, in strobe light fashion, or to oscillate between dim and bright, as though someone were turning a rheostat. Changing light values sequentially from square to square - that is, turning one square off and the next square on in sequence - can give the impression that the light source is moving. If an animared fireball or other illuminated object moves from square to square along with the changing light source, it will appear as though the object is casting the light. With block-aligned lightmaps this effect might be a little rough, with the light source seeming to j ump from maze square to maze square, but a series of light tiles could be designed so that it would seem as though the lightmapping were moving only a few pixels per frame. There are plenty of ways in which an imaginative programmer or game designer can utilize lightmapping to create dramatic visual effects. For some ideas, look at Id Software's Doom and Bethesda Softworks' The Eider Scrolls: Arena, two games that make creative use of what appears to be lightmapping. And then come up with some new ideas of your own.
he ray-casting engines that we've developed over the last three chapters do a great job of rendering still images of the interior of a maze. Bur if still images were all that we wamed to create, we might as weil use a ray tracer such as POVRAY, which would produce even more vivid images and render a wider variety of shapes. The whole point of using a ray-casting engine rather than a ray tracer is to produce images that move! Before our ray-casting engine is capable of creating real time animati o n , tho ugh , we' l l need to o p t i m ize i t s o t h a t it will run a great deal fas ter. Fortunately, I have a few tricks up my sleeve that will speed up this engine to the poim where it will be useful in an animared computer game. By the end of this chapter, we'll have increased the speed of the engine to the point where it will be chugging along at a more than satisfactory pace. Mind you, the optimization tricks discussed in this chapter are not all that exist. A clever programmer should be able to speed up the engine developed i n this chapter even further; don't assume that the tips and tricks memioned in this chapter represent the whole extent of the programmer's art. Rather, the program presented in this chapter should set you on your way to creating an even more optimized program of your own.
GARDENS O F IMAG I NATION
Th e Ta rg et M a c h i n e The precise way in which you optimize your program will depend on the types of computers you expect it to be running on. For instance, considerably greater optimization will be necessary if you want your program to run weil on machines based on the (now ancient) 80286 processor than if you expect it to run only on Pentium machines, since even the slowest Pentium processors are many times faster than the fastest 286s. And, as we'll see below, there are special quirks of the 4 8 6 and Pentium processors that must be taken into account during optimization. When you start to optimize, you should have a target machine in mind. This is the m i nimum computer configuration necessary to run the program at acceptable speed. You'll need to have this machine available for testing the program during the optimization process, so that you'll know when you've reached the point in your optimization where the program runs weil on that machine. When that point is reached, you can stop optimizing, even if more optimization is theoretically possible. What kind of machine should you choose for your target machine? The answer to that question depends on a Iot of variables, not the least of which is the time frame in which you expect the program to be released. The optimal target machine for the spring of 1 99 5 won't be the same as the optimal target machine for the fall of 1 996 - or even the fall of 1 99 5 . Guessing what the optimal machine will be, however, is a tricky business. At the time this book was written, the 80286 processor had long ago ceased to be a viable gaming machine and had been pretty much abandoned by game developers as a target machine. The same process was beginning to happen with the 80386 as weiL By the time you read this, there will probably be no real need to use anything slower than an 80486 as a target machine, and you may weil want to target the Pentium or whatever chip Intel releases as the Pentium's successor. Just be careful not to choose such an advanced target configuration that only a few lucky users will be able to run your game.
A F e w O pti m i zati o n Tri c ks Most optimization tricks can be grouped into a few categories. We'll discuss those categories before we dive into the optimizations themselves.
Tra n s l ati n g i nto Ass e m b ly La n g u a g e Perhaps the oldest optimization trick of all i s to rewrite high-level code - code written in C++, for instance - in assembly language. Because assembly language
CHAPTER TWEl VE
Optimizdtion
allows the programmer to specify the precise machine-level instructions that will m ake up t h e fi n a l p r o g r a m , i t a l l ows c l ever p rogra m m e rs to p e r fo rm optimizations that would not be possible in high-level code. O f course, as compilers have become better at performing optimizations on their own, and as microprocessors have been redesigned in such a way that high-level optimizations are more and more effective, this optimization technique has become increasingly less important. B ut it still can produce effective increases in execution speed when used on time-critical code. In a moment, I 'll discuss ways of identifying which portians of a program are time critical. The Borland C++ compiler, like most state-of-the-art CIC++ compilers, lets us write certain portians of a program in assembly language and others in high-level code, so it isn't necessary to translate an entire program into assembly language. This is for the best, since translating a nontrivial program into assembly language is a major, and extremely time-consuming, task. In this chapter, we'll concentrate on translating three relatively small sections of our ray-casting engine into assembly language. Incidentally, just because l'm translating portians of this ray-casting engine into as sembly language does n't mean that you n ecessarily should include as s e m b l y l a n gu a ge m o d u l e s in y o u r own game e n g i n e s . Many of t h e optimizations discussed i n this chapter don't require assembly language i n order to produce beneficial results. If yo u aren't yet comfortable with assem bler programming, write your first games in straight C++ or whatever high-level language you feel comfortable using. Then, once you've mastered (or at least gatten pretty good at) high-level game programming, consider gerring a good book on 80x86 assembly language, such as Robert Lafore's Assembly Language Primer from The Waite Group, and translating selected portians of your C++ code into assembler as a learning exercise. But don't do this until you feel ready.
u n ro l l i n g the Loop Loops are one of the most important techniques in the programmer's bag of tricks. By instructing the computer to execute a block of code a fixed number of times, or until some event occurs, a lot of power can be packed into a relatively small number of programming lines. And by changing the value of the variables referenced wirhin the loop, the same block of code can perform a different task on each iteration. But there's a drawback to loops, especially when the code of which the loop is part needs to be highly optimized in order to p e r form acceptab ly. T h e instructions that cause the computer t o repeat a block of code take time to execute. If the block of code in the loop is particularly small, these instructions may well take more time than the instructions in the loop itself And if the loop will be executing many times a second, this extra time may be noticeable.
GARDENS O F I MAG I NATION
What can be clone about this? In an extreme case - and we'll be looking at a couple o f these extreme cases i n a moment - we can eliminate the loop altogerher. Since a loop is nothing more than a set of instructions that are executed repeatedly, we can write those instructions over and over again the number of times that they would normally be repeated. For instance, if the loop were to irerate ten times, we could replace the loop with a set of instructions repeated ten times over. This eliminates the "bookkeeping" instructions required by the loop, such as comparing values of variables against the loop's Iimit values. All that will be left is the code that does the real work, such as placing pixels on the display. Perhaps your first response is that such an unrolled loop would take up a lot of space in the program. But that's not necessarily so. If we write the loop code in assembly language, the size of the code wirhin the loop can be made quite small and the resulting unrolled loop will often be no more than a few kilobytes in length - small in comparison to the hundreds of kilobytes that the completed program and its data will take up. You might also think that an unrolled loop would take a lot of typing to create, but the use of macro instructions (not to mention the cut-and-paste capabilities of the Borland editor) can reduce the typing considerably. More about that in a moment. There's one more thing that you should know before you unroll a few loops of your own. On the 80486 microprocessor, loop unrolling might not produce the speed gains that would normally be expected. That's because the 486 has a 4kilobyte internal memory cache, where instructions are sto red after being executed so that they can be executed more quickly the second time around. This allows short loops - that is, those under 4 kilobytes in length - to execute quite rapidly on the 4 8 6 . That's not to say that further speed-ups can't be achieved by unrolling a loop or two. Bur if unrolling the loop makes it Ionger than 4 kilobytes, it will no Ionger be able to fit inro the cache; and the extra time that the 486 spends loading the instructions back into the cache on each iteration of the loop will more than cancel out the gains. Fortunately, it's not necessary to unroll a loop completely in order to optimize it. Unrolling it perhaps eight times, then placing those eight iterations in a loop, will give you almost 90 percent of the speed-up of a complete unrolling. For instance, a loop that executes 80 times can be unrolled eight times, then placed in a loop that execures ten times. We'll look at such a partially unrolled loop in a moment.
Ta b l e Look- U ps Mathematical calculations, of which our ray-casting code has more than a few, can be quite time consuming to perform. One way to speed up the performance
CHAPTE:R TWEl VE
Optimizdtion
of j ust about any piece of computation-intensive code is to avoid performing those computations while the code is running. lnstead, perform them in advance - and place the results in an array of values from which they can be retrieved later, when they're needed. This is especially useful in speeding up trigonometric calculations, such as sines and cosines. Most programs that feature three-dimensional graphic effects only need to take the sines and cosines of a relatively limited number of angles. For instance, if a program only allows rays or objects (the viewer included) to be rotated in 1 -degree increments, then only 360 different sines and 360 different cosines will be needed. These can be calculated in advance and stored in 360element arrays. Later, when the sine or cosine of a given angle is required, the angle can be used as an index into the appropriate array and the sine or cosine extracted with minimum strain on the CPU. These tables can be calculated either during program initialization (if the task isn't too time consuming) or at compile time. (lf calculated at compile time, the tables can either be placed in program modules and compiled along with the rest of the program, or placed in binary data files, from which they can be loaded during program initialization.) We'll take a closer look at the precalculation of trigonometric tables in a moment. First, let's take a look at the type of arithmetic that we'll be using in these tables.
Fixed Point Arit h m etic Umil now, whenever we've needed t o solve some heavy mathematical problems i n the program we've been developing, we've turned to ßoating point math to do the job. A CIC++ compiler uses ßoating point math when performing standard arithmetic operations, such as addition, subtraction, multiplication, or division, on pairs of numbers in which at least one number is declared as a float, a double, or a long double. Floating point is also used by library functions such as sqrtO, sinQ, and cosO. Although ßoating point is easy to use (because it's supported automatically by most compilers), it isn't always the fastest way to get the desired mathematical results . When isn't ßoating point the fastest way to get results? That's a complicated question. On any machine that doesn't have a built-in ßoating point unit, or JPu, ßoating point arithmetic can be slower than Washington, D.C. traffic at rush hour. However, the maj o rity of recent computers - including those with 80486DX and Pentium processors - come with ßoating point units as Standard equipment. And even those machines that are not invariably equipped with fpu's, such as those based on the 80286, 80386, and 80486SX processors, often have an optional unit installed in a special socket on the motherboard.
GARDE:NS OF I MAG I NATION
Even those machines with fpu's installed can become somewhat sluggish when too much floating point math is used - with one exception: machines based on Pentium processors. The Pentium has a built-in fpu that reportedly works at lightning speed. On a Pentium, floating point may weil be the best way to go when complex mathematical operations are required in a program. At the time this book was written, Pentium processors still represented a minor portion of the PC market. Before long, though, the Pentium may be the domimant processor on the computing scene. Until that happens, it's best for programmers of high-speed animated games to avoid floating point math wherever possible. What's the alternative to floating point math? Fixed point mach, of course. Fixed point mathematics is a surprisingly simple method for performing complex mathematical operations - in particular, those that involve fractions - using the i nteger registers of an I BM-compatible CPU. The 32-bit registers and instructions set of the 386 and 486 microprocessors are especially amenable to fixed point operations. 1'11 have more to say about fixed point arithmetic in a moment.
W h e re to O pti m i z e ? Just as important as knowing how to optimize program code is knowing where to optimize the code. There's no point in translating an entire game program into assembly language - a task that may take Ionger than writing the high-level version of the code in the first place. (Assembly language is also more difficult to debug and maintain than high-level code, so you'll want to use it only in the most crucial places.) Not every loop needs to be unrolled; in fact, only a few need to be. There are even places in the most optimized of game programs where it's perfectly acceptable to use floating point math, even if the resulting program is targeted for a slow processor. But any program that makes use of high-speed animation has a few time critical j unctures that would profit from highly optimized code. The question is, how do you know where those time-critical j unctures are? The answer is: Look for the innermost loops in the animation code. I f you're at all fluent in writing computer code, you're familiar with ehe concept of nested loops. Almost every nontrivial computer program contains a few of ehern: loops that are located inside other loops and that are executed several times for every time that the outer loop is executed. Most programs even contain loops that are nested inside loops that are nested inside loops.
CHAPTE:R TWElVE
Optimizdtion
As a rule of thumb, we can say that the more deeply nested a loop is inside other loops, the more i mpo rtant it is for that loop to be optimized. Why? Because inner loops are executed more times than outer loops. If a for() loop designed to execute 1 00 tim es is located inside another for() loop that also executes 1 00 times, which is in turn located inside yet another for() loop that executes 1 00 tim es, the innermost for() loop will execute 1 million times every time the outer loop executes 1 00 times. That's a Iot of iterations! Optimizations in the i n nermost loop w i l l p ay you back many t i m es over compared t o optimizations in the outer loop. In fact, the outermost loop may not need t o be optimized at all, unless it contains some unusually sl uggish code. How do you locate the innermost loops in your program? In many cases, they will be obvious. In our ray-casting engine, for instance, the innermost loops are rhe ones that draw the pixels in walls, the pixels in the floor, and the pixels in the ceiling. Simply by optimizing these loops, we can speed up the execution of the engine a thousandfold. Further optimizations will produce even more beneficial results, but optim izing these three i nner loops will produce code that is fast enough for many animation purposes. The profi/er is a helpful rool in determining where a program most needs to be optimized. Profilers are programs that analyze computer code in order t o determine which subprograms a n d even individual l ines of code are being executed most often. By fol l owing the suggestions of a good profiler and optimizing only those pieces of code that the CPU spends most of its time executing, you can save a Iot of time that might otherwise be spent optimizing non-time-critical portions of a program. A good profiler will help you place your programming resources where they really count. The Borland C++ compiler comes with a profiler known as Turbo Profiler, or TPROF for short. Before you purchase a more powerful profiler, give this one a whirl. My own experience is that the Borland profiler is a powerful rool, but one that sometimes falls short when asked to profile very !arge programs or code that uses floating point arithmetic. (TPROF has some problems determining how much of a program's time is spent executing floating point instructions, which is unfortunate when you consider that floating point can be one of the major drags on program execution speed.)
M o re About F ixed P o i nt M ath Now that we know where to optimize a program, let's talk a little more about how to optimize it. In particular, let's Iook into some of the details of fixed point math.
GARDENS O F I MAG I NATION
To understand how fixed point math works, think about how numbers with fractional values are written using the decimal numbering system. The number that represents one plus half of one, for instance, is written as 1 . 5. The number to the left of the decimal represents the i ntegral (that is, whole number) portion of the n umber, wh ile the number to the right of the decimal represents the fractional portion of the number. Here are a few more decimal fractions: 1 5 2.234 -7.3333 890234.9801 7203322 In each case, the numbers to the left of the decimal point represent integral amounts and the numbers to the right are fractional amounts. We can write binary numbers that have fractional values in the same way: 1 1 000 1 1 1 0 1 . 1 1 1 1 000 1 00 1 1 .0 1 - 1 1 1 0 . 000000 1 The rules for bi nary fractions are the same as for decimal fractions. The digits to the left of the decimal point represent integral values and the digits to the right are the fractions. (Strictly speaking, binary numbers don't have "decimal points" at all, since they aren't decimal numbers. However, it's easier to use the familiar term.) An i nteresting properry of numbers with decimal points is that when we perform certain mathematical operations on them - in particular, multiplication and division - the digits in the result are the same no matter where the decimal points in the original numbers were located. Only the position of the decimal changes. For instance, suppose we want to multiply the two decimal numbers 1 . 3 and 4 1 .7 : 1 .3
X
4 1 .7
=
54.2 1
The result is 54.2 1 . But suppose we move the decimal point in the first number one position to the right and multiply instead the numbers 1 3 and 4 1 .7: 13
X
4 1 . 7 = 542. 1
lnterestingly, the only difference in the result is that the decimal point also moves one position to the right. What will happen if we also move the decimal point in the second number one position to the right and multiply the decimal numbers 1 3 and 4 1 7? This is what will happen: 1 3 x 4 1 7 = 542 1 Now the decimal point has moved two spaces to the right.
C HAPTE:R TWEl VE
Optimizdtion
The reason for this becomes apparent if you recall what you were taught in elementary school about "long multiplication. " In a multiplication such as X
4 1 .7 1 .3 54. 2 1
the number of decimal positions to the right of the decimal point i n the result is the s u m of the decimal positions to the right of the decimal point in the multiplier and multiplicand (the two numbers being multiplied). This rule also a p p l i e s to b i n a r y m u l t i p l i c a t i o n - a n d i t 's the s e c r e t of fixed p o i n t multiplication. l t doesn't matter where we place the decimal point i n a binary number as long as (a) we remember where we put it; and (b) we understand where it will be after we perform a mathematical operation on that number. For instance, if we multiply a binary number that has two digits after the decimal point by a binary number that has three digits after the decimal point, the result will have five digits after the decimal point - but the digits themselves will be exacdy the same as if there had been no decimal point at all. This means that we can perform fixed point math with the same integer math instructions that we use on numbers without decimal points, as long as we keep track of the position of the decimal point after each operation and treat the numbe rs accordingly. Addition and subtraction are a great deal easier to perform on fixed point numbers, because they don't move the decimal point at all . In fact, the Standard CIC++ and assembly language addition and subtraction instructions can be used with fixed point numbers. However, you must be sure that the decimal points are in the same positions in the numbers to be added or subtracted, even if it's necessary to shift the decimal point into the proper position before adding or subtracting. We'll see how to perform such a shift in a moment. In the code that we develop in this chapter, we'll assume that all fixed point numbers are 32-bit long values with a decimal point right in the center, so that each fixed point number will have 1 6 digits to the left of the decimal point and 16 digits to the right of the decimal point. We'll refer to such numbers in this chapter as 1 6: 1 6 numbers, where the colon represents the decimal point and the numbers to both sides of the colon represent the number of digits on those sides. There's no rule that says there have to be that many digits on both sides of the decimal point. You can place the decimal point anywhere that you find useful, as long as you remember where it is. For instance, if you are dealing stricdy with numbers in the range 0 to 1 , you might want to use 1 :3 1 fixed point numbers, with only one digit to the left of the decimal point and the remaining 3 1 digits to
GARDE:NS O F I MAG I NAT I O N
the right. This will give you much greater fractional precision. For our purposes, however, 1 6 binary digits of fractional precision will be enough.
F e l l ow That oec i m a l ! Now that you know how to construct a fixed point number, the next thing you need to know is how to perform arithmetic on that number. Actually, you can use o rdinary addition, s ubtraction, m ul tiplication, and division ope rations between fixed point numbers (and between fixed point and regular numbers) . But you m ust always be aware where the decimal point is after performing those operations and be prepared to move the decimal point elsewhere if necessary. Following the decimal point in a fixed point number is a matter of learning a couple of rules. As you've seen, when multiplying two numbers, fixed point or otherwise, the number of digits after the decimal point in the result will be the sum of the number of digits after the decimal point in the two numbers being multiplied. When dividing two numbers, the number of digit positions after the decimal point in the result will be the number of digit positions in the dividend (the number being divided into) minus the number of digit positions in the divisor (the number being divided into it) . Since we'll be using 1 6: 1 6 fixed point numbers in this book, each of which w i l l h ave 1 6 digit positions after the decimal point, we know that after multiplying two of these numbers there will be 32 digits after the decimal point. But wait a rninute! The largest integer type affered in C++ (or by the 80x86 C P U) is o nly 32 b i t s l a n g . T h a t m e a n s that the res u l t of fixed p o i n t multiplication will leave u s with only the digits to the right o f the decimal point. The 1 6 digits to the left of the decimal point will simply vanish. Fortunately, if we write our fixed point multiplication code in 80386/486 assembly language, there's a trick that we can use to catch those digits and put them back where they belang befo re they're l o s t fo rever. ( O f course, taking advant age of these instructions means that our fixed point routines will not work on 80286 or earlier processors. But few garnes developed after 1 983 will run on the 286, so it's less than imperative for us to support it. If you want your garne to run on a 286, you'll either have to avoid fixed point or write your own fixed point routines using 286, or C++, instructions.) A similar problern occurs during fixed point division. The 1 6 digits to the right o f the decimal point will be shifted inro obl ivion during a division operation b etween rwo 1 6: 1 6 fixed point numbers, unless we take steps ro preserve those digits. As you might guess, assembly language also provides us with a trick we can use ro do this. Both of these tricks are made possible by left- and right-shift instructions.
CHAPTE:R TWEl VE
Optimizdtion
Sh ifting Left a n d R i g ht A shift instruction shifts the decimal point - and all of the digits - i n a number to the right or left by a given number of digit positions. There are two types of shift instructions: left shifts and right shifts. These operations shift the digits and decimal to the left and right, respectively. (The directions left and right as used here are relative to the Standard orientation of digits in a number, with the most significant digits - that is, those digits that r.epresent the largest values - to the left and the least significant digits to the right. Microcomputer shift operations only work on numbers written in the binary notation system, because that's the way in which the computer treats numbers on the CPU Ievel. For instance, if we shifted all digits in the binary number 00 1 00 two digits to the left, it would become the number 1 0000. Shifting those digits two positions to the right would produce the result 0000 1 (or j ust 1 ) . See Figure 1 2- 1 for an illustration. The CIC++ right-shift operation is represented by the symbols "»" and the left-shift operation is represented by the symbols " < < " . The number to be shifted should be placed to the left of these operators and the number of digit positions by which the number is to be shifted is represented by the number to the right of these operators. To shift the digits in the number represented by the variable a two digits to the right, you would write: a >> 2
Similarly, to shift the digits in a two digits to the left, you would write:
Right Shift
Figure 1 2-1
..
Left-sh ift and right-shift
operations on binary numbers
GARDENS OF I MAG I NATI ON a =2 )
i n t e n s i ty=a t o f ( a r gv [ 1 J ) ;
l i ne i f present :
if
( a r g c >=3 )
a m b i e n t_l eve l =a t o f ( a r g v [ 2 J ) ;
II
L o a d t ex t u r e map i ma g e s :
CHAPTER TWELVE if
( L o a d P C X ( " wa l l s . p c x " , & t ex t maps ) )
I I Load ba c k g round s c reen if
exi t ( 1 ) ;
i ma g e :
< L o a d P C X < ' ' d emobg8 . p c x " , & bg ) )
I I C reate texture
L i s t a r ray :
for
i =O ;
( u n s i gned i n t
O ptimizdtion
exi t ( 1 ) ;
i ; t i me r 1 . t i m e r 0n ( ) ; II
Put
fi rst
f r ame
i nt o buf f e r :
d r a w_ma z e ( w a l l s , f l o r , c e i l i n g , s c r e e n_bu f f e r , xv i e w , y v i e w , v i e w i n g_a n g l e , v i e w e r_h e i g h t , am b i e n t_ L e v e l , t e x t m a p s . i ma g e , L i t e t a b l e , L i t e l e ve l ) ;
C HAPTE:R TWEl VE:
Opti mi zdtion
II Move s c r e e n b u f f e r t o s c r e e n : p u t w i ndow ( V I EWPORT_L E F T , VI EW P O R T_TO P , V I EWPO RT_W I DT H , V I EW PORT_H E I GH T , s c r e e n_bu f f e r ) ; I I Move v i ewer a c c o r d i ng t o i n p u t even t s : g e t event ( W H I CH_E V E N T S , &even t s ) ; I I D i d a go f o r w a rd event o c c u r ? if
( ev e n t s . g o_f o rwa r d ) II
If
so,
{
a s s ume v i e w e r i s s t a n di ng a t
o r i g i n of m a p . . .
L ong new_x=O; II
. . . fa c i n g no r t h .
II
t i m er t o s e e what
I I s i nce
Last
C a l c u l a t e de s t i na t i o n by c h e c k i ng f r a c t i o n of a se cond has pa s s e d
f r a me o f a n i ma t i o n :
l ong new_y= ( L on g ) ( D I STAN C E_P E R_S E C O N D I 1 000000 . 0 * e l a p sed_t i me ) > 1 6 ; yvi ew+= ( f i xmu l ( ne w_y , C O S ( v i e w i ng_a ng l e ) ) + f i xmu l ( new_x , S I N ( v i e w i ng_a ng l e ) ) ) >> 1 6 ; } II
D i d a go ba c k e v e n t o c c u r ?
if
( eve n t s . go_ba c k )
{
I I Repea t p ro c edu r e f o r f o rwa rd event . . . L ong new_x=O; II
. . . but ma ke
I I of
the d i s t a n c e nega t i ve ,
to the
rea r
t h e v i ewe r :
L ong new_y=- ( l ong ) ( D I STAN C E_P E R_S E C O N D I 1 000000 . 0 *e l a psed_t i me ) > 1 6 ; y v i ew+= ( f i xmu l ( new_y , C OS ( v i e w i ng_a ng l e ) ) +f i xmu l ( ne w_x , S I N ( v i ew i ng_a ng l e ) ) ) >> 1 6 ; } II
D i d a go r i g h t
if
( e ve n t s . go_r i g h t )
event o c c u r ? {
II
I f so,
II
L a s t f r ame o f a n i m a t i on :
ca l c u l a t e n e w a n g l e b a s e d on e l a psed t i me s i n c e
continued on next page
G A R D E N S O F I MAG I NAT I O N continued.from previous page
v i e w i nR_a ng l e+= ( ( f l oa t ) ROTA T I ON_P E R_S E C O N D I 1 000000 . 0 * e l a p s ed_t i me ) ; I I W ra p a round if
to 0 i f a n g l e e x c e e d s ma x i mum d e g r e e s :
( v i ew i nR_a n g l e> N U M B E R_O F_D E G R E E S - 1 ) vi e w i n R_a n g l e-=NUMB E R_O F_D E G R E E S ;
} II
D i d a go
if
( ev e n t s . go_L e f t ) II
I f so,
II
L a st
Left event occur? {
c a l c u l a t e new a n g l e b a s e d on e l a p s ed t i me s i n c e
f ra m e o f a n i ma t i on :
v i e w i nR_a n g l e-= ( ( f l oa t ) R O T A T I ON_P E R_S E C O N D I 1 000000 . 0 * e l a p s e d_t i me ) ; I I W r ap a round t o m a x i mum d e g r e e i f a n g l e if
< v i ew i n R_a n g l e ;
i ;
for
( un s i g n e d i n t j =O ;
for
( un s i g ned
i nt
j 1 6 ; i nt
a l i g ny= ( x * S I N ( -v i e w i n g_a n g l e )
+ y * C O S ( -vi e w i n g_a n g l e ) ) >> 1 6 ;
You'Il recall from chapter 7 that these equations rotate a n object around an origin position (that is, the 0,0 point relative to which the object coordinates are defined) . All of the objects in our maze are defined by a two-dimensional coordinate position, so this is all we need do to adjust their coordinates relative to the current viewing position. (If we were dealing with three-dimensional, rather than two-and-a-half-dimensional, graphics, we would also need to rotate the coordinates around the y and z axes of our coordinate system.) The variables alignx and aligny teil us where the object is, or would be, if the viewer were rotated to angle 0 - directly facing north along the y axis - within the coordinate system. Since this operation realigns the objects as though we were facing northward along the positive y axis, we can teil if the object is behind us or in front of us by checking to see if its y coordinate is positive or negative (see Figure 1 3-3). If it's positive, the object is in front of us. If it's negative, the object is behind us. Thus we wil l only p roceed with our drawing code if the obj ect's y coordinate is positive, that is, if the object is in front of us: if
( a l i g ny>=D >
{
Of course, this problern should have been solved by the earlier if Statement that only accepted squares marked as visible, so this code is a bit redundant. lt's
CHAPTE:R THIRTE:E:N Putting lt All Tagether +
Une of sight
+
X-
y Figure 1 3-3
When facing northward a long
the positive v axis. the value of the v coordinate teils us if an object is in front of us (positive coordinatel or behind us (n egative coordinatel
probably possible to remove the visibility marking code without penalty, though this is left as an excercise for the reader. We'll use the familiar Pythagorean method to determine how far away each object is: d i s t a nc e=sq r t ( ( L o ng ) a l i gnx*a l i gn x+ ( L o n g ) a l i gny*a l i g ny ) ;
This assigns the distance of the object, in maze pixels, to the integer variable distance that we defined earlier in the draw_maze() function. We can now project the object's position onto the video display using a standard perspective equation. lnterestingly, it's only necessary to project the object's x coordinate onto the display, since this is the only position that's actually variable within our two-and-a-half-dimensional coordinate system: i n t p r o j x=a l i gnx*V I EW E R_D I S T A N C E / d i s t a n c e +H O R I Z_C E N T E R ;
Now we need to determine the horizontal and vertical size of the object as it will be displayed. We can do this with the Standard perspective equations too: obj [ o b j numJ . wdraw =obj [obj numJ . wb i t * V I E W E R_D I STAN C E / d i stance; obj [o b j n umJ . hd raw=o bj [ o b j n um J . h b i t * V I E W E R_D I STAN C E / d i stance;
This code sets the wdraw and hdraw fields of the object structure, which represent the width and height of the object on the display, to the appropriate perspective-adjusted values.
GARDENS O F I MAG I NATI O N
We also need to determine the x and y coordinates on the display at which the upper-left corner of the object's image should be drawn. A moment ago, we calculated the x and y coordinates at which the object will be displayed, in the variables alignx and aligny. But these are the coordinates of the center of the object (see Figure 1 3-4) . To determine the coordinates of the upper-left corner of the object, we must subtract half of its width and height from this value and center it vertically araund the VERT_CENTER of the display (as defined in the GARDENS.H header fil e) : ob j [ ob j num J . s c r e e n x = - p r o j x-ob j [ o b j numJ . wd ra w / 2 ; ob j [ o b j n u m J . s c r e e ny=VE R T_C E N T E R-ob j [ o b j num J . h d r a w / 2 ;
Now we'll add the number of the object to the list of objects to be drawn, which we'll maintain in an array called objlist: o b j l i s t [ numob j s + + J = ob j num;
The numobjs variable was established back before we began Iooping through the objects, like this: i n t numob j s =O ;
By incrementing the value of numobjs every time an object number is added to objlist, we know how many objects are currendy visible.
(e�ter _
_
_
Fig u re 1 3-4
_
_
_
_
_
_
_
_
_
_
_
_
",
The x,y coord i nates mark the
center of the object. Before we draw it. we must calcu late the coordinates of its u p per· left corner on the display
CHAPTE:R THIRTEEN Putling lt All Tagether
The Pa i nter's Al g o rith m We're clone compiling rhe objects list. (I'll spare you all of the closed curly brackets that are necessary to terminate the loops that we starred in this process .) Now we can starr a new loop that acrually draws rhe objects. First, however, we need to perform some more hidden surface removal. Earlier, we developed a method fo r detecting whether a given column of an object's bitmap is behind or in front of a wall, for any given column of the viewport. However, we also need to know if one object is in front of another object. To determine rhis, we'll use a technique called the Painter's Algorithm. The Painter's Algorithm gets its name from rhe idea rhat pain ters, when rendering a landscape, paint background objects first, rhen paint fo reground objects over the background objects. Translared into computer rendering terms, rhe Painter's Algorithm says rhat we should draw objects in the background before we draw objects in rhe foreground, so rhat objects in the foreground will be drawn on top of background objects. This requires rhat we perform an operation known as a depth sort. A depth sort takes all of the objects that are visible (or potentially visible) on rhe display and sorts them inro a list with the objects most distant from the viewer at the beginning of the list and the objects closest to the viewer at the end of rhe list. If we then draw the objects in this order, it is guaranteed that foreground objects will be drawn on top of background objects. In a polygon graphics system of rhe type rhat is used in most flight simulators, rhis depth sort may sort objects into the wrang order. This is because polygons can be rotared at odd angles ro rhe viewer, allowing backgro und objects to obscure foreground objects. The objects that we'll be drawing in this chapter, however, will not be arienred at such odd angles. Rather, each object will face squarely toward the viewer, with all pixels in the object at the same distance from the display (see Figure 1 3-5) . Thus a simple depth sort is all we need in order to determine a correct drawing order for these objects. We'll use rhe simplest form of sorr, the bubble sort, to arrange the objects in rhe proper front-ra-back order: if
( n umob j s )
{
i n t swap=- 1 ; whi le
( swap)
{
swap=O; for if
( i nt
i =O ;
i < ( numob j s- 1 ) ;
i n t t emp=o b j l i s t [ i J ; obj l i s t [ i J=obj l i s t [ i +1 J; o b j l i s t [ i + 1 J = t emp; s w a p=- 1 ; }
i ++ )
{
( ob j [ o b j l i s t [ i ] J . d i s t a n c e> 1 6 ;
To advance the variable column t o its next position, we'll need to add the value of jix_wincrement to it: c o l u mn+= f i x_w i nc r emen t ;
Now, before we begin drawing the object, we need to create two new variables, which we'll call line and lastline: u n s i gned
L o ng
u n s i gned
int
L i ne=O; L a s t l i ne=O;
Now we can set the unsigned integer variable column to point to the upper-left corner of the object, by adding x and leftrow: u n s i gned
i n t c o l umn=x+ L ef t r o w ;
We'll draw the object by iterating from the left clipping parameter to the right clipping parameter: if
( ( ( c o l um n ) > L c l i p x )
&&
( ( co l umn ) < r c l i p x ) )
{
CHAPTER T H I RTE:E:N Putting lt All Tagether
Here the hidden surface removal comes into play. We should not draw the current column at all if there's a wall closer to the viewer than the object: i f ( d i s t a n ce>1 6 ) ) { bpt r+=ob j ->wb i t ; L a s t l i n e = L i ne>> 1 6 ; } spt r+=320; } }
The most important line here is the if() Statement if (b) screen[sptr=b; . This line checks to see if a pixel is 0 before printing it to the display. Why? Because 0 pixels in an object image represent transparency. Zero pixels should not be displayed; rather, the backgro und p ixels behind these pixels should show through. If not for these transparent p ixels, all objects would be perfecdy rectangular, which is obviously undesirable. l nstead, we allow 0 pixels to be transparent. When we detect such a pixel, we don't draw over the background, allowing the background to show through. And that, aside from the closing brackets, is all that there is to drawing objects.
Ad d i n g Ga m e Featu res Learning t o program isn't all that there i s t o creating a game. You also have to give some thought - preferably a great deal of thought - to game design. Alas, game design is at least as complex a subject as game programming, and really deserves a book of its own. We can, however, offer a few suggestions to help you get started. Start by looking at games that you've enj oyed playing. Try to identify the features that make those games attractive: graphics, atmosphere, story line, and so on. Decide what genre of game you'd like to create. For example, an adventure game emphasizes exploration and p uzzle-solving. In such a game, the player needs to acquire the objects necessary to solve a puzzle, and perhaps select actions
GARDENS O F I MAG I NATION
to be performed on those objects - lift the rock, push the button, open the drawer, and so on. In a role-playing game, on the other hand, the player takes the part of a character who grows in experience and power while overcoming monsters and fulfilling quests. Many successful games (such as the Ultima series) combine aspects of both adventure and role-playing. Obviously, any game designed araund the algorithms in this book will take place in some kind of maze. Once you've decided what kind of game you want to create, think about the kinds of objects to be found in that maze. In addition to creating pictures for the objects, you'll need to expand the obj type structure to account for the important characteristics of objects in your game. For example, you might add structure members like these: c h a r * narne
/ / narne o f ob j e c t
i n t owne r ;
//
0
=
unowned,
1
=
owned by p l a yer
II 2-99 owned by rnons t e r # i nt wei g h t
//
i n t t y pe
how rn u c h o b j e c t w e i g h t s
II
( p l a y e r c a n on l y ca r r y h i s / h e r
II
s t r eng t h
//
1
II
4-99
in obj e c t s )
= w e a pon 2 = t re a s u r e 3 =
spe c i a l
=
food
ob j e c t
You could then use auxiliary structures for information specific t o partiewar types of objects. For example, a weapon would do a certain amount of damage, food might restore a certain amount of the player's strength, and so on. You will also need a structure to hold info rmation about the player as represented by the "view p o i n t character" in the gam e . S a m e traditional characteristics used in role-playing games include: i nt
str
II
h o w s t rong :
a f f e c t s darna g e d o n e i n a t t a c k s ,
I I a b i l i ty t o c a r ry o b j e c t s I I f o r c e d oo r s o p e n , a n d so o n i nt dex
II
how
fast/agi le
the p l a yer
i s;
I I a b i l i ty to dodge a t t a c k s or i nt
con
II
con s t i t u t i o n ,
i nt
hp
II
h i t po i n t s
t raps
o r a b i l i t y t o a b s o r b darnage
( a rnount o f darnage p l a y e r can t a k e
I I b e f o r e dyi ng ) i nt
exp
II
e x p e r i ence
II
i n c reases a b i l i t i e s of c h a r a c t e r )
po i n t s
( g i v en f o r a c cornp l i s hrne n t s ,
Yo u w i l l probably need a structure t o h o l d similar i n formation about monsters, though it probably won't be as detailed as the structure for the player. Once you've built this "infrastructure" of structures and arrays, you'll need a main "command processor" function that interprets the player's mause clicks and sends control to the appropriate function. Typical player actions might include:
�
Move Ieiiek on directlon arrows or use keyboardl
CHAPTER T H I RTEEN Putting lt All Tagether
Turn araund Exa m l n e a n object Pick up an object Attack a m o nster Review Information about the player character save the game
and so on. lt's up to you whether to use a menu interface or an icon interface, but the latter is more popular in contemporary games. Of course, we've already provided you with mouse, keyboard, and joystick in pur routines. Systems for manipulating objects (and particularly combat and magic systems) can become very complex. We recommend you start with simple functions and add features only after the game has been tested and is working well. Appendix A lists some further readings on game programming and design that you may find helpful.
B e i l s a n d Wh ist l e s Before l'm clone, however, l'd like to add a few additional bells and whisdes to our "game." Specifically, I'd like to add a set of opening credits and an auromap. First, let's add the opening credits.
The Op en i n g c red its The opening credits will be a series of PCX files displayed one after another. The code for displaying each PCX file will be identical to the code for displaying the rest. Here's the code for displaying the first credit: f a depa L o u t ( 0 , 2 5 6 ) ; I I Load Wa i t e G roup if
Logo :
( L o a d P C X ( " wa i t e . p c x " , & L o go ) ) e x i t ( 1 ) ;
I I Copy for
L o g o i n t o v i deo memo r y :
( i = O;
i
l o c a t i on
operator, 1 1 5, 47 1
birmapped maze, builcling, 1 29- 1 57
1 6 : 1 6 numbers, 469
bitmapped mazes, 1 2- 1 3
360-degree system, 248
BITMAZE program, 1 49- 1 5 7
A
blit( ) function, 1 5 1 - 1 52 , 1 58
BITSCALE.CPP, 202-206
absolute angle, 309 absorbed light, 262 addresses, 1 8- 1 9 ambient light, 269, 4 1 9, 5 1 7 ANDing, 3 0 1 , 304-305, 328 animation, 14, 240, 325, 46 1 , 466 animation engine, 9- 1 0 animation, and heighrmapping, 397-398 animation, and lighrmapping, 458 animation, maze, 97- 1 03 ARG clirective, 22, 8 1 argument parameters, 1 1 7- 1 1 8, 322 assembly language, 1 7 , 22, 563-570 assembly language translation, 46 1 -462, 5 1 9 automap, 546-548 auromap( ) function, 547
B background color, 269 background images, 2 1 8
block-aligned heightmaps, 360-37 1 block-aligned lightmaps, 433 BLOKCAST.CPP, 363, 37 1 -377 BLOKDEMO.CPP, 378-38 1 Borland C++, 1 7 Borland Graphics Interface (BGI), 1 7 , 32 botdiff, 17 6 boterror, 1 77 botrom [ ) array, 186 bounds checking, 346, 364 break code, 70-72 break instruction, 53 Bresenham's Aigorithm, 40, 43-46, 1 73, 1 77, 299 brighmess, 40 1 - 404, 406- 407, 4 1 9 bubble sort, 535-536
c calculations, math, 464-465 Cartesian coordinate system, 32-33, 24 1 , 250, 299
GARDENS OF I MAG I NATION Canesian plane, 232-234
direction vecrors, 241 -247, 250-252
Cartesian units (units of rneasure) , 241
discrirninant, 278, 283
ceil() function, 285
display_slice( ) function, 146
ceiling[ ] array, 338-339, 344, 347
distance[ ) array, 527-528
ceilingcasting, 338, 343-344
dithering, 285
ceilinglites[ ] array, 433-434, 458
divide-by-0 error, 273, 284, 304, 3 1 2 , 405
ceilings, optirnized, 489-497
division (/) operator, 29
circular system, 248
Doorn, 9- 1 0, 359-360, 396-397
clipping wall, 369 CLK_TCK rnacro, 98
draw_rnaze( ) function. See also drawrnaze( ) function
clock() function, 98
block-aligned heightrnapped, 371 -377
clrscr() function, 83
floorcasting, 344-346
CLS (clear screen) procedure, 3 1 -32
floorcasting, wallcasting, 348-354
CMP instruction, 7 1
lightrnapped, 434-440
color (palette), 27-30, 268-269
lightsourced, 423-430
color brighmess, 407
optirnized, 500-508
color and light, 262-264
texture-rnapped ray-cast, 332-336
color rnap, 1 1 1 - 1 1 2, 1 1 4- 1 1 5
tiled heighunapped, 389-392
column angle, 308
tiled lightmapping, 446-453
column clipping, 328-330
wallcasting, 306-32 1
COLUMNLOOP rnacro, 487, 489
draw_slice( ) function, 1 49
cornments (; and II), 24, 26, 564
drawbg( ) function, 2 1 8
cornpass, 248
drawbox( ) function, 50, 1 84
cornpass PCX suucture, 1 5 0
drawceilrow( ) function, 49 1 , 499
cornpass_face array, 1 50, 1 53
drawfloorrow( ) function, 49 1 , 498
cornponents, vector, 247 cornpressing data, 1 09, 1 2 1 , 1 59
drawrnaze( ) function, 99. See also draw_rnaze( ) function
bitrnapped rnaze, 140- 1 49
cornputer role-playing garnes (CRPG), 4 continuous (srnooth) rnotion, 7, 325
polygon rnaze, 1 8 5-1 90
coordinate pair, 32
wirefrarne 3D maze, 48-58
coordinate systerns
drawobject( ) function, 537-539
Cartesian, 32-33, 24 1 , 250, 299
drawplayer( ) function, 547
fine, 300-30 1 , 3 1 1
drawwall( ) function, 484-488
polar, 247-258
Dungeon Master, 6, 76
cosine adjustment, 3 1 5-3 1 6 , 342 cosine/sine, 254-256, 476-478 CPU flags, 567 CPU registers, 564-567 credits, opening, 54 1 -546
DW (Define Word) directive, 67
E EMPTY.TGA file, 2 1 8 endpoints, 36-37
D
eraseplayer( ) function, 548
decirnal nurnbering, 468, 470
error rnessage, 4 1 4
delete function, 1 53
error terrn, 43, 1 78- 1 79, 330-332
error code, 8 1 -82, 1 1 3 , 1 1 7- 1 1 8, 1 24
depth sort, 535-536
error terrns, 173, 1 77, 1 99-20 1 , 2 1 0-2 1 2
dereferencing, 20
escape clause, 74
I ndex evenr manager, 9 1 -97
gameport byte, 85-86
evenr srrucrure, 92-93, 98
Gamers Forum, xiii, xviii
evenrs, maze, 92-93
GARDENS .CPP, 549
exir( ) function, 1 1 8
Ger Inrerrupt Vector, 67
expanded memory (EMS), 1 59- 1 60
getevenr( ) function, 95-97
Eye of the Beholder, 7, 1 07
F fabs( ) function, 4 1 3 field o fview, 235 file formars, 109- 1 1 0 fine coordinate system, 300-30 1 , 3 1 1 fish-eye effect, 3 1 5 , 342 fixdiv( ) function, 475-476 fixed poinr math, 1 74 , 3 1 2 , 465-476, 5 1 9 fixed poinr rrigonometry, 476-48 1
GIF format, 1 09 , 1 1 0- 1 1 1 GO GAMERS, xiii, xviii Gold Box games, 5 GOMAZE program, 1 0 1 - 1 03 gotoxy( ) function, 83 grab( ) function, 1 50- 1 5 1 , 1 5 8, 1 97 GRAPHDEV forurn, 554 graphics utiliries, 553 gray-scale palerte, 268 grid lines, 1 44- 1 4 5 , 302
fixmul( ) funcrion, 473-474
H
flicker, 1 00- 1 0 1
hardware, 462-466
flight simulators, 7-8, 23 1 , 234, 239-240
header files, 26
floating poinr math, 1 73- 1 74
height changes, floor, 365-371
floating poinr unit (fjm), 465-466
heighr tiles, 383-384
floating poinr variables, 31 1
height variable, 1 77
floor( ) function, 285, 339, 342
height of viewer, 364-365
floorbase[ ] array, 384-385
heighrcasting, 363-364, 386, 395-396
floorcasting, 298, 337-346
heighrmapped ceilings, 3 8 1
FLOORDEM, 346
heighrmapping, 9, 3 5 4 , 360
floorheight changes, 365-371
heightrnapping, animated, 397-398
floorheight[ ] array, 36 1 -364, 367
heighrmapping engine, 386-389
floorheight [ ] array, tiled, 384
heightmapping, hybrid, 396-397
floorlires[ ] array, 433-434, 458
heighrmaps, block-aligned, 360-37 1
FLOORLOOP macro, 493, 499
heighrmaps, pixel-aligned, 382
floors, optimized, 489-497
heighrmaps, riled, 382-389
floor[ ] array, 342, 347
hidden surface removal, 324, 382, 524-528, 535,
flor[ ] array, 338-339, 342, 362-363
539
flor[ ] array, tiled, 385-386
HIGHMAPS.PCX, 383
for( ) loop, 27
HORZLINE program , 38-39
for( ) loops, nested, 29, 1 1 8 . See als-o loops, nested
HTIMER routines, 508-5 1 0
fractions, 468
freeware, 553, 557 function 25h, 67 function 35h, 67
if( ) Statement, 1 79, 200 increment value, 485 incremenral division, 43, 1 96, 330
G
init_evenrs( ) function, 93
game features, 539-541
initkey( ) function, 67-69
GARDENS O F I MAG I NAT I O N inirmouse( ) function, 78-79, 8 1-82
lightsourcing formula, 405- 4 1 4, 4 1 8
inline function, 479
lightsourcing tables, 409- 4 1 0, 4 1 7- 4 1 9
input devices, 63
line drawing, 35-43 line, horizontal, 37-39 line, vertical, 39-40
insertion sort, 535-536 installation, xix INT 2 1 H routines, 67
linedraw( ) function, 44-46
INT 33H instruction, 77
lines, 37-43
interface types, 54 1
lines of arbitrary slope, 40-43
interrupt handler, 65-67, 69 interrupt, keyboard, 65
lisrings bitmapped drawmaze( ) funcrion, 1 47- 1 49
inverse square law, 404-405
BITSCALE.CPP program, 202-206 blit( ) function, 1 52
IRET instruction, 69, 73
J joysrick, 84-9 1 joystick buttons, 85-86 joystick calibration, 89, 93-95 joystick posirion, 86-88 JOYSTICK program, 89-9 1 jump instruction, 7 1 -72, 568
K kbhit( ) function, 27, 542 keyboard, 63-76
block-aligned heightmapped draw_maze( ) function, 371 -377 BLOKDEMO.CPP, 378-3 8 1 CLS (clear screen) procedure, 3 1 -32 draw_slice( ) function, 1 49 drawfloorrow( ) function, 498 drawwall( ) function, 488 fixdiv( ) function, 476 fixmul( ) function, 474 floorcasting draw_maze( ) function, 344-346 floorcasting, wallcasting draw_maze( ) function, 348-354
keybuffer array, 93, 96 kilobyre, defined, 230
FRONT l .POV, 560 gerevent( ) function, 96-97
Kirk, James T., 23 1
grab( ) function, 1 5 1 HORZLINE program, 38-39
kludge, 523-524
L
libraries of functions, 1 7, 64
light, 402-404 light and color, 262-264 light intensiry, 405-409, 4 1 9, 432 light pool, 403, 409, 446 lightmapping, 432-434 lightmaps, block-aligned, 433 lightmaps, tiled, 445-446 lightmaps, using, 458 lighrsource( ) function, 279-280, 282, 284-285 lightsource[ ] [ ] array, 409, 4 1 4, 423 lightsourced 3D graphics, 9 lightsourcing, 1 90- 1 9 1 , 226, 279, 40 1 -404 lightsourcing, alternate effects, 430-432
init_events( ) function, 93 initkey( ) function, 68-69 inirmouse( ) function, 79 JOYSTICK.CPP program, 90-91 lightmapped draw_maze( ) function, 434440 lightsourced draw_maze( ) function, 423430 linedraw( ) function, 44-46 LITEDEMO.CPP program, 4 1 9-422 load_image( ) function, 1 26- 1 27 load_palette( ) function, 1 27 loadPCX( ) function, 1 2 5 loadTGA( ) function, 1 1 5 - 1 1 7 main( ) funcrion from BITMAZE.CPP, 1 54- 1 57
I nd ex TGASHOW.CPP program, 1 1 9- 1 20
listings {contimad) main( ) function from GOMAZE.CPP,
tiled heighrmapped draw_maze( ) function, 389-392 tiled lightmapping draw_maze( ) funcrion,
1 0 1 -1 03 MAKELITE.CPP program, 4 1 5-4 1 7
447-453
MAKEMAP, 558 MAKEMAZE.BAT, 5 59
TILEDEMO.CPP program, 392-396
MAKETGA, 558 MAKETRIG.CPP, 48 1 -483
TLMDEMO.CPP program, 453-457 TRIG.H file, 483-484
MAPDEMO.CPP program, 44 1 -445
VERTUNE program, 39-40
MAZE.CPP program, 53-58
wallcasting draw_maze( ) funcrion, 3 1 8-32 1 WALLDEMO.CPP program, 323-324
MAZE.INC, 556 MOUSE.CPP program, 83-84 newkey( ) function, 73 OPTDEMO.CPP module, 5 1 0-5 1 7
WHITEOUT.CPP program, 26 LITEDEMO.CPP, 4 1 9-422, 430 litelevel[ ] array, 4 1 7- 4 1 8 , 423
optimized draw_maze( ) function, 501 -508
LITEMAP.CPP, 434-440
PALETTE.CPP program, 29-30
load_image( ) function, 1 24- 1 27
PCX_HEADER strucrure, 1 2 1
load_palette( ) function, 1 24, 1 27- 1 28
PCX_STRUCT scrucrure, 1 22
loading Targa files, 1 1 2- 1 1 7
PCXSHOW program, 1 28- 1 29
loadPCX( ) function, 1 24- 1 25, 1 53, 326
POLYCLIP program, 1 84- 1 8 5
loadTGA( ) funcrion, 1 1 2-1 1 7 , 1 53
POLYDEMO.CPP program, 1 83- 1 84
loop unrolling, 463-464, 5 1 9
polydraw( ) function, 1 80- 1 82
LOOPNE insrrucrion, 87
POLYGON.CPP, 1 65- 1 66
loops, nesred, 29, 1 1 8, 365, 4 1 1 , 466-467
polygon-based drawmaze( ) function, 1 88-
low-resolution ray rracing, 268
1 90 polyrext( ) function, 2 1 2-2 1 5 portians ofTRIG.CPP, 484 PREVIEW.BAT, 557 readjburton( ) function, 86 readrnburron( ) function, 79-80 readstick( ) funcrion, 87-88 relpos( ) funcrion, 8 1 remkey( ) funcrion, 69 RTDEMO.CPP program, 285-290 SCANKEY.CPP program, 75 sercenrer( ) function, 94 setmax( ) funcrion, 94-95 setmin( ) function, 94 SETMODE procedure, 24 SETPALETTE procedure, 28 TESTMAZE.POV, 554-55 5 TEXTDEMO.CPP program, 2 1 5-2 1 7 TEXTMAZE.CPP program, 2 1 8-226 texrure-mapped ray-cast draw_maze( ) function, 332-336 rga_header structure, 1 1 1
lseek( ) function, 1 1 3
M macro CLK_TCK, 98 macro COLUMNLOOP, 487, 489 macro FLOORLOOP, 493, 499 macro MK_FP, 20, 27 macros, 464, 478-479, 487, 5 1 9 magnitude, 24 1 , 243-248 main( ) funcrion, 26 make code, 70-72 MAKELITE.CPP, 4 1 0, 4 1 4-4 1 7 , 43 1 MAKETRIG.CPP, 48 1 -483 MAPDEMO.CPP, 44 1 -445 marh, 1 73- 1 74 marrix[ ] array, 1 85- 1 86 maze animation, 97- 1 03 maze elements, 1 3 1 maze engine, refining, 1 58- 1 60 maze games, 4 maze grid, 1 44- 145, 298, 300-30 1
GARDENS O F I MAGI NATI ON maze touring, 1 57-1 58, 1 60 maze types, 1 2- 1 4 maze views, 1 3 1 -1 33
MAZE.CPP, 53-58 maze-drawing algorithm, 1 34- 1 40 megabyte, defined, 229-230 memoty circuit, 1 8 memory problems, 1 58- 1 60 messages to user, 4 1 0-4 1 4 mickey (unit o f measure), 78, 80
M K_FP macro , 20, 27 mode 1 3h, 2 1 modulus operation, 29, 30 1
MONA.TGA, 196 mouse, 76-84 mouse driver, 77-79
MOUSE program, 82-84
p paint programs, 1 08, 1 2 1 , i 63, 383 Painter's Algorithm, 535 palette, 27
PALETTE.CPP program, 29-30 palfadeout( ) function, 542 parameterized form, 277
PCX format, 1 09, 1 2 1 - 1 29 PCX_HEADER structure, 1 2 1 PCX_STRUcr strucrure, 1 22 PCXSHOW prograrn, 1 28- 1 29 perror( ) function, 4 1 4 perspective, 234-238, 3 1 6, 368 perspective equation, 533 Piclab, 1 1 0, 1 2 1 , 1 98 , 557 pixel, 21, 3 5-36 pixel, drawing, 201 -202 pixel, plorting, 33-35
N
plane, defined, 229
new function, 1 14
player, tracking, 548-549
newkey( ) function, 67, 69-73
plot( ) function, 276, 285
normal vector, 244, 266
point-slope equation, 1 74- 1 75, 302-304,
normalizing, 252
3 1 3-3 1 4
NOT instruction, 85 nurober line, 230
points, 35-36
numbering systems, 1 8- 1 9 , 468
polar coordinate system, 247-258
0 objdraw( ) function, 528 object array, 524, 528 objects, drawing, 5 3 1 -534 objects, maze, 523-524 objects, representing, 528-53 1 objlist array, 534, 537 offset, array, 27 offsets, 1 8- 1 9 open( ) function, 1 1 3 operator precedence, 246
OPTDEMO program, 508-5 1 7 optic parallax, 234 optimization review, 5 1 7-520 optimize, where to, 466-467, 520 origin, 35
pointers, 1 9-20
POLYCLIP prograrn, 1 84- 1 85 POLYDEMO prograrn, 1 82- 1 84 polydraw( ) fimction, 1 7 1 , 1 80-1 82, 1 86 polygon clipping, 1 69- 1 7 1 , 1 73- 1 74, 1 76 polygon, filled, 1 66- 1 68 polygon graphics, 1 64 polygon maze, building, 1 7 1 - 1 82 polygon mazes, 1 2- 1 3
POLYGON.CPP prograrn, 1 65- 1 66 polygon-drawing algorithm, 1 7 1 - 1 73 polygon-fill graphics, 1 67, 238 polygons, 1 64- 1 68
POLYMAZE.CPP prograrn, 1 90- 1 9 1 POLYRAY ray tracer, 291 polytext( ) function, 208-2 1 5 polytype structure, 1 69, 1 7 1 ports, inputring data, 70, 85
POVRAY ray tracer, 1 08, 1 1 0, 1 2 1 , 1 30- 1 3 1 , 29 1 , 5 54-556
I ndex prev_r variable, 280, 282
remkey( ) function, 67, 69, 76
PROC (procedure) statement, 22-24 processors, 462, 464-466, 470
ROM BIOS, 22-23 roors (quadraric), 278, 283
profiler, 467, 520
rotaring birmaps, 1 96
prr variable, 1 78
rotaring poim, 254-257
purwindow( ) funcrion, 1 0 1 Pyrhagorean merhod, 245, 25 1 , 275, 284,
rotaring ray, 309-3 1 1
3 1 4-3 1 5, 533
Q
rorarion equation, 3 1 0-3 1 1 rorarion, rhree-dimensional, 257-258 RTDEMO.CPP program, 285-290 run-lengrh encoding, 1 22- 1 23
quadraric equarion, 278, 283
s
R
scaling birmaps, 1 96-206
radian sysrem, 249 radiarion (light), 402-404 random number generaror, 272 random( ) funcrion, 285 randomize( ) , 272 rario variable, 209-2 1 0 ray casring, 292, 297-298, 303-305, 3 1 1 -3 1 3 ray rracer, building, 264-280 ray rracer, defined, 1 3 1 ray rracers, 291 ray rracing explained, 262-264 ray rracing a maze, 1 30- 1 3 1 ray-casr graphics, 258 ray-cast mazes, 1 2- 1 4, 324-325 ray-casting algorirhm, 298, 303-305 ray-object comparisons, 276, 280, 295-296 ray-wall comparisons, 304-305, 309 RAYDEMO, 354 rays, reducing, 296-297 read( ) funcrion, 1 1 1 , 1 1 3- 1 14 readjburron( ) funcrion, 86, 88 readmbunon( ) funcrion, 78-80, 82-83 readsrick( ) function, 87-89 realism, 26 1 recursion, 1 34, 1 39, 1 4 1 recursive algorirhm, 1 86 recursive funcrion, 1 34, 1 40 reflecred light, 262, 403 reflective surfaces, 291 regisrer, 23 relpos( ) funcrion, 78, 80-83
scan code, 65, 70-72 SCANKEY program, 74-75 SCREEN.ASM, 22 screen-clearing, 30-32 segmenrs, 1 9 semicolons, use of, 24, 564 Set Interrupt Vecror, 67 ser_palene( ) funcrion, 1 1 8 sercenter( ) funcrion, 93-94 sermax( ) funcrion, 93-95 sermin ( ) funcrion, 93-94 SETMODE procedure, 24 sermode( ) funcrion, 27 SETPALETTE procedure, 28 shadows, 269, 403, 4 1 9, 430 shifr operarion, 30 1 , 3 1 1 , 47 1 -473 similar triangles merhod, 340-343, 364, 388 sine/cosine, 254-256, 476-478 sizeof( ) funcrion, 1 1 3 slice-and-dice maze generation, 1 34, 1 63-164 slope, 40-43, 1 74- 1 75 slope of ray, 3 1 2 smoorh movement, 7 , 325 smoorh-scrolling arcade games, 1 0 sorr types, 535-536 sphere, ray rracing, 282-284 sqn function, 246 srack, 524, 567 srring insrructions, 3 1 surface normal, 266 swapping method, 535-536 swirch( ) Statement, 5 1 , 281
GARDENS OF I MAGI NATI ON
T
u
t unit (unit of measure), 277, 283
Ultima games, 4-5 underscore, use of, 22, 474
table Iook-ups, 464-465, 476-478, 5 1 9 target hardware, 462 TEST instruction, 87
union structure, 265, 27 1 unit circle, 253-254
TEXTCAST.CPP, 332-336
unit vector, 252-253
TEXTDEMO, 336 TEXTDEMO.CPP, 2 1 5-2 1 7 textdemo( ) function, 2 1 5 TEXTMAZE.CPP, 2 1 8-226 textute clipping, 209-21 1 texture mapping, 1 64, 1 9 1 , 1 95, 206-208, 29 1 , 326-336 texture-mapped maze, 2 1 8-226 texture-mapped polygon function, 208-2 1 5 TGA (Targa) format, 1 1 0- 1 20 tga_header structure, 1 1 1 TGASHOW program, 1 1 7- 1 20 three-dimensional graphics, 226 three-di mensional space, 23 1 -234 tick (unit of measure), 98 tiled heightmapping engine, 386-389 tiled heightmaps, 382-389 tiled lightmaps, 445-446 TILEDEMO.CPP, 392-396 TLMDEMO.CPP, 453-457 tmcolumn variable, 328 topdiff, 1 76, 1 79 toperror, 1 77, 1 79
V vector componenrs, 247 vecror_type strucrure, 265 vecror_type variables, 272 vecrors, direction, 24 1 -247 vecrors, unit, 252-253 vertex/vertices, 1 64, 238 VERTUNE program, 39-40 VGA palette, 27-30 video display programming, 1 7 video memory, 1 8, 24-27 video mode setting, 22-24 viewer height, 364-365 viewer orientation, 48-49 viewer position, 48-49 viewport, 169 virtual reality, 7-8, 523 visibility array, 524, 548-549 visibility variable, 5 1 visible[ ) [ ) array, 527 VIVID ray tracer, 29 1
top [ ) array, 1 86 trace_ray( ) function, 276-285
w
transformations, coordinate, 255-258
wall squares, 46-47
transforming, 256
wallcasting, 298-300, 306-321
translating poim, 256
WALLDEMO.CPP program, 32 1 -325 walls, optimized, 484-488
triangles, similar, 340-343, 364, 388 TRlG.CPP, 484 TRlG.H file, 483-484
wall[ ) array, 347, 362-363
trigonometry, 308-309, 465, 476-48 1 , 5 1 9
wall[ ) array, tiled, 385-386 while( ) loop, 1 53- 1 54
Turbo Assembler (TASM), 22
while( ) Statement, 1 79, 200
Turbo Profilet (TPROF), 467, 520
WHITEOUT.CPP program, 26
rwo-and-a-half dimensions, 238-24 1
width variable, 1 76
rype override, 27 1
wireframe mazes, 4, 1 2, 32-59 Wizardry games, 5, 1 07 Wolfeostein 3D, 9-1 1 , 337-338
TO ORDER TO LL FREE CALL 1 -800-368-9369 TELEPHONE 41 5-924-2575
FAX 41 5-924-2576
•
OR SEND ORDER FORM TO: WAlTE GROUP PRESS, 200 TAMAL PLAZA, CORTE MADERA, CA 94925
Book
Block Art ol Windows Game Progromming Creote Stereograms on Your PC Fotol Distroctions Flights ol Fantasy Modeling the Dreom CD Ploying God Wolkthroughs ond Flybys CD Colil. residents odd 7.25% Soles Tax
Shipping
USPS ($5 lirst book/$1 eoch odd'l) UPS Two Doy ($1 0/$2) Conodo ($1 0/$4) International ($30/book)
TOTAL
US/Can Price $34.95/48.95 $26.9 5/37.9 5 $26.95/37.95 $34.95/48.95 $32.95/45.95 $26.95/37.95 $32.95/45.95
Total
Ship to N o rne
__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __
Compony ------Address ------City, Stote, Zip Phone
--------
---
Payment Method
o
Check Enclosed
o
VISA
o
MasterCord
Cord#____________________ Exp. Date ___ Signoture ______________________________
SATISFACTION GUARANTEEO OR YOUR MONEY BACK.
This is a legal agreement between you, ehe end user and purchaser, and The Waite Group®, lnc., and the authors of the programs contained in ehe disk. By operring the sealed disk package, you are agreeing to be bound by the terms of this Agreement. lf you do not agree with the terms of this Agreement, prompdy return the unoperred disk package and the accompanying items (including the related book and other written material) to the place you obtained them for a refund.
SOFTWARE LICENSE 1.
The Waite Group, lnc. grants you the right to use one copy of the enclosed software programs (the programs) on a single computer system (whether a single CPU, part of a licensed network, or a terminal connected to a single CPU) . Each concurrent user of the program must have exclusive use of the related Waite Group, lnc. written materials.
2.
The program, including the copyrights i n each program, i s owned b y the respective author and the copyright in the entire work is owned by The Waite Group, lnc. and they are therefore pro teered under the copyright laws of the United States and other nations, under international treaties. You may make only one copy of the disk containing the programs exclusively for back up or archival purposes, or you may transfer the programs to one hard disk drive, using the original fo r backup or archival purposes. You may make no other copies of the programs, and you may make no copies of all or any part of the related Waite Group, lnc. written materials.
3.
You may not rent or lease the programs, but you may transfer ownership o f the programs and related written materials (including any and all updates and earlier versions) if you keep no copies of either, and if you make sure the transferee agrees to the terms of this license.
4.
You may not decompile, reverse engineer, disassemble, copy, create a derivative work, or oth erwise use the programs except as stated in this Agreement.
GOVERNING LAW This Agreement is governed by the laws of the State of California.
LIMITED WARRANTY
The following warranties shall be effective for 90 days from the date of purchase: (i) The Waite Group, Inc. warrants the enclosed disk to be free of defects in materials and workmanship under normal use; and (ii) The Waite Group, Inc. warrants that the programs, unless modified by the purchaser, will sub stantially perform the functions described in the documenration provided by The Waite Group, Inc. when operared on the designated hardware and operaring system. The Waite Group, Inc. does not war rant that the programs will meet purchaser's requiremenrs or that operation of a program will be unin terrupted or error-free. The program warranty does not cover any program that has been altered or changed in any way by anyone other than The Waire Group, Inc. The Waite Group, Inc. is not respon sible for problems caused by changes in the operaring characteristics of computer hardware or computer operaring systems that are made after the release of the programs, nor for problems in the inreraction of the programs with each other or other software. THESE WARRAN TIES ARE EXCLUSIVE AND IN LIEU OF ALL OTHER WARRANTIES O F MERCHANTABILI1Y O R FITNESS FOR A PARTICULAR PURPOSE O R OF ANY OTHER WARRANTY, WH ETHER EXPRESS OR IMPLIED.
EXCLUSIVE REMEDY The Waite Group, Inc. will replace any defective disk wichout charge if the defective disk is returned to The Waite Group, Inc. wirhin 90 days from date of purchase.
This is Purchaser's sole and exclusive remedy for any breach of warranty or claim for conrract, tort, or damages.
LIMITATION OF LIABILITY THE WAlTE GROUP, INC. AND THE AUTHORS OF THE PROGRAMS SHALL NOT I N ANY CASE BE LIABLE F O R SPECIAL, INCIDENTAL, CONSEQUENTIAL, INDIRECT, O R OTHER SIMILAR DAMAGES ARISING FROM ANY BREACH OF THESE WARRANTIES EVEN IF THE WAlTE GROUP, INC. OR ITS AGENT HAS BEEN ADVISED OF THE POS SIBI LITY OF SUCH DAMAGES. THE LIAB ILI1Y FOR DAMAGES O F THE WAlTE GROUP, INC. AND THE AUTHORS OF THE PROGRAMS UNDER THIS AGREEMENT S HALL IN NO EVENT EXCEED THE PURCHASE PRICE PAI D .
COMPLETE AGREEMENT This Agreement constitutes the complete agreemenr between The Waite Group, Inc. and the authors of the programs, and you, the purchaser. Some states do not allow the exclusion or Iimitation of implied warranries or liability for inciden tal or consequenrial damages, so the above exclusions or limitations may not apply to you. This lim ited warranty gives you specific legal rights; you may have others, which vary from state to state.
SATISFACTION REPORT CARD Piease fill out this card if you wish to know of future updates to Gardens ol lmagination, or to receive our catalog.
Company Name:
Division/Department:
Mail Stop:
Last Name:
First Name:
Middle Initial:
State:
Zip:
Street Address:
City:
Daytime telephone:
Day
Date product was acquired: Month
Year
Your Occupation:
Overall, haw wauld you rate Gardens ol lmagination?
Where did you buy this book?
0 Fair
0 Discount store (name): ____________
0 Excellent
0 Very Good
0 Good
0 Below Average
What did yau like MOST about this baak?
0 Poor
_ _ _ _ _ _
0 Bookstore (name): ------
0 Computer store (name): 0 Catalog (name):
------
-------
0 Direct from WGP What did yau like LEAST abaut this baak?
0 Other
-----
_ _ _ _ _ _
What price did you pay for this baok?
_ _ _ _ _ _ _ _
What inlluenced your purchase of this boak?
0 Recommendation Piease describe any prablems you may have encountered with installing or using the disk: ____________
0 Magazine review 0 Mailing
0 Reputation of Waite Group Press Haw did you use this boak (problem-solver, tutorial, reference• • . )?
0 Advertisement
0 Store display
0 Book's format 0 Other
How many computer books do you buy each year?---How many other Waite Group books do you own?____ What is yaur favorite Waite Group book?-------
What is your Ievei of computer expertise?
0 New
0 Power User
0 Hacker
0 Experienced Professional
ls there any program or subject you would like to see Waite
What computer languages are you familiar with?_ _ _ _ _
Group Press cover in a similar approach?_______
Piease describe your computer hardware:
Additional comments? ------
Computer
_______
5.25" disk drives Video card
.
D Dabbler
D Programmer
Hard disk
_ _ _ _ _ _ _
3.5" disk drives _____ _ _ _ _ _ _ Monitor __
Printer
Peripherals
Sound Board
CD ROM
______
______
Piease send to:
Waite Group Press Attn: Gardens ol lmagination 200 Tamal Plaza (orte Madera, CA 94925
here for a free Waite Group catalog � J --------------------------------------------0--Check ------------------------�� � __ - - ��--�-�""" Ga,dens ol lma�na twn
m l> z
9