Build Your Own Flight Sim in C++ (DOS GameDev) Michael Radtke & Chris Lampton
Build Your Own Flight Sim in C++ (DOS GameDev) Michael Radtke & Chris Lampton
LD YOUR OWN
INCLUDES
SAMPLE
FLIGHT
SY]
fe]
LV] Wy
AND
DEMOS
LONI DRA
00P
SIMULATOR USING
MICHAEL RADTKE
CHRISTOPHER LAMPTON
\ of
Waite Group
Press™
Corte Madera, (A
Publisher: Mitchell Waite
Editor-in-Chief: Charles Drucker
Acquisitions Editor: Jill Pisoni
All terms mentioned in this book that are known to be trademarks, registered trademarks, or service marks
are listed below. In addition,
terms suspected of being trademarks, registered trademarks, or service marks have been
appropriately capitalized. Waite Group Press™
cannot attest to the accuracy of this information. Use of a term in this book should not be regarded
as affecting the validity of any trade-
mark, registered trademark, or service mark.
The Waite Group is a registered trademark of The Waite Group, Inc. Waite
Group Press and The Waite Group logo are trademarks of The
Waite Group, Inc. Apple, Apple 11, and Macintosh are registered trademarks of
Apple Computer Inc. Amiga is a registered trademark of
Amiga Corporation. Atari is a registered trademark of Atari Corporation. Atari 800 and Atari ST
are trademarks of Atari Corporation.
Borland and Borland C++ are registered trademarks of Borland International, Inc. Chuck
Arts. Commodore 64 is a trademark of Amiga Corporation. Dr. Dobb's
Yeagers Air Combat is
a trademark of Electronic
Journal is a trademark of Miller Freeman, Inc. Electronic Arts is a
registered trademark of Electronic Arts. F-19 Stealth Fighter
is a trademark of MicroProse Software, Inc. Falcon 3.0 and Tetris
marks of Spectrum Holobyte. Flight Simulator, Flight Simulator 2, and
are trade-
Flight Simulator 4 are trademarks of SubLOGIC. IBM is a registered
trademark of International Business Machines, Inc. Jetfighter 11 is a trademark of
Velocity Development. LucasArts Games is a trademark
of LucasArts Entertainment Company, a subsidiary of Lucasfilm Ltd.
Megaortress is a trademark of Pacific. MicroProse is a registered trade-
mark of MicroProse Software, Inc. Microsoft is a registered trademark of Microsoft
Corporation. Microsoft Flight Simulator is a trademark
ol Microsoft Corporation. Secret Weapons of the Luftwaffe is a trademark of LucasArts Games.
Sierra Dynamix is a trademark of Sierra On-
line. Spectrum Holobyte is a trademark of Sphere, Inc. TRS-80 Model
1 is a trademark of Radio Shack, a division of Tandy Corporation.
Velocity is a trademark of Velocity Development. All other product names are trademarks,
registered trademarks, or service marks of their
respective owners,
Printed in the United States of America
96907989910987654321
Library of Congress Cataloging-in-Publication Data
Radtke, Michael.
Build your own flight sim in C++ / Michael Radtke, Chris Lampton.
p. cm.
Rev. ed. of: Flights of fantasy. 1993.
Includes bibliographical references and index.
ISBN: 1-57169-022-0 : $39.99
1. Flight simulators--Computer 2. C++ (Computer program language)
programs. 1. Lampton, Christopher.
IL. Lampton, Christopher. Flights of fantasy. 111. Title.
TL712.5R32 1996
794.8' 75--dc20
69-11089
CIP
Dedication
Publi
WELCOME TO OUR NERVOUS SYSTEM
Some people say that the World Wide Web a graphical
extension of the information superhighway, just a network
is
of humans and machines
sending each other long lists of the
equivalent of digital junk mail,
I think
it
is much mor than that. To me the Web is nothing less than the
nervous sys-
tem of the entire planet—mnot just a collection of computer brains connected together,
but more like a billion sili neurons entangled and recirculating electro-chemical sig-
nals of information and data, each contributing to the birth of another CPU and
another Web site.
Think of each person’ hail disk connected at once
to
every other hard disk on earth,
driven by human navigators searching like Columbus for the New World. Seen this
way
the Web is more of a super el tity, a growing, living thing, controlled by the universal
human will to expand, to be more. unlike a purposeful business plan with rigid
Yet,
to be part of this sy ev
0 experience, like a nerve in the body, the flow
food chain of the mind. Your mind. Even more,
e
ney to ends any topic from convenience of your own screen. Right now we are
focusing on computer to
If you are interested in dise
but the stars are the limit on the Web.
¢
200 Tamal Plaza
<
ner
Sincerely, eH ETE
NEw, http: //www.waite.com/waite
> Fr i i
Come Visit
ie
rt
WA g y E 1 CO M
Waite Group Press -
=
a
“The Wide Gmoup
zeaeSg
A eaSn
mer
Bementwet
Pees i &
Group
Horter
Press
vod
World
0S
Wide
ot
AO Yo fod
Wan
for
Site.
books
emt
Fe fei»
a pr
ops
El
Senn
Om Po te ee 45Sh
mare Tr comort of Yd
UT
ASe
COTE OR
IPE (0M)
seutRe
sapparsiee
ERT
VARA
Now find all the latest information on Waite Group books at our new Web ere ssp
where you
site, http://www.waite.com/waite. You'll find an online catalog
Bina
can examine and order any title, review upcoming books, and send email
and editors. Our FTP site has all need to update your book: the
to our authors you
latest program listings, errata sheets, most recent versions of Fractint, POV Ray,
ARAL
Polyray, DMorph, and all the programs featured in our books. So download, talk to
us, ask questions, on http://www.waite.com/waite.
The New Arrivals Room has all our new books
listed by month. Just dlick for a description,
Index, Table of Contents, and links to authors.
|
LT EO TR
you'll interact with Waite
ETT Ta HEIR NE
About WEP
TE
you in touch with other
CL
EEE
Links to Cyberspace]
POT
and other interesting
(IRC
(ene
World Wide Web:
http://www.waite.com/waite
Gopher: gopher.waite.com
FTP: ftp.waite.com
About the Authors
......
..
1
Chapter 1: A Graphics Primer . .
............. o.oo
. . . .
57
Chapter 3: Paintingin256 Colors. . .
101
Chapter 4: Making It Move.
...
...................
. .
123
Chapter 5: Talking to the Computer...
163
Chapter 6: AllWired Up
oo
.
...
. .
291
Chapter 9: Faster and Faster.
319
Chapter 10: Hidden Surface Removal . . . .
............
Chapter 11: Polygon Clipping . . . .
391
Chapter 12: Advanced Polygon Shading . .
..
443
Chapter 13: The View System
oe
......
.
of Scenery. 475
Chapter 14: Fractal Mountains and Other Types . . .
513
Chapter 15: Sound Programming. . .
.. ...................... 543
..
.
615
Appendix B: Bibliography . .
............ 617
627
INARX
vii
.... .eT
|.
|
-- Contents
;
Table
gre
of
= ire
=
ai
Chapter 1: A Graphics Primer
, of, ER ae Pe
.
1
The Descendants of Flight
..........0..
Simulator...
0 cL
4
opal TT
ann
.
GephicsapigGames.
0
5
SWtverus Graphics
oo Lo Lat
LL
aeeam
6
ey aa
teProgammensView
sn 6
Lc
ISRO VOR
LL DL ee
8
Ta
EM aapnicImprove
Tl
cE
8
a
.
Np MadesGslare, 9
«vr
EA
0LLL Ee a Bee da
Resohutionand Color Depth.
aa
. .
10
The Memory Compaction...
«ov. 12
lh ne
LLL
Bits,
12
MeO
ee
AddIesses
Te
LoLcE
.LoLoo,
.
14
ge a
Porting atMemory.
0 15
ona
NEBRERd FAC... 16
sa, on melar J
TUR pn
HOWEmMapEWork. 17
ACaiorBiinep.
oo.
es
18
Mode BhiMemory
ch el Re 19
.
2 ==
TheColoiPaidier
...
VOR.
19
ieda
POSAMMIREIE
Te 20
. ............................._
Chapter 2 The Mathematics of Animation
ane
.
23
Sing lie
ComespncOaiommes.
26
ERCHen Pane...
«0. 27
=, o.oo LL mE
Geometry Meets Algebra
28
(ii
. . .
CodtdiniesontheComputer
oso 0 en
To
o
.
0, 31
room Coordindtes to Pot Offsets...
ole
32
MRR he Ongin, ool 33
ot........................
. i
Pils,
a de ee Cn
+.
Adsressing Multiple
hs
v0 33
inthe Mid Dimension... 34
Shapes, Solids, and Coordinates . . .
36
viii =
te 0.
... «oo
«Le
Table of Contents
mmm
.
Three-Dimensional VErtices 38
oo.
. . . . .
40
oe
Graphing EQUations. . . . . . . .
«ooooo
. ©.
43
Transformations
«ot
o.oo
© .
and Scaling 44
.
Translating
.
. . . . . . .
X,Y, and Z 45
Rotatingon . . .
«oo
47
«ooo
Doing What Comes Virtually. . . . .
48
Degrees versus Radians. . . . . .
49
«oo.
Rotation FOrMUIAS. . .
«© vo
viii
Matrix AIBEDIa. 50
.
51
Building Matrices . . . .
51
Multiplying Matrices. . . .
o.oo
53
Three Kinds of Rotation Matrices «vo
«ooo
. . . . . .
«oe
54
Floating-Point Math . . . .
...........................
LL.
57
Chapter 3 Painting in 256 Colors
.
. . .
60
Getting inthe Mode . . . . .
61
Doing It in Assembly Language. . . . . .
62
Assembly Language Pointers
eee
. . . . .
«o.oo
«oo
63
Getting It into a Register.
vv
. . . .
64
Assembly Language Odds and Ends.
...........
oo
. . . .
«o.oo
67
Accessing the Video BIOS
ooo
.
. . .
69
oo
Restoring the Video Mode.
.....
. . . .
eea
70
More about BItMAPS . . «+ ©
ov oe
Finding the Bitmap . . . . .
7
72
Drawing Pictures in MEMOTY . . .
«oo 73
Clearing the SCrEeN . .
+. oo
75
Lots and Lots of Colors . .
«oo
o.oo
76
Setting the Palette. . . . . . . .
77
The Default Palette . . . . .
«oo
SIOMNG BIMAPS ve . ©
+ +
ve eee 79
Compressing BItmaps. . . .
«cco 80
82
a
Graphics File Formats.
cel ce aia
. . . .
PONE oovin
Inside @
APCUSHUGHIIE
vines iene bb aad hs RE Big oe en, 82
83
me RRF
.
ee
i
ee
a
hee ~~ Hm BE BE
EN
oT
Build Your Own Flight Sim in C++
~
nn dee
yen .
RICCI. vu oo sd
aE
©
POXLIMIRtONS
84
REGRET ee eT
. he
85
IRCKRoader.
a
87
Reading the Bitmap and Palette. . . . . .
i
Compiling and Running the Program
......... .. ..
el ae ee
92
0 oo
. . . . . . .
ean
0........................
Agen.
oa
Musag ROME
ee
«0,
93
Ie
en
Compresgion Funclion™. 94
pecompressingthe Data.
0 ©. 99
Building tie Pmogmim
en
re 99
a
Chapter 4 Making It Move . . 101
Motion Pictures, Computer-Style
. LL
. . . .
104
lq 0LLea BE
BARRE Maton 105
ca
et
a
ASHE QrSEifles.
1, . cml men
Cu ae
107
ne
BundingaSpiitedlass 108
fc a
A Simple Sprite Drawing Routine.
oF Loop
en
. . . 110
a Ll ar
TmspaletoMels. 11
EE
a Te deas
tings Up.
Ea
speedme
Eea
oo, 112
PUMERBRIRS.
oil
Lol
112
ee
Sprites...
a
BASING
nm
13
LL Ga
necui
GED Sprites. ©.
ae
ee
114
ThelhalmanComieth
©... oo 116
hn
Spite...io nial
ooo
BolmngtheStoen 116
de
, ©
Conglflckipga
ow. onl 117
.....ee
cio
lh
Setting Up the Walking Sequence. . 118
Stuff...©...
. . .
1 eR
Walkman Sirus His
0, oo 18
. Loc LoL
120
ei
Budge ogiam
pn
122
Dee
Chapter 5 Talking to the Computer
LCL, oe Ds
. .
123
.
Progammgthe dowstiek. 126
AplogenimOiilal. 126
BOB
ee LD a oe
128
TooMathemgncsof Truth.
©...
cD
129
Masking Binary Digits
Other Bitwise Operations.
oo
cub.....................
cial 131
132
.. .......................
. . . .
Decodingthe GameportByte . . .
133
The Button-Reading Procedure.
.................... .
134
oe
Table of Contents m ®
«ooo
mm
«oc eee
ee
vee
135
ABit of INfOrMation «oo
«o.oo
. .
135
Timing the Status Bit. . . . .
136
oe
COUNLING DOWN
ee
© ©
oo
©
136
«ooo
The Joystick-Reading Procedure...
137
Calibrating the Joystick... . . .
138
vee
Not an Input Device Was Stirring... Except for the Mouse . . .
«vv 138
oo
Listening to the Mouse's Squeak . .
141
BUtON-DOWN MICE
eo ee
«ooo
oo
.
. © ©
141
Mickeying with the MOUSE .
«+ «ooo
vo
142
The Mouse Position FURCHON
«oo ov vv
«cove
ALKeyed Up
«ooo
144
«ooo
The BIOS Keyboard ROUEINES.
.
.
145
eee
Managing EVENTS
148
The EventManager Class
«coco
. . . . .
148
ooo
The EventManager FUNCHONS.
oe
. . .
150
cove
The Master of EVENTS. .
«oo vv 150
Inside the getevent() FUNCHON . . .
154
Walkman REMUINS. ov ve eee
«ooo
© + +
oo
162
Entering a New DIMENSION . . . .
«oo ooo
162
Building the Program . . . .
«oo
163
Chapter 6 All Wired up
ooo
166
Snapshots of the URIVEISE
166
«oo
BItMap SCAING. «+ + +
«vo 167
«vv vv
«ooo
oe
Rendering TeChnIQUES . . .
ooo
168
Wireframe and Polygon-Fill
.
. .
170
Vertex DESCIIPIONS + +
«vv 172
«ooo
Two-Dimensional Wireframe Package
«coo
A . . . .
176
Bresenham’s Algorithm. . .
178
C+...
o.oo
Line-Drawing Routine in ooo
vee
A
180
ee
Testing the Line-Drawing Function
«oe
. . .
181
Drawing Random Lines. . . .
183
A Line-Drawing Function in Assembler...
186
Drawing SHAPES «+ «vo
eee
. .
188
Creating Shape Data. . . .
«oc
A Shape-Drawing Program in C+... o.oo 190
192
Transforming SHAPES
eee
. + + + +
192
Local COOFdINGLES « «
«+ vv vv vv
xi
aase..aee
Le
a
icoa..
~~ Build Your Own Flight Sim in C++
aad
=
0. nl oR
ee
el
SERRE.
aL
194
BOR
PEER i
197
LLL
202
DoiglmtE Matrices.
el
206
Chapter 7 From Two Dimensions to Three
. ..
.
asco EERE
.
213
(Sill
CL
215
Qbengses. 217
From World Coordinates to Screen Coordinates
... ....
a 00
. . .
.. 219
Ee
Storing the Local x, y, and z Coordinates
....... .. ..
EL
219
..
. . . .
.
MED
Creating a Three-Dimensional Shape. . .
.. ....... . 222
0
amabespedive
223
WEEE Bngion
ee
228
............ .............
Transformationin Three Dimensions
... 229
oa,©.
.
232
sc
.
ERE
Hm EInchon
234
Helps Fundion.
Dna
aE Ee
ET
o 0
eam
235
oD
ndion
es
a
236
TERSaMD Funglion
238
oo
nl
oi Leeee
WsREWg bneion,
CT
PIERCE.
ee
239
ne
ee
iw LLL.... ooo
240
ee
Amainglelibe: 0
ion
241
©
.........
UBWIRERImIIG.
ae
244
LRlRrlY.
oe
DOWNER
245
00 ly i
Chapter 8 Polygon-Fill Graphics
249
ada
.
Thefoleibetipor.
Ge ne
251
Te
WE
©,
Vole Tbe Stugnre,
0
Eon Boel hon be
254
eles...
0Ee
aeLE
254
WSN
MOwdl 255
seine.
of
256
257
The Polygon-Drawing Algorithr
., 0.
WER
,
261
Terohgonlrwing Fusion
7... ,
262
RMONaL
as, ee
263
264
DWE Rogen.
o.oo...
265
Beeseabam DoesHisThing
266
The Four Subsections
Counting Off the Pixels.
Looping Until y
Drawing the Line
Cleaning Up.
The Complete Function
Manipulating Polygons
The Transform() Function
The Project() Function
Backface Removal
Readingthe
The Polygon Demonstration
Code.
Executing TProf
Reducing
Profiling for Real
tO
oo
Integer Fixed-Point Arithmetic.
Zero
.
oo
.
..
«ee
...
o.oo
oo
Limitations of Backface Removal
L
Data File
«oo
«oo
©.
.
©...
«oo
.
...
.
.
Increments...
Loop...
LL
.
Unrollingthe Loop
Putting
The Problem .
It
©...
Using Look-Up Tables to Avoid Calculations.
i
Alto
.
Work...
.
.
.
«©
.
.
.
.
.
.
.
oo
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Program.
.
.
..
.
.
.
a,
oo. Lb vou oo vio
.
.
.
.
.
.
.
............................
.
.
cooamenaivdiGmiiioa
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
...
ooo
. . .
.....................
. .
...................... ..
.
.
Table of Contents m
LL
LL
ieilBen Lp id
"ou
266
267
268
269
270
271
280
280
281
281
283
284
284
284
286
289
291
294
294
295
295
297
298
299
300
301
302
303
305
309
310
312
319
322
323
325
327
.i
~~
ar ere ——& one Ho
ee
a.......a
Build Your Own Flight Sim in C++
ou
Ee ae
LL}
oo ee
nh
ee ea
Test) 327
ad
co Ln
cv
JOSE 329
vo
col
BE kn eae ss
cosee
i eea
TOSED. 329
a
ae ae eee
Testd 330
SL
a nT
TBS 33]
MutuahOverlap. 33]
livre
Time Considerations. . . . . 332
The Z:Buffer Algorithm... oo cs LL 333
Le ....................
Reworking the Classes a Step Ahead
co
335
en
. . . . . .
a ce
Drawing the Polygon List. . . . . . . . 353
TheViewNVolume. =~.
Lo vv 358
Clipping against the View Volume
..................... 363
Eni
. . . . . .
«vi .. «cou WE
. . . . .
a
FourTypesofibdge. 366
i asna
.
©.
Ce
The Point-Slope Equation
Eee
=. a
. .
Ga 368
er
. .
vii
©.
sa
The Clipping-FUunCtions 375
... oi cus
oc ou vl cud es
v0
ir a
. .
ER ee Le
ee Ee
The Frontofthe ViewVolume
«ea ee
.-. ia 376
a
ed
. . .
pe bBdge 376
i
oo sr
WPE2 EGER . 377
WP BEdge ... 377
We BEdge:, 378
The ZAPO
“ws.. no.osoiiain yd oan iin
FURCHON . 378
i
The Restof the View Volume. . . 380
Clipping. 0 «Lo
Finishingthe 382
a as.
...... .. .... ... .. ..
0, 0, 0
Chapter 12 Advanced Polygon Shading . . . .. 391
(eran
...
lamination...
LoL
a
394
The Visible Spectrum and Color Space. . .
©... 394
..................
.....
Light Sources: Lightbulbs versus the Sun. 395
a
. . . . .
0...visLoo
sn
. . . . .
Keeping ftSimple
dha 398
ol aase
Shedding Light
A Light
on Polygons...
Source for QurWorld
cia, ae Dh 398
398
.. Lo. Ln Ee es
. . . .
Tn
AGradusted Palette. . 400
insieliing the Palette... 5
403
Xiv ®
What's Normal? ©. .
The Shading FUNCHONS.
Completing the Lighting Demonstration
Gouraud Shading.
Vertex NOMMalS
LIGHTDEM SUMMArY
.
Bresenham Revealed.
.
Bresenham on Brightness
oo
The Brightness Calculation.
.
«oo
.
«+
Aligning ROMLION
UNIVErSe
WOTK
HOMIZON
.
Function...
+
©
©
.
.
oo
.
«oe
«ooo
oe
eee
....
oo
oto
...
eee
ooo
«©
«oo
FUNCHON.
.
«
oo
ooo
©. oo ov
«ooo
«oo
oo
o.oo
.
«
.
.
.
oo...
ooo
«oo
«o.oo
«oo
«ooo
oo
«ooo
ooo
ee
«ooo
.
..
.
.
.
«oo
Code...
vo
.
.
.
.
.
ee
oo
vv
«oo
«oo
«oo oe ee
.
.
.
.
.
World...
.
.............................
.
.
.
.
.
.
.
.
«oo
.
.
.
..
Program...
.
.
..
.
Viewer...
. .
..
. .
ee ee
.
Table of Contents
................
coes
....cc
EmERE
oo
m mm
405
407
411
415
416
420
427
430
43]
432
432
433
434
434
436
437
439
440
44
443
446
446
447
449
450
451
452
454
455
456
457
458
459
460
460
461
461
wm
a
i
re
4 =" mn
EER
a
Build Your Own Flight Sim in C++
ogiEE ch
oe
EE
Remappmgthe Angles... LL 464
oi ..
SRE 465
aa
......... ......
Creating the Sky and Ground Polygons.
Me DisplavO RURCHON,
©.
. . . . .
.. .. 466
471
Le
. .
i... ..
Iles...
. .
ee
0...
COR
....... ii...
.
USER
478
coLEaSe
HowanObjectFilelsOrganized.
Using a Program to Generate Object Files
. .
..... ... 478
.. ..
Le
. . . . . 481
Righthanded orleftHanded?
FOOTE
©... , .
Sinus To 482
482
LL
ee
ESkailanty 483
Ming
LL
Use of Fractals.
oo oh
0 ce
484
Ranges... 0
LLLL........... .....
ch
Meuse Graphics.
©. 486
FaM MEU
._
486
oo i ee
0 nee
The Mountain-Drawing Program.
a
oo
488
=... snp
. . . .
.
The lmesegOranction.
sa 489
Plog, 0,
ee
meMsuaMa 492
a
MeeDimensional Fractals.
on
494
©... i va
iii
ASDNMOURAI Generator
...
eia BU
494
a
Pabudo-Rasdom Numbers’,
eee
497
an
es
"0
ea
Recisminginfothe Depths © .
499
Senge =.
a
Fmcal Date
he
oi in Dee
co
Mountaifs..
501
Me fRAUILIZEO Fundlion
ea
©
Changes.LL
503
MBng Many
[Le
506
Wee
«o.oo. ob
506
a
TOE FRRCTEMO Program
aa a aE
510
SWIM FICS. .. oc 511
| Lea Ree
Chapter 15 Sound Programming 513
JMEHEOY
. . .
SRE.
515
ce
PMN,
cn
516
pie
ee
520
SRNRESID 523
ne
oun
DIM VIRIS FRY. 526
Lu aT
00 saa ha Te a
PuttingismeSonntls. 526
Komolinglhe Chip 528
xvi H
ee
oo
A
Table of Contents m ®
«ooo
=m
eee
oo ee
529
oe
1s SoUNd OUE TREE. © ©
532
Initializing the FM Chip... 534
vv
o.oo
Sounds and SHENCE +
© ve
e eee
537
FIVING SOUNDS
«ooo
«ooo
. + +
542
Sounding Off .
«oo
..................oors
.
543
Chapter 16 The Flight Simulator
ooo
. .
eee
oo
546
Interfacing to the View System
.«oo
. .
ee
548
Animating the VIEW
549
Flight SIMUIAtOTS.
550
The Flight Model
«oo
.
. .
551
The Flight of an Airplane «oc
eee
.
ee ee
552
TRIUSE © +
oo ee
555
ooo
557
Controlled Flight. . . «+ «oc
558
CONTOl SUFACES . © +
«vv oe ee
eee
560
vee
The AITCraft Class «+ «vo oe
eee
.
562
From Input to Flight Model... o.oo
«oo
562
The GetControls() FUNCHION . .
................. 595
vee «ov
Chapter 17 The Three-Dimensional Future . . . .
eee
598
oe
eee
ID ACCEIOIAMONS. © + +
oo
598
+ «oo
oo
Polygon SMOOHNING. +
599
RAY TIACNG. © + +
IEX
cocoa
. . .
602
Bitmapped ANIMALION . .
«oo 604
.
DO-IEYourself BItMAPS
«+ «vv
. «oo
+
.
605
The Ultimate Flight SImulator. .
615
Appendix B Bibliography .
E BE HE EE BH
xvii
Acknowledgments |
his is a book of synergy, and as such,
its sum has many parts to be appreciated.
First of all, the publisher, Waite Group Press, has
continued to pursue visions
which excite computer aficionados, Everyone at Waite is
supportive of techno-
cyber-compu-babbling writers, and their efforts make
the Waite webmaster who introduced it easy to get the job done. It was
me to them. Thanks, [email protected]! And a
tip of my virtual hat goes to Mitch and his keen
eye for the cutting edge of technology.
Coordinating the entire hodgepodge, Andrea Rosenberg provided const
declarations for
us global modules, including Harry Henderson, André
LaMothe, and the contributing
programmers and authors.
The entire experience was an excellent demonstration
of class inheritance. The first
version of the program, Flights of Fantasy, had a considerable
list of contributors.
Christopher Lampton and Mark Betz wrote the lion’s share of code in
that version, and
it would be a disservice not
to give them tremendous kudos here. If their code has
been changed, the design lives on in a revised form.
It is also noteworthy to mention
the other contributors listed by Chris
Lampton in the first edition: Hans Peter
Rushworth, John Dlugosz, and Hal Raymond. I do not know
exactly what they did,
but first edition references alluded to
many late night phone calls while working out
flight algorithms.
The new version of the program was
enlightened by Dale Sathers excellent shad-
ing implementation and examples. Keith Weiner and Erik Lorenzen
intoned two sep-
arate sets of sound code, one of FM sound effects and the other
using music by David
Schultz. Check out Appendix C for a version of the
flight simulator which uses the
DiamondWare Sound ToolKit,
a shareware sound library.
xviii =
Foreword
the birds in the
ince the dawn of time humankind has dreamt of flying among
the ages passed. The
skies above. This dream became clearer and took form as
great artist, sculptor,
and scholar Leonardo da Vinci was one of the first to try
to create a flying machine. His renderings
show some of the first visualizations of what
we now know as the airplane and helicopter.
life one winter day in the
As time marched on, the drawings of Leonardo came to
epic first flight of the Wright
Brothers. The day that “The Flyer” rose above the Earth
and flew would have a profound effect on humankind in
the near and distant future.
finally realize the machines that
Today we have begun to create modern aircraft
that
look like something out of Star Wars.
we have so longed to see in action. Fighters that
and flown by computers at many times
Experimental planes made of new materials
the speed of sound.
those chosen few with
However, these flights of fantasy have always been privy to
for long. As the aircraft industry
the access to these amazing machines. True, but not
the computer industry. Two men, Evans
was humming forward at mach speed, so was
visualization that could be used
and Sutherland, laid much of the groundwork for 3D
to create synthetic cockpits and simulate flight
in the virtual cyberspace of the com-
for the elite. Those who owned or had
puter. Alas, this new technology was again
access to supercomputers and custom hard-
of oper-
ware needed to perform the millions
ations per second to create the illusion of
flight. Then something happened. The com-
industry, in particular the personal
puter
computer industry, started making quantum
leaps in processor power and soon the PC
had as much computing power as a mini-
simulation had
computer. The dream of flight
become a reality to the home user.
We have all played flight simulators
on our PCs in awe of the worlds we flew
above. However, the underlying magical
technology was still out of reach for
most, except, of course, for the students
"Build Your Own Flight Sim in Ca
André LaMothe
Preface
With the program's metamorphosis, the text was changed to reflect these code
improvements. Chapters were added, expanded, and/or replaced. Code listings and
figures were revamped.
At the time of this writing, the book is finished and another
pass is being made to
the code presented in the book. Any and all code is included on the accompanying
CD as long
as it was done at the time the CD was put to bed (publishers jargon for
sent to the presses). As was said previously, a program is a growing work. The book
and CD are snapshots of that work at one point in time. For this reason, on the CD,
there is a file called README. TXT that documents any major changes or bugs
unearthed since the book was completed. Smaller changes are documented within the
source files. Revision notes preface all code changes along with comments noting the
reason for the change and action taken.
To reflect the dynamic nature of the evolving code
in this book, the latest code is
available on the Internet at www.waite.com. Again, the README. TXT file will map
any changes or anomalies since the CD version. The site for the continued develop-
ment of the flight simulator was changed from a CompuServe forum to provide a
common area for those net-using readers who don't have a CompuServe account.
Now, straight net shooters and AOL folk can partake in ongoing development along
with CompuServants since all three can access the Internet.
Michael Radtke
Berkeley, CA
xxii ®m
Setup |
The accompanying Build Your Own Flight Sim in C++ CD contains the flight simulator
built throughout this book and the example source code. It also includes an installation
program to copy these files to your hard disk.
Requirements
To install all the files, you will need approximately 10.5MB of free space on your hard
disk. The installation program creates a directory called BYOFS, then creates the fol-
lowing subdirectories within BYOFS:
3DWIRE
FRAC
FSIM
FSIM_BYOFS
FSIM_STK
GENERAL
GOURAUD
LIGHTING
MOUNTAIN
OPTIMIZE
POLYGON
SOUND
TEXTURE
WALKMANZ2
WIREFRAM
The files are then copied into the appropriate subdirectory. The FSIM subdirectory
contains the full flight simulator. Other subdirectories contain code modules devel-
oped within several chapters throughout the book.
Installation
There is nothing magical about the installation process. It is totally understandable
that you may only want toinstall certain source modules to save hard disk space. We
have included an installation program on the CD called SOURCE.EXE which lets you
HE BE BE HE BE
xxiii
or Build Your Own Flight Sim in C++ "EEE
select the modules you wish to copy to your hard disk. An alternate batch file named
INSTALL will copy ALL of the source code using the DOS XCOPY command. Or alter-
nately, you may want to use your own tools to select which files you copy to your hard
(F)
disk.
We'll assume that you're installing the CD from drive E:. (If not, replace E: with the
correct CD drive letter.) To begin, place the Build Your Own Flight Sim in C++ CD in the
E: drive. Switch to the root directory of the E: drive at the DOS
prompt by typing
E:
Ccb\
After a moment, the main screen appears. The source projects are listed in the same
order as
they appear in this book. The main features of this SOURCE program are
m the list which allows you to highlight and select items for copying
® the menu which determines what action to take for selected items
m a system display for various statistics such as how much disk space is available
Select and Copy Projects
To copy a project to your hard disk:
1. Move the highlight to the list by using the tab key.
2. Locate the project you want
to
copy and press to flag the project for copying.
If you want
to list,
select all the projects in the press (A).
3. The SOURCE program normally creates a directory on the C: drive called
BYOFS. If you prefer a different destination, press (0), then enter the drive and
directory you like better.
4. To begin copying the selected projects, press (C). The program creates the direc-
tory on the destination drive if it does not exist, and the projects are copied into
subdirectories.
Flash Copy
If you only want to copy one project, you can Flash Copy
it
to your hard disk. As long
as no items have been flagged for copying, the Flash Copy option is available to you.
To do this,simply highlight the desired item, then press (C).
xxiv =
Setup
EE
Do-It-Yourself Install
all neces-
If you want to install files yourself, go for it. We have taken care to group
subdirectories. To this, first create a destination
sary source files within their own
do
directory on the desired drive. For this example, we'll call it FLYER. Then copy the
desired files from the CD drive. For example, install the Walkman2 project files to
to
the F: drive type:
E:
CcD\
MD F:\FLYER
XCOPY /S WALKMAN2 F:\FLYER
CONFIGURATION
files are configured to
The program project files which are included with the installed
installed C:\BC45. The path to the
a Borland C++ version 4.5 compiler which was on
In this case,
compiler libraries and header files may be different on your computer!
include file IOSTREAM.H” (or some
the typical error message will be “Unable to open
default, one of the
different header file name). To change these paths from our open
Directories dialog allows you
projects, then click on Options - Project. The displayed
the to match computer. Enter the correct path if necessary and
to change paths your
selection.
save the project using the Options - Save menu
Borland 4.5 was used to create the make files in each directory. If you are going to
also need to change the path to
use these MAK files to build the program, you may
A DOS error message similar to the above
your compiler from our default (C: \BC45).
IDE error message will result if this is the case. Use an ASCII
editor to adjust the paths
The paths are located in the Options section of the
for your computer environment.
individual make files.
and
The older version of Turbo project files (ending in .PRJ) have been updated
versions. Older version users will likewise have to
tested along with the .IDE project
adjust the project directories ifthey are different from our default (Cz \BORLANDC).
EA
rd
Thiel
A Graphics
Primer
In the late 1970s, Bruce Artwick changed the course of microcomputer gaming
history. The computer arcade games of that era were making the transition from the
simplicity of Pong and Breakout to the relative complexity of Asteroids and Pac Man,
but Artwick’s ambitions went beyond even sophisticated two-dimensional animations.
He wanted to create a world inside his microcomputer and to show animated images
of that world on the video display.
To that end, he developed a set of three-dimensional animation routines tailor-
made for the limited capabilities of 8-bit microcomputers. With the encouragement of
a friend who flew small planes, Artwick used these animation techniques to write a
game called Flight Simulator, which ran in low-resolution graphics on the Apple II and
TRS-80 Model computers.
T
3 EH
Even so, Flight Simulator is memorable more for what it became than for what
HEH
it
was. Over the next three years, Artwick wrote a sequel, Flight Simulator 2, which was
first marketed in 1982 by the Microsoft Corporation for the IBM PC. FS2 was a quan-
EH
EB
tum jump beyond Artwicks first simulator. Although the original FS was still embed-
ded within it as “World War I Flying Ace,” the meat of the new game was a huge data-
base of scenery that let the player fly over much of Illinois (Artwick’s home), north-
west Washington State (site of Microsoft), Southern California, and the New York-to-
Boston corridor—all in high-resolution color graphics.
Although the effect that they produce seems like magic, the techniques used to ren-
der the three-dimensional world of a flight simulator aren't magical at all. For years
they were hidden in the pages of esoteric programming texts, where programmers
without a background in advanced mathematics and computer theory have found it
difficult to apply them. Until now.
To illustrate how these techniques work, we'll build a number of demonstration
programs in later chapters, culminating in a working flight simulator.
For the benefit of programmers with no PC graphics-programming experience,
though, we'll start with an overview of the basic concepts of computer graphics in
general and IBM PC graphics in particular. Programmers who have already mastered
two-dimensional graphics techniques may wish to skim the rest of this chapter,
though skipping
it entertaining.
it entirely is not recommended—at the very least, I hope you'd find
~~
The images on a video display fall loosely into two categories: text and graphics. At
any given time, the video display of a PC-compatible microcomputer
either in text is
mode or graphics mode. A text mode display consists of alphanumeric characters,
while a graphics mode display is made up of pictures.
The distinction isn’t always that obvious, though. Sometimes what looks like text
is really graphics and what looks like graphics really text. The Microsoft Windows is
operating system, for instance, uses graphics mode to produce all of text output. its
(See Figure 1-2.) And the IBM character set (see Figure 1-3) includes graphics char-
acters that can be used to render crude pictures in text mode.
In fact, the two display modes have one important characteristic in common.
Whether intext or graphics mode, everything on the video display is made up of a
matrix of dots called pixels, short for “pictorial elements.” The pixel the atomic com- is
ponent out of which all computer images are constructed. If you look closely at the dis-
play screen of your microcomputer, you may be able to discern the individual pixels.
Program Manager
Eile Options Window Help
Paintbrush Terminal
2
Notepad
TT
Recorder
5Cardfile
O@
Calendar Calculator
(RE
DEERE] Print Manager Clipboard MS-DOS
Prompt
Figure 1-3 The IBM PC text mode character set. Notice the graphics
characters that can be used to construct a crude image
different characters. Any of these characters can be placed in any of 2,000 different
positions on the video display. But the individual pixels within the characters cannot
be altered, nor can a character be positioned on the display any more precisely than
the 2,000 available positions allow. Thus, the text characters must line up in neat rows
and columns across the display. (See Figure 1-4.)
When a PC is in graphics mode, on the other hand, the programmer has direct con-
trol over every pixel on the display. There is no predefined character set in graphics
mode. The programmer is free to invent pixel images and place them anywhere on the
video display.
Since graphics mode offers the programmer that kind of power, you might wonder
why any programmer would choose to work in text mode. The answer is
that graphics
"EE EEN 7
BEB
ha
HE BE BE BE
mode, while offering considerable power and freedom, isn't easy to use. If all you need
to do is display text characters on the video screen, it’s much easier to use the prede-
fined character set.
A game programmer really doesn't have that choice. Game buyers demand state-
of-the-art graphics when they purchase software, and there's no way to produce state-
of-the-art graphics in text mode. So for the rest of this chapter I'll look at the graph-
ics capabilities of the video graphics array (VGA), the graphics adapter sold with the
vast majority of PC-compatible microcomputers.
The VESA interface does make life easier for getting high-performance graphics out of
a multitude of different SVGA cards. Since the standardis well supported, any design-
er using VESA can be assured that the majority of users will have VESA drivers. Copies
of the standard can be obtained by writing to VESA, 2150 N. First Street, San Jose,
CA 95131-2020.
What are the differences among the standard VGA modes? Obviously, some of
them are text modes and some are video modes. We've already looked at the difference
between text modes and video modes. But what could be the difference between one
text mode and another text mode? Or between one video mode and another video
mode? In both cases, the answer is the same.
Figure 1-5
3(Ri
A
2 XL PREeintion
i text mode,
acrosscharacters fi
comparison
8
horizontally
a=
brograms relatively
the smal YE 1's
that use 1t.
— A
Build Your Own Flight Sim in C++
12 =
Graphics Primer
A m
"ou
Figure 1-7 The VGA sends a signal to the VGA monitor much like a
television station transmits a signal to your television set
Memory Addresses
Every memory circuit in the computer has an identifying number called an address,
which makes it easier for the numeric values stored in that circuit to be retrieved at a
later time. In a computer with 1 megabyte (or meg) of memory, there are 1,048,576
separate memory circuits, arranged in sequence so that the first circuit has address 0
and the last circuit has address 1,048,575. Usually, we represent these addresses in the
hexadecimal numbering system, which uses 16 different digits—from 0 to 9, plus the
first six letters of the alphabet—to represent numeric values. Although this might
seem to complicate matters somewhat, the mathematical relationship between hexa-
decimal and binary actually makes it simpler to represent numbers in this manner
than in decimal. In hexadecimal, the memory circuits in a 1-meg computer have
addresses 0x00000 to OXFFFFE (In C, the Ox prefix is used to identify a hexadecimal
number. Sometimes | will also use the assembly language convention of identifying
hexadecimal numbers with a trailing “h,” as in FFFFFh.)
Because of the way in which the IBM PCs memory is referenced by the earlier
processors in the 80X86 series, its traditional (and usually necessary) to reference
addresses in the PCs memory using two numbers rather than one, like this:
5000:018A. The first of these numbers is called the segment, the second is the offset.
To obtain an absolute address (that
is, the sort of address discussed in the previous
paragraph) from a segment:offset address, you multiply the segment by 16 and add it
to the offset. Fortunately, this is easy to do in hexadecimal, in which multiplying by
16 is similar to multiplying by 10 in decimal. To multiply a hexadecimal number by
16, simply shift every digit in the number one position to the left and add a 0 in the
14 8
A Graphics Primer
CLL
least significant digit position (i.e., at the right end of the number). For instance,
0x5000 multiplied by 16 is 0x50000, so the address 5000:018A is equivalent to the
absolute address 0x50018A.
If we want to store a number in the computers memory and retrieve it later, we can
store the number in a specific memory address, note that address, then retrieve that
number later from the same address (assuming we haven't turned the machine off or
otherwise erased the contents of memory in the meantime). And, in fact, this is exact-
ly what we are doing when we use variables in a high-level language
such as C. The
variable represents a memory address and the statement that assigns a value to the
variable stores a numeric value at that address. Of course, we never need to know
exactly what memory address the value was stored at; it’s the job of the C compiler to
worry about that. Even in assembly language, we generally give symbolic names to
assembler about what addresses those names
memory addresses and let the worry
represent.
Pointing at Memory
There are occasions, though, when we need to store a value in (or retrieve a value
from) a specific memory address, especially when we are dealing with video memo-
this with the
ry—which I'll discuss in more detail in just a moment. In C, we can do
value somewhere in the
help of pointers. A pointer is a variable that “points” at a
value
computer's memory. The pointer itself is equal to the address at
which the is
stored, but we can dereference the pointer using the indirection operator (*) to obtain
the value stored at that address. Here's a short program that demonstrates the use of
pointers:
#include <iostream.h>
void main()
{
int intvar;
int *pointvar=&intvar;
*pointvar=3;
cout << '"\n\nThe value of INTVAR is
" << intvar endl;
<<
char c;
cin.get(c);
}
The first line in the main() function creates an integer variable called intvar. The sec-
ond line creates a pointer to type int called pointvar and uses the address operator
(“&") to set this pointer equal to the address of intvar.
Build Your Own Flight Sim in C++
4% mu"
Finally, the program prints out the value of the variable intvar (to which you'll
Ew
notice we have not yet directly assigned a value), the value pointed to by
pointvar
(using the dereference operator), and the actual value of pointvar—that
is, the address
to which pointvar is pointing. So that the program doesn't end and return to Windows
or the Borland IDE, the last line waits for you to press a key. You could type this
pro-
gram and compile it with Borland C++ to find out what values are pointed out (or you
can read ahead and find out the easy way), but can you predict in advance what val-
ues each of these will have? (Don't worry if you can't figure out the address to which
pointvar is pointing, since this is arbitrary and unpredictable.)
When I ran this program, the results were as follows:
The
The
value of INTVAR
is 3.
value of *POINTVAR is 3.
The address at which POINTVAR
is pointing is 0x1 1ebOffe.
The address represented by the variable INTVAR
is 0x1 1ebOffe.
Notice that intvar has taken on the same value as *pointvar. That's because,
C++ is concerned, these are the same variables. When
as far as
we use the dereference opera-
tor in front of the name of POINTVAR, tells the compiler
it
A Two-Color Bitmap
In the simplest kind of bitmap, one that represents a video image containing only two
colors, the value of each bit in each byte
represents the state of a pixel in the image.
The zeroes usually represent pixels set to the background color while the ones
repre-
sent pixels set to the foreground color.
For instance, in Figure 1-9, there is a simple, two-color bitmap of the letter Z. This
bitmap consists of eight bytes of data, which would normally be arranged sequential-
ly in the computers memory. We have displayed it, however, as though the bits in
those bytes were arranged in a rectangle, with eight pixels on a side. Bitmaps are
almost always arranged in rectangular fashion on the video display.
20 m
A Graphics Primer
mer, but the results are of limited utility at best, as shown in Figure 1-10. The BGI will
set the VGAs graphics mode; set the color registers; even draw lines, squares, and cir-
cles on the screen. But its ability to perform animation is not great. The routines are
fairly slow and are not optimized for animation, either two-dimensional or three-
to
dimensional. That's not say that it’s impossible to perform animation with the BGI,
just that the results are not of commercial quality. I recommend that you use the BGI
for drawing charts and graphs, but not for flight simulation or fast arcade games.
Okay, suppose you don't own or don't want to use BC version 4.5? What kind of
problems will you encounter along the way? If you are programming on a Mac, you
will have to figure out how to implement our screen assembly language functions.
They are used throughout the book and the flight simulator adds some more assem-
bly. If you are programming for another compiler, such as Visual C++, you will have
to create your own project or make file, using the .mak files which accompany each
program. These make files have been created with the Borland Make Generation
option. Your C++ compiler should have the iostream classes, cin and cout. The
CONIO.H and DOS.H headers are frequently included. These contain several non-
ANSI functions which govern screen and keyboard interface. Again, adjustments
EB
might have to be made, but it is surprising how many nonstandard functions will be
the same. In all cases, there are no special Borland functions being used—everything
can be emulated. We have selected Borland because of its interface.
The VGA board contains a ROM chip with many common graphics routines built
in. The contents of this ROM chip are called the video BIOS. (BIOS is short for basic
input-ouput system. The video BIOS is a kind of extension to the IBM’ standard ROM
BIOS, which contains routines for performing nongraphic input and output.) In the
programs that we develop in this book, we'll call on the video BIOS to perform most
of our initialization. We will use it, for instance, to set the video mode and the color
registers. We will not be using it to draw images on the display, however. Like the BGI,
the video BIOS is slow and not optimized for animation.
of
Most our actual drawing in this book will be done by interacting directly with
the VGA hardware. After we've used the video BIOS to initialize the VGA board, we'll
place values directly into the video memory to create images. We'll look at how this is
done in Chapter 3.
We've looked at graphics in this chapter from the standpoint of the graphics hard-
ware—that is, as a sequence of bytes in the computer's memory that becomes
ic image when processed by the VGA board and output to the monitor. But there are
a graph-
other, more abstract ways of looking at graphics; these ways are often more useful. It
is these more abstract methods, which owe more than a small debt to mathematics,
that lead directly to three-dimensional graphics programming. We'll look at these
methods in Chapter 2.
22 =m
The Mathematics
of Animation
25
uid Your Own Flight Sim in C++
"EEE
Cartesian Coordinates
In the last chapter, we considered computer graphics from the viewpoint of the com-
puter hardware. We looked at ways in which the hardware could be induced to put
colored pixels on the computers video display. Anyone who wants to
program high-
speed, state-of-the-art computer games needs to understand how
computer graphics
work from a hardware perspective. But there is another way in which the
programmer
can, and occasionally must, look at the computer graphics display: as a slightly mod-
ified version of a Cartesian coordinate system.
Legend has it that the seventeenth-century French philosopher/mathematician
René Descartes was lying in bed one day watching a fly buzz around the ceiling when
he conceived the coordinate system that bears a Latinized version of his
name. It
occurred to Descartes that the position of the fly at any given instant could be
described with only three numbers, each representing the insect’s position relative to
some fixed point, as shown in Figure 2-1. Later, he expanded (or perhaps reduced)
this idea to include two-dimensional points on a plane, the position of which could
be described with only two numbers for any given point. He included the idea in a
long appendix to a book published in 1637. Despite being tucked away in the back
of another book, Descartes’ method of representing points on a plane and in
space by
numeric coordinates launched a new branch of mathematics, combining features of
geometry and algebra. It came to be known as analytic geometry.
iil
Figure 2-1 Descartes’ fly can be pinpointed at
any instant by three numbers
26 m
The Mathematics of Animation m "a.
The Cartesian Plane
You've probably run across (or bumped up against) the Cartesian coordinate system.
The
Even if you haven't, you're about to bump up against it now. Don't be afraid.
Cartesian coordinate system is simple, elegant, and surprisingly useful in certain kinds
of computer graphics. Because three-dimensional animation is one of the areas in
which
it is surprisingly useful, we're going to look at it here in some detail.
Any number can be regarded as a point on a line. In geometry, a
line (a theoretical
concept that doesn't necessarily exist in the
“real” world) consists of an infinite num-
ber of points. And between any two of those points there is also an infinite number of
points. Thus,
can still
a it
line is infinitely divisible. No matter how much you chop it up, you
chop up some more.
The system of real numbers is a lot like that too. Real numbers, roughly speaking,
fractional values—numbers with decimal points, in other
are numbers that can have
words. (C programmers will recognize real numbers as corresponding to the data type
known as float, about which I'll have more to say later.) There are an infinite number
of real numbers, both positive and negative. And between any two of those real num-
like a
bers, there are an infinite number of real numbers. The real number system,
line,is infinitely divisible.
Therefore, if we have a line that’s infinitely long (which is allowed under the rules
of geometry), we can find a point on it that corresponds to every real number.
Although in practice it’s not possible to conjure up a line that’s infinitely long or even
2-2.
infinitely divisible, we can make do with approximations like the one in Figure
This line, rather like a wooden ruler, has been marked off at intervals to denote the
remember seeing
integers—that is, the whole numbers—from -5 to 5. You probably
such lines in junior high school math class, where they were called number lines.
the
Traditionally, the numbers in such a line become smaller to the left and larger to
be extend-
right. We'll have to take it on faith that this number line can theoretically
ed an infinite distance in the positive and negative directions; there isn't enough paper
the points on
to draw the whole thing! However, we should have no trouble locating
this line for such numbers as -3, 5, 4.7, -2.1, and so forth.
27
—
“Build Your Own Flight Sim in C++
—
started dividing the line up this finely, we would
a
Of course, these are only the points on the line that have been indicated by
marks. Between these points are points representing even more
SEE
tick
precise fractions such
as .063, -4.891, 1.11111, and even -2.7232422879434353455. In practice, if we
surpass the resolution of the print-
er’ ink used to inscribe it on the page. So when we say that all of these
points are on
the line, we are speaking theoretically, not literally. But on a perfect, theoretical,
metric line, there is a point corresponding to every real number.
geo-
at the zero point on both lines. Together, these lines describe what is sometimes called
a Cartesian plane. Not only does every real number have a corresponding point on
both of these lines, but every pair realof numbers has a corresponding point on the
rectangular plane upon which these lines are situated.
For instance, the pair of numbers (3,1) corresponds to the point on the plane that
is aligned with the 3 tick on the horizontal number line and the 1 tick on the vertical
number line, as in Figure 2-5. The number pair (-4.2,2.3) corresponds to the point
that is aligned with the -4.2 tick on the horizontal line and the 2.3 tick on the verti-
cal number line, as in Figure 2-6. And so forth. (The first number in such a pair isthe
always the one aligned with a point on the horizontal line and the second number,
one aligned with a point on the vertical line.) Just as there are an infinite number of
each of which
points on a line, so there are an infinite number of points on a plane,
can be described by a different pair of numbers. A pair of numbers that corresponds
to a point on a Cartesian plane is called a coordinate pair or just plain coordinates.
Using pairs of coordinates, it was possible for Descartes to give a precise numeri-
cal description of hitherto vague geometrical concepts. Not only can a point be
described by a pair of coordinates,
the endpoints of the
a line can be described by two pairs of coordinates,
line. The line in Figure 2-7, for instance, extends
representing
from the point with coordinates (2,2) to the point with coordinates (4,4). Thus, we
also
can describe this line with the coordinate pairs (2,2) and (4,4). Other shapes can
be described numerically, as we'll see in a moment.
The two number lines used to establish the position of points on the plane are
called the axes. For historical reasons, coordinate pairs are commonly represented by
HE BE BE
EN 29
~~" Build Your Own Flight Sim in C++
"- =
"EEE
the variables x and y, as in (x,y). For this reason, the coordinates themselves are fre-
quently referred to as x,y coordinates. The first (horizontal) coordinate of the pair is
known as the x coordinate and the second (vertical) coordinate of the pair as the
y
coordinate. It follows that the horizontal axis (the one used to locate the
position of
the x coordinate) is referred to as the x axis and the vertical axis (the one used to locate
The Mathematics of Animation m
" -
the position of the y coordinate) is referred to as the y axis. The two axes always cross
all points are num-
at the zero point called the origin of the coordinate system, because
bered relative to this (0,0) point.
HE HE BE BE B®
31
yi
~~
ot Build Your Own Flight Sim in C++ 4 5m
5
"mE
the coordinates in this manner simplifies the task of calculating the video RAM
address of a pixel at a specific pair of coordinates on the display.
If the coordinates of the pixel in the
upper left corner of the display are (0,0), then
the coordinates of the pixel in the lower right comer of the mode 13h
display are
(319,199). Note that, because we start with coordinates (0,0) rather than (1,1), the coor-
dinates of the pixels at the opposite extremes
of the display are one short of the actual
number of pixels in the horizontal and vertical dimensions (which, in mode 13h, are
320 and 200, respectively). All other pixels on the display are at coordinates between
these extremes. The pixel in Figure 2-8, for instance,
is at coordinates (217,77)—that 16:
it’s located 217 pixels from the left side of
the display and 77 pixels from the top.
Most programming languages have a built-in command, or
a library function, for
changing the color of a pixel on the display. This command is usually called some-
thing like plot or set—in Borlands BGI graphics package its called putpixel—and
almost invariably the parameters passed to this command begin with the
screen coor-
dinates of the pixel whose color you wish to change.
simple formulas, depending on video mode. In mode 13h, the formula is particular-
ly simple. It is:
You'll see this formula pop up several times in the chapters to come.
33
a mE
EEE
it
of the pixel at coordinates (x,y), can be advanced to the address of the pixel at coor-
dinates (x+1,y) with the instruction, offset++. Similarly, it can be moved leftward to
the address of the pixel at coordinates (x-1,y) with the instruction, offset--.
Moving
the pixel location by one y coordinate in either direction is equally easy. In
mode 13h, the screen is 320 pixels wide, so moving down one line—that
is, one y
coordinate—is a matter of adding 320 to the value of the offset. If the variable offset
is equal to the address of the pixel at coordinates (x,y), then it can be advanced to the
address of the pixel at coordinates (x,y+1) with the instruction, offset+=320. You can
probably guess that we can move up one y coordinate from (x,y) to (x,y-1) by sub-
tracting 320 from the offset: offset-=320.
To move diagonally—that is, to change both coordinates—combine the two
actions. To move offset from (x,y) to (x+1,y+1), just add 321: offset+=321.
34m
The Mathematics of Animation
of
the fly to coordinates (5,7,0). After a move
in
two units the third direction—which,
for obvious reasons, we'll call the z direction—the fly will wind
up at coordinates
(5,7,2). Of course, most flies don't zip around with such neat regard for coordinate
axes. More likely, the fly went straight from coordinates (0,0,0) to coordinates (5,7,2)
in a single spurt of flight, then meandered off in a different direction altogether,
pos-
sibly plunging suicidally into Descartes’ cup of steaming tea.
We can't draw a three-dimensional coordinate system on a sheet of
paper. But we
can devise such systems in our minds. And, as we shall see in Chapter 8, we can cre-
ate them on the computer. In a three-dimensional coordinate system we call the three
axes x, y, and z. Typically, the x and y axes correspond to the x and y axes of a two-
dimensional graph while the z axis becomes a depth axis, running into and out of the
traditional two-dimensional graph, as shown in Figure 2-12. In Chapter 8, we'll look
at ways to make the axes of the three-dimensional graph correspond to directions in
our real three-dimensional world.
i6 m
The Mathematics of Animation
"En
of these shapes are easier to describe than others. Since this is going to be an impor-
tant topic throughout the rest of this book, let’s see just how we'd go about creating
this kind of coordinate representation.
From describing a line with two coordinate pairs representing the lines endpoints
it takes only a little leap of imagination to extend this concept to describe any shape
that can be constructed out of line segments—triangles (Figure 2-13), squares (Figure
2-14), even polygons of arbitrary complexity (Figure 2-15). Any shape made out of
an unbroken series of line segments can be represented as a series of vertices. In geom-
etry, a vertex is a point at which two lines intersect, though we'll use the term more
loosely here to include endpoints of lines as well. Since vertices are points in space,
they can be described by their coordinates. Thus, the square in Figure 2-14 could be
described by the coordinates of its vertices: (4,9), (4,3), (10,3) and (10,9). We'll use
this scheme to describe two-dimensional shapes in Chapter 6.
Not all shapes are made out of a continuous, unbroken series of line segments;
consider the square with an X inside it shown in Figure 2-16, for instance. Although
all the lines in this shape touch one another, they cannot be drawn with a single con-
tinuous line. If you doubt this, try drawing this shape without lifting your pencil
from the paper or retracing a previously drawn line. A more versatile system for stor-
ing shapes that would take such awkward (but quite common) cases into account
would consist of two lists: a list of the coordinates of the vertices and a list of the lines
that connect them. The list of vertex coordinates for a square with an X inside would
look exactly like that for the square in the last paragraph. The list of lines could con-
sist simply of pointers to the entries in the vertex descriptor list, indicating which
Figure 2-12
Three-dimensional
Cartesian axes
HE BE BE EE BN
37
build Your Own Flight Sim in C++
HH BE BE BR
BEB
vertices in the list represent the endpoints of a given line: (0,1), (1,2), (2,3), (3,0),
(0,2), and (1,3). This tells us that the first line in the shape connects vertex 0 (the
first vertex in the list in the last paragraph) with vertex 1, the second line in the shape
connects vertex 1 to vertex 2, and so forth. We'll use this scheme for describing
shapes in Chapter 8.
Three-Dimensional Vertices
Describing a three-dimensional shape made up of line segments is
done in exactly the
same manner as describing two-dimensional shapes, except that a third coordinate
must be included when describing each vertex, so that the vertex location in the z
The Mathematics of Animation
"Ew
dimension can be pinpointed. Although it’s difficult to do this on a sheet of paper, its
it.
easy to imagine In Figure 2-17, T have “imagined” a cube made up of line segments
within a three-dimensional coordinate system.
Shapes made out of line segments aren't especially realistic, but all of these con-
wonder, though,
cepts can be extended to drawing more complex shapes. You might
how something like a circle or a sphere can be described as a series of vertices con-
nected by line segments. Wouldn't the number of vertices required to describe such a
shape be prohibitively large, perhaps even infinite?
Well, yes, but there are two approaches that we can use to describe such shapes
within a coordinate system. One is to approximate the shape through relatively small
—
Build Your Own Flight Sim in C++
—————————————
8 line segments,is
a
line segments. How small should the line segments be? That depends on how realis-
is
tic the drawing intended to be and how much time you can spend on mathemati-
BREE
tion in a computer.
- -
Graphing Equations
Descartes came up with an alternative method of describing shapes such as circles and
spheres, one thatisnicely suited to microcomputer graphics even though Descartes
=Y
The letters x and y represent numeric quantities of unknown value. And yet, though
we do not know what the values of these numeric quantities are, we do know some-
thing important about the relationship between these two values. We know that they
are the same. Whatever the value of y
is, x has the same value, and vice versa. Thus,
if y is equal to 2, then x is also equal to 2. If y is equal to 324.218, then x is equal to
324.218.
In C++, on the other hand, the statement
xX
=Y;
would be an assignment statement. It would assign the value of the variable y to the
variable x. After this assignment, the algebraic equation x = y would be true, but it
wouldn't necessarily be true before this assignment statement. The difference between
an algebraic equation and an assignment statement, then, is
that the algebraic equa-
tion asserts that a relationship is true while the assignment statement makes it
true.
Heres another familiar algebraic equation:
x = 2y
This tells us that the value of x is two times the value of y. If y is equal to 2, then x is
equal to 4. If y is equal to -14, then x is equal to -28.
41
~~ Build Your Own Flight Sim in C++
"EEE
the value of x and the value of y. By treating these pairs of numbers as Cartesian coor-
dinates we can depict them as points on a two-dimensional Cartesian plane, thus
graphing the equation. The graph of the equation x = y is shown in Figure 2-20. The
equation has been solved for four values of y—0, 1, 2, and 3—which produces the
coordinate pairs (0,0), (1,1), (2,2), and (3,3). These coordinates have been graphed
and connected by lines. The lines, however, are not an arbitrary part of the graph.
They represent additional solutions of the equation for noninteger (i.e., fractional) val-
ues of y as well, such as 1.2, 2.7, and 0.18. All solutions of the equation between 0
and 3 fall somewhere on this line. Theres no way we could work out all of these frac-
tional solutions, since there’ an infinite number of them, but the graph shows these
solutions anyway, as points on an infinitely divisible line.
Similarly, the equation x = 2y produces a line when it is graphed, but the line has
or
a different angle, slope, from the line produced by the equation x = y. (We'll talk
more about the slope of the line in the chapter on wireframe graphics.) For any
straight line that we can draw on a graph, there is an equation that will produce that
line when graphed. That equation called, logically enough, the
is
equation of the line.
Not all equations produce straight lines, however. For our
purposes, the more
interesting equations are those that produce curves, since curves are so difficult to rep-
resent with line segments. It's more efficient for some purposes to represent a curve
with an equation, though not for all purposes. Solving an equation for various values
can take quite a bit of time, even when performed by computer, so it’s often faster to
approximate a curve with line segments.
42 =
The Mathematics of Animation m a om
Fractals
One type of shape called a fractal is often easier to produce using equations than by
storing vertices and line segments, simply because of the difficulty in storing such a
shape in the computers memory. A fractal—the term was coined by the mathemati-
cian Benoit Mandelbrot—is a shape that possesses the quality of self-similarity. This
simply (or not so simply) means that the shape isbuilt up of smaller versions of itself,
and these smaller versions of the shape are in turn built up of still smaller versions of
the shape.
is
A tree, for instance, a kind of fractal shape. A tree is made up of branches, which
resemble tiny trees. A branch in turn is made up of twigs, which resemble tiny branch-
es. And the twigs are in turn made up of twiglets, which . well, you get the idea.
. .
Transformations
In this book, we'll represent two-dimensional and three-dimensional shapes as ver-
tices connected by line segments. (Actually, we'll use line segments in the early chap-
ters, then graduate to connecting vertices with colored polygons to
create greater real-
ism in later chapters.) And we'll develop routines that will draw those shapes at spe-
cific coordinate positions on the video display. If we're going to animate these objects,
however, it’s not enough for us to create and display them. We must also be able to
~~ “Build Your Own Flight Sim in C++
"= = EE ER
manipulate them, to change their position on the display, even rotate the shapes into
brand new positions. In the language of 3D graphics, we are going to transform the
images.
One of the many advantages of being able to define a shape as a series of num-
bers—advantages for which all 3D graphics programmers should at least once a day
mutter a heartfelt thanks to the ghost of René Descartes—is that we can perform
mathematical operations on those numbers to our heart’ content. And
many genera-
tions of mathematicians and computer programmers have worked out the mathemat-
ical operations necessary for performing most of the transformations that we will wish
to perform on three-dimensional objects.
There are quite a few such transformations that we could perform, but we will con-
centrate on three: scaling, translating, and rotating. (Actually, when performed on
three-dimensional objects, there are five transformations because 3D rotation consists
of three separate operations, as you will see.) Scaling means changing the size of
an
object, translating refers to moving an object from one place to another within the
coordinate system, and rotating refers (obviously) to rotating an object around a fixed
point, through a specified axis, within the coordinate system. These are operations
you will perform in the programs developed in later chapters and the formulas for
performing them are widely known. It’s a simple matter to include these formulas in
a computer program. Making the computer code based on these formulas work quick-
ly and well is another matter, one that we'll talk about at length
inlater chapters. Note
it
that in all the formulas that follow, is assumed that the objects are represented by a
list of coordinate pairs or triples representing all vertices within the object.
new_z = z + tz;
Scaling an object is no more difficult from the programmer’ point of view, though
it may take the processor of a computer slightly longer to accomplish, since it involves
is somewhat
a multiplication operation. (Without a math coprocessor, multiplication
all of the
more time-consuming than addition.) To scale an object, we simply multiply
coordinates of all of the vertices by a scaling factor. For instance, to double the size of
an object, we multiply every coordinate of every vertex by 2. Actually, we are using
the term “double” very loosely here. This operation actually doubles the distance
between vertices, which would have the effect of quadrupling the area of a two-
dimensional object and increasing the volume of
tor of 8! triple
To the size of the object, we
a
multiply
three-dimensional object by
every coordinate by three.
aTo
fac-
cut
the size of the object in half, we multiply every coordinate by .5. Here is the general
formula for scaling the x,y coordinates of the vertices in a two-dimensional object:
*
new_x scaling_factor x;
new_y = scaling_factor * y;
For a three-dimensional object, the z coordinate of each vertex also must be scaled:
new_z = scaling_factor * z;
Rotating on X, Y, and Z
of
Rotation is the most complex of these transformations because it involves the use
the sine and cosine trigonometric functions. If you don't, as the song says, know
much about trigonometry, don't worry. You don't need to understand how the rota-
tion formulas work in order to use them. You don't even need to know how to per-
form the sine and cosine operations, because Borland has conveniently included func-
tions for doing so in the math package included with the BC++ compiler. To access
of your
them, you need only place the statement #include <math.h> at the beginning
the slow side, but they'll suffice for
program. Alas, these functions tend to be a bit on
~~ Build Your Own Flight Sim in C++
—EE CORI ER
HE BE BE BE B®
47
yd “Build Your Own Flight Sim in C++
#4 EEE mm
the radius half the diameter, there are 2*1 or roughly 6.28 radians
ference of a circle. Figure 2-23 shows how the in the circum-
degree and radians systems can both
describe a circle. When you rotate this book 360
degrees, you are also rotating it 6.28
radians.
The truth is, it doesn’t
matter which of these units we use to measure the rotation
of objects, as long as the sine and cosine routines that
we are using are designed to
deal with that type of unit. The sine and cosine routines
in the Borland library are
written to handle radians, so thats the unit of measure
we will use in the early chap-
ters of this book. Later, when we write our own sine and cosine
routines, we'll invent
48 m
Figure 2-23 A full circle consists of 360 degrees or 6.28
radians, depending on which system you are using
convenient for
a unit of measure, equivalent to neither degrees nor radians but more
our purposes.
Rotation Formulas
look at the formulas
Now that we have a unit of measure for object rotations, we can
of vertices (and
for performing those rotations. Since we'll be constructing objects out
the lines and polygons that connect those vertices), we'll rotate objects by rotating ver-
be represented by
tices. Since a vertex is nothing more than a point in space that can
a trio of Cartesian coordinates, we'll couch our
rotational formulas in terms of those
when
coordinates. In effect, a rotational formula is a mathematical formula that,
the new coordinates of that
applied to the coordinates of a point in space, will tell us
of degrees on one of the three
point after it has been rotated by a given number
(0,0) of the in which that point is
Cartesian axes around the origin point system
defined.
that
The best known and most widely applicable of these formulas is the one
since it is the only one needed for rotating two-
rotates vertices around the z axis,
dimensional shapes. Here is the formula for rotating the vertex of an object, or any
other point, around the origin of the coordinate system on the z axis:
new_x x * cos(z_angle) - y * sin(z_angle);
new_y y * cos(z_angle) + x * sin(z_angle);
is
where x and y are the x and y coordinates of the vertex before the rotation, z_angle
the angle (in radians) through which we wish to rotate the object around its z axis,
coordinate is
and new_x and new_y are the x and y coordinates of the object. (The z
rotation.) The cos() and sin() functions calculate the cosine and
not changed by the
- ~~
EA
Build Your Own Flight Sim in C++
sine of the angle, respectively. The center of rotation for this formula
gin of the coordinate system, which is why we defined the origin of our
tual book as the book’ center, since that greatly simplifies
the book using this formula (and the formulas that follow). To
x axis and its y axis. Here is the formula for rotating an object around the
coordinate system on the x axis:
new_y = y *
new_z = y *
cos(x_angle)
sin(x_angle)
—
+ z *
z *
sin(x_angle);
cos(x_angle);
Note that the x axis is not changed by this rotation.
EEE
always the ori-
rotating vir-
calculating the rotation of
rotate an object about
the origin of the cordinate system, we must perform these calculations
on the coordi-
Matrix Algebra
If we have a lot of vertices in an object—or,
even worse, more than one object—it’s
going to take a lot of calculations to scale, translate, and perform x, y, and rotations
z
on the coordinates of all of the vertices of all of those objects. Since the ultimate
goal
is to perform all of these operations in real time while
animating three-dimensional
objects on the video display of the computer, you might wonder if theres
enough time
to perform all of these calculations.
The answer is—maybe. Microcomputers are
surprisingly fast at this kind of calcu-
lation and I have a few tricks my sleeve for
up optimizing the way in which these cal-
culations are performed. Still, it would be nice if we had
some way of reducing the
calculations to be performed, especially
if
there were some way that the five transfor-
mations—scaling, translation, x rotation, y rotation, and z rotation—could be
per-
formed with a single set of operations instead of five sets.
As
ithappens, there is. Instead of performing the transformations with the formu-
las given earlier in this chapter, we can
perform them using matrix arithmetic which
is not as intimidating as it sounds. Matrix arithmetic is
a mathematical system for per-
forming arithmetic operations on matrices, which are roughly
equivalent to the
The Mathematics of Animation m
"ou
Building Matrices
in rows and
Matrices are usually presented as a collection of numbers arranged
columns, like this:
1 2 3 4
5 6 7 8
9 10 11 12
This particular matrix has three rows and four columns and
3x4
thus
of
is
referred to as a
int, like this:
3x4 matrix. In C++, we could store such a matrix as a array type
Multiplying Matrices
rules for matrix mul-
How do we multiply a vector times a matrix? There are special
it to say that if we
tiplication, but we really don't need to go into them
here. Suffice
float vector[11[3];
float newvector[11[31];
float matrix[31L31;
we can multiply vector and matrix together to produce newvector with this fragment of
code:
0sf 0 0
0 0sf
0 0 0
O
1
The Mathematics of Animation m m =m
This is called a scaling matrix. If we want to get even fancier (as we will, later on), we
can scale by a different factor along the x, y, and z axes, using this matrix:
sx 0 0 O
0O0sy 0 O
0 0sz O
0 0 0 1
where sx scales along the x axis, sy scales along the y axis, and sz scales along the z
axis. Thus, if we wish to make an object twice as tall but no larger in the other dimen-
sions, we would set the sy factor equal to 2 and the sx and sz factors equal to 0.
We can also perform translation with matrices. The following matrix will scale a
coordinate vector by the translation factors tx, ty, and tz:
1 0 0 O
0 1 0 O
0 0 1 0
tx ty tz 1
00 01
When you want to perform several of these transformations on the vertices of one
or more objects, you can initialize all of the necessary matrices, multiply the matrices
together (that is, concatenate them), and then multiply the master concatenated
matrix by each vertex in the object or objects. How do you multiply a 4x4 matrix by
another 4x4 matrix? Here's a fragment of C++ code that will do the job, multiplying
matrix] by matrix2 and leaving the result in newmatrix:
wu
~~ Build Your Own Flight Sim in C++
In addition to the transformation matrices given above, there's one more matrix that
will come in handy in the chapters to come. Although it may seem silly to do so, there
will be times when we need to multiply one matrix by another matrix and leave the
first matrix unchanged—that is, the resulting matrix will be the same as the first
matrix. If so, we must multiply the matrix by the identity matrix, which looks like
this:
1 0 0.00
0
0
1.0
0 1 0
0 0 0 1
The identity matrix is simply the matrix in which the main diagonal—the diagonal
strip of numbers running from the upper left corner of the matrix to the lower right
corner—consists of 1s and all the other numbers are 0s. Any matrix multiplied by the
identity matrix will remain unchanged by the operation.
Floating-Point Math
Before we leavethis discussion of useful mathematics, let's look at how the computer
handles math operations. As you're probably aware, the C++ language offers several
numeric data types. For our purposes, the most important of these are the int type and
the float type. Numeric data declared as int, which is short for integer, consists of
whole numbers, usually in the range -32,768 to 32,767. Numeric data declared as
float, which is short for floating point, includes whole numbers in the int range but it
can also include fractional values and numbers over a wider range. We can modify
these types in several ways. In particular, we can substitute the long type for the int
type, which gives us a much wider range of whole numbers. Or we can define type
int as type unsigned int, which gives us a range of whole numbers from 0 to 65,535
rather than from -32,768 to 32,767.
The sort of mathematical operations performed in three-dimensional animation
programs usually require the floating-point data type because they use fractional val-
ues. The functions that C++ uses to perform mathematical operations on floating-
point data can be quite slow, however, especially on a machine that doesn’t have a
math coprocessor installed. So, we'll avoid the floating-point type whenever possible.
Nonetheless, by way of illustration, we'll use float data in many of the early programs
that demonstrate three-dimensional transformations and animation. You'd best be
54m
The Mathematics of Animation
"Ew
warned now that these programs will be rather slow. Later, we'll rewrite this code
using the long data type. This requires some clever rewriting of our programs, though,
and so the better part of a chapter will be devoted to this process. In the meantime,
just grit your teeth and bear with the sometimes excruciating slowness of floating-
point code—unless you have a math coprocessor, in which case you can smile benev-
olently down on those who don't.
Painting
in 256 Colors
Unless you live in the zebra house at the 200, when you look around
you'll see colors, lots of colors. According to some estimates, the human eye can dis-
tinguish more than 16 million different colors, and there’s probably a lipstick shade
named after every one of them. Without color, life would be, well, colorless. So would
computer graphics.
Color is the single most important thing that a computer graphics programmer
needs to be concerned with. As we saw in Chapter 1, a computer video display is
nothing more (or less) than a matrix of pixels. The only thing that distinguishes one
it
pixel from the pixel next to is color. Computer graphics programming is a matter of
changing the colors of the pixels on the display in such a way as to create interesting
and meaningful patterns. Once you've learned to do that in a clean and efficient man-
ner, you'll know everything you need to know about graphics programming. As you
might guess, thats more easily said than done.
which each of those pixels can be displayed. The first of these attributes is called res-
olution; the second is sometimes referred to as color depth.
Both of these attributes are important, but frequently we must choose to make one
attribute more important than the other. As programmers, we generally want a large
number of pixels on the screen in a great variety of colors. However, as the Rolling
Stones once said, you can't always get what you want. The VGA adapter often forces
us to trade off one attribute for another. Generally speaking, the more pixels we can
squeeze onto the screen in a given mode, the fewer colors we're allowed. So we have
to decide which is more important to us: resolution or color depth.
Game designers usually opt for color depth over resolution. The standard VGA
graphics adapter, which we are using for our graphics in this book, supports resolu-
tions that can display pixel matrices as large as 640x480, for a total of 307,200 pixels
on the screen at one time. But when working at these resolutions, we're only allowed
to show those pixels in 16 colors. Ironically, this lack of color depth can make the dis-
play look lower in resolution than it actually is. A block of high-resolution pixels in a
single color tends to look an awful lot like one big pixel.
By lowering the resolution to 320x200, for a total of 64,000 pixels on the screen,
we gain the ability to display pixels in 256 colors, an exponential leap in color depth.
The pixels in 320x200 mode look rather large and blocky, but the extra color depth
more than compensates for the loss of resolution. Despite the newer video cards and
higher resolutions, more than 90 percent of the many commercial games on the mar-
ket today still rely on the 320x200x256 color mode, designated mode 13h by the ROM
BIOS. This is due to the wealth of libraries and routines which already have been writ-
ten for it, its satisfactory speed on slower (386) machines, and its buffer size in use of
dynamic RAM memory. In Chapter 1, soft modes, such as 640x480x256, were men-
tioned as the king of the marketplace. Generally, these modes start with mode 12h or
13h and adjust registers to achieve their resolution and color depth. For learning about
video programming, the 320x200x256 mode 13h is great.
And it must end with the name of the procedure followed by the assembler directive
ENDP, (which tells the assembler that the procedure is over):
AsmFunction ENDP
Between the PROC and ENDP directives you place a series of assembly language
instructions, each of which occupies a single line of the assembly language source file.
These instructions alternate with occasional assembler directives—the names of
which are usually written in uppercase letters—and with comments, which must be
preceded by a semicolon (;). (The semicolon in assembler works exactly like the dou-
ble slash (//) in C++, causing the assembler to ignore everything until the next carriage
return.)
Each instruction consists of a mnemonic, which represents a CPU operation, fol-
lowed optionally by one or more operands indicating what memory locations are to
be operated upon. The mnemonic is usually a three-to-six-letter word, such as MOV
or PUSH or ADD. Here’ a typical assembly language instruction, followed by a com-
ment:
add ax,[length]l ; Add AX and LENGTH, leaving sum in AX
Most of the assembly language instructions in the PROC will concern themselves
with moving numeric values around in the computer's memory and performing
~~ Build Your Own Flight Sim in C++ "EE EER
mathematical and logical operations on those values. Often, these instructions refer-
ence a special set of memory locations situated in the computer CPU itself. These
memory locations, or CPU registers, can be considered a set of permanent integer vari-
ables. The registers we will be concerned with are named AX, BX, CX, DX, SI, DI, BP,
SP, ES, DS, CS, and SS. Four of these registers—AX, BX, CX, and DX—can be broken
in two and treated as pairs of 8-bit variables. AX becomes AH and AL (with AH the
high byte of AX and AL the low byte), BX becomes BH and BL, CX becomes CH and
CL, and DX becomes DH and DL. The CPU registers are diagrammed in Figure 3-1.
Actually, we will need to use two CPU registers to point to a memory location, with
one register holding the segment portion of the address and another holding the off-
mov cx,size
where size is a name that we've assigned to a 16-bit memory location.
Values can be copied not only from memory locations, but from other registers and
even from the instruction itself. A value copied from an instruction is called an imme-
diate value and is referenced like this:
mov dx,1040h
This copies the value 1040h (the “h” indicates that the value
the instruction into the DX register.
is in hexadecimal) from
it
When using assembly language pointers, is sometimes necessary to specify what
sort of value the pointer is pointing at. The three types of values recognized by 80X86-
series CPUs are BYTEs (8-bit values), WORDs (16-bit values), and DWORDs (Double
WORDS, or 32-bit values). Thus, a pointer is either a BYTE PTR, which points at a
BYTE value; a WORD PTR, which points at a WORD value; or a DWORD PTR, which
points at a DWORD value. For instance, these instructions copy a WORD value from
the address pointed to by ES:BX to the AX register:
BEB
Of course, the assembler can also figure out that [es:bx] must be a word pointer
because you want to move the value stored there into a 16-bit register. But there will
be instances when the size of the value is less obvious. And it doesn't hurt to make it
explicit anyway, if only to make your code clear to somebody else reading it.
There are special assembly language instructions for putting pointers into registers.
An assembly language pointer actually stretches across two registers, with either ES or
DS holding the segment part of the pointer and another 16-bit register holding the off-
set. Does this mean that we must use two MOV instructions to get the pointer into
these two registers? Fortunately not. The 80X86 instruction set (that is, the set of
instructions that can be executed by CPUs in the 80X86 series) includes special
instructions for loading pointers into a pair of registers. The two instructions that will
be used in the programs in this book are LDS and LES. The first loads a segment into
the DS register and an offset into a second register. The other does the same with the
ES register. For instance, if we wished to load the pointer stored at the address we've
named old_pointer into the ES:DX registers, we could write
Les ds,old_pointer
64 BH
Painting in 256 Colors
certain way. For instance, the instruction CMP AX,DX would compare the values in
m "wu
ene eee
the AX and DX registers. This could then be followed by a JZ instruction, which
would cause the processor to “jump” to a new address if the zero flag is set—i.e., if
the values in AX and DX are equal. This is logically equivalent to the C++ instruction:
if (ax==dx).
Perhaps the fanciest of these “conditional jump” instructions is LOOP, which sub-
tracts one (decrements) from the value in the CX register and jumps to a certain address
if CX doesn't then equal zero. Variations on this instruction, such as LOOPNE and
LOOPZ, can be made to check the value of a flag and terminate the loop when a cer-
tain flag setting is detected, even if CX has not yet been decremented all the way to
zero. These instructions can be used to produce structures similar to the high-level
While and for loops.
Occasionally, assembly language procedures must call other procedures, the way
that C++ functions call other functions. This is done via the CALL instruction, which
must be followed by the name of the procedure being called, like this:
call other_procedure
The assembler translates the name of the procedure being called into the actual
address at which the code for that procedure resides in memory.
Finally, every assembly language procedure must end with a RET instruction,
which causes the CPU to return control of the program to C++ when the assembly
language procedure is complete (equivalent to the C++ return instruction, as well as
to the final curly bracket (}) at the end of a C++ function), and the ENDP (end)
directive.
There is a great deal more to know about assembly language and we can only
scratch the surface in this book. To avoid confusion, I'll explain the few relevant
assembly language concepts as they arise. For now, we will mention only one more:
how to pass parameters from C++ to assembler.
organizes those parameters on the stack. But Turbo Assembler (and most other
Microsoft-compatible assemblers for IBM-compatible computers) provides a simple
method of accessing parameters: the ARG directive.
No, ARG isn’t what you say when you stub your toe against your computer desk—
it is a directive that tells the assembler which parameters you expect to be passed from
C++, the order in which those parameters will be passed, and the number of bytes that
each parameter will occupy. In return, the assembler allows you to refer to those para-
meters by name rather than by their location on the stack. The ARG directive works
like this:
ARG first_param:BYTE, second_param:WORD, third_param:DWORD;
This tells the assembler to expect three parameters—first_param, second_param,
and third_param—to be passed to the procedure from C++. It also tells the assembler
how much memory each of these parameters will occupy. The first will occupy 1 byte
(as indicated by the word BYTE attached to the name of the parameter by a colon),
the second 2 bytes (or one WORD), and the third 4 bytes (or one DWORD). In prac-
tice, BYTE is not used as ARG size for assembly language routines which will interface
with C++ since the C++ compiler performs automatic adjustment (called promotion)
of chars to ints in function calls. Since assembly language doesn’t demote the int back
to char, this could cause misalignment of successive parameters and leave us scratch-
ing our heads with another bug to hunt down. It is best to restrict sizes to WORD and
DWORD when writing assembly code for C++. All assembly procedures in this book
confine themselves to these ARG sizes.
Once the ARG directive has been placed at the beginning of the procedure, just
after the PROC directive, you can refer to these parameters by name. The one catch is
that the following pair of instructions must be included at the beginning of any pro-
cedure that uses the ARG directive:
push bp
mov bp,sp
Place these two instructions before any other PUSH instructions are executed. These
instructions set up the BP register as a pointer to the stack area where the parameters
are stored. The assembler will translate all references to these parameters into point-
ers using the BP register. For this reason, you should never use the BP register in any
procedure that has an ARG directive.
Near the end of the procedure, after all other POP instructions have been execut-
ed, include the following instruction:
pop bp
This restores the BP register back to the value it was previously storing. (Most likely,
the C++ function that called the assembly language procedure was also using the BP
register as a pointer to parameters stored on the stack, which is why it is important
that the value be returned intact at the end of the procedure.)
Painting in 256 Colors m
LL
Is it possible for an assembly language procedure to return a value the way that a
C++ function can? Sure it is. An assembly language procedure can do anything that
a C++ function can (and occasionally a bit more). The way in which a value is
returned from an assembly language procedure depends on what type of value it is.
For instance, values of type int (or any other 16-bit values) are returned in the AX reg-
ister: the value must be placed in the AX register before the procedure terminates.
Larger and smaller values are returned using similar techniques.
And now, with the assistance of these few assembly language procedures, we can
consider how to access the video BIOS.
HE BE BE EH B=
67
~~
a Build Your Own Flight Sim in C++
mE
EE EER
Executing software interrupt number 10h causes the computer to look in the memo-
ry location that contains the address of the video BIOS code. This in turn causes the
computer to begin executing that code. However, simply executing the INT 10h
instruction doesn't tell the video BIOS what you want it to do. You must also pass it
additional information, by placing that information in the CPU registers.
The video BIOS is capable of executing several different functions. Each function
is identified by a number. You place the number of the desired function in CPU reg-
ister AL before calling the video BIOS. The function that sets the video mode is func-
tion 0, so to call it you place a 0 in the AL register before executing interrupt 10h. It
is necessary to tell this function which video mode you wish to set, so you must also
place the mode number in the AH register. In assembly language, all of those things
can be done in three instructions, like so:
mov al,0 ; Put function number (0) in register AL
mov ah,13h ; Put mode number (13h) in register AH
int 10h ; Call video BIOS at interrupt 10h
In the course of this book, we'll put together a short library of assembly language
procedures that we can link to our C++ code to support the basic screen operations
we'll need in our programs. Since calling the ROM BIOS
assembly language, we'll include a procedure for setting
is
the
a task best performed in
video mode, equivalent
to the C++ function shown earlier. The procedure function, callable from C++, is in
Listing 3-1.
_setgmode PROC
ARG mode : WORD
push bp ; Save BP
mov bp, sp ; Set up stack pointer
mov ax,mode ; AL = video mode
mov ah,0 ; AH = function number
int 10h ; Call interrupt 10h
pop bp ; Restore BP
ret
_setgmode ENDP
As you can see, this procedure uses several of the techniques we talked about ear-
lier in this chapter. The ARG directive tells the assembler that the procedure expects
a single 16-bit parameter, referred to as mode. The first two instructions set up the
stack area so that this parameter will be correctly referenced by the assembler. The last
two instructions restore the BP register and return to the calling function. In between,
INT 10h is called after loading mode into CPU register AX and function number zero
Painting in 256 Colors
CLL
into AH. The only thing that may be confusing is why the mode value is MOVed into
AX rather than just AL. We do this because mode will be passed as a parameter of type
int from C++ and we might as well load the entire 16-bit integer into AX, since only
the low 8 bits of the parameter will be used (there being no video modes with num-
bers greater than 255). Thus, the mode number still winds up in the AL register,
where it belongs.
This procedure can be found in the file SCREEN.ASM on the included disk. We'll
talk about more of the procedures in that file in this and later chapters. To use the pro-
cedure in that file from a program being developed in the Borland Integrated
Development Environment (IDE), put SCREEN.ASM (or SCREEN.OBJ, if you've
already compiled it to an object file) into your project window and include the file
SCREEN. H at the top of each module of your program that requires the routines. The
file SCREEN .H contains C++ prototypes for all of the assembly language procedures
in SCREEN.CPP, showing the compiler how these procedures are to be called as C++
functions.
This particular procedure can be called from C++ like this:
setgmode (0x13);
This example would set the video mode to mode 0x13, the 320x200x256 color
graphics mode.
Be warned: When you set the video mode it stays set. When the user exits the pro-
gram, the video display will still be in whatever mode you've switched it to, which can
make a mess of the screen. As a matter of courtesy and professionalism, you need to
save the previous video mode before setting a new mode so that you can restore it on
exit from the program.
You'll notice that in one statement we've both declared an integer variable and set
it equal to the value at a memory location. This is legal in C++, where we can declare
a variable at the time of first use, rather than in a block of declarations at the head of
a function. The variable will remain in scope until the end of the block in which it is
~~" Build Your Own Flight Sim in C++
— EA EE EE
declared. (A program block in C is a sequence of program lines that begins and ends
with curly brackets.) If we declare this variable at the beginning of the main() func-
BR——
And that’s all there is to setting the PC's video mode. Which leads us to
a far more
important question: Now that we've set the video mode, what do we do with it?
70 =
Painting in 256 Colors
EE
Figure 3-4
Video memory as
bytemap in
256-color mode
normally organized into individual bytes. In mode 13h, video memory is not so much
a bitmap as a bytemap, though I'll continue referring to it here as a bitmap. Figure 3-
4 illustrates this arrangement. The actual pixel color represented by a given byte value
in the bitmap depends on the current setting of the VGA color registers, which I'll dis-
cuss in a moment.
represents the color of the pixel to the immediate right of that pixel—and so on, for
the first 320 bytes of the bitmap. The 320th byte, at location A000:013F, represents
the pixel in the upper right corner of the display. The bitmap continues with the sec-
ond row of pixels, with the next byte (at location A000:0140) representing the pixel
immediately beneath the pixel in the upper left corner of the display. The byte at
A000:0141 represents the pixel to the immediate right of that one, and so on.
There are 64,000 pixels on the mode 13h display; therefore the bitmap for the dis-
play is 64,000 bytes long, continuing from address AO00:0000 to address AOOO:FOBFE
As you've probably already guessed, the last byte of the bitmap (at AOOO:FOBFF) rep-
resents the color of the pixel in the bottom right corner of the display.
72m
Painting in 256 Colors m mm
(Obviously, you can use the name of your choice for this array in your own programs.)
For instance, to
set the pixel in the upper left corner of the mode 13h display to color
167, you would use this statement:
screenl0] = 167;
Is that simple, or what?
_cls PROC
ARG screen:DWORD
push bp ; Save BP
mov bp,sp ; Set up stack pointer
push di ; Save DI register
les di,screen Point ES:DI at screen
mov cx,SCREEN_ WIDTH/2*SCREEN_ HEIGHT ; Count pixels
mov ax,0 ; Store zero values..
rep stosw ; «...in all of video memory
continued on next page
~~" Build Your Own Flight Sim in C++
pop di ; Restore DI
pop bp ; Restore BP
ret
_cls ENDP
Let’ take a look at what this procedure does and how it does it. At the head of the
procedure is a pair of EQU statements. EQU is an assembler directive similar to the
CONST instruction in C++ (or to the #define preprocessor directive that C++ inherit-
ed from C). It sets a symbol equal to a value. In this case, it sets the symbol
SCREEN_WIDTH equal to 320 and the symbol SCREEN_HEIGHT equal to 200.
These symbols can now be used in our program in place of these values.
The ARG directive tells us that this procedure receives a single parameter—a point-
er to the video display that we've called screen. If video memory is always at the same
address, why does this procedure need a pointer to its location? There will be times
when it is necessary to assemble the video image in a special memory buffer, and then
move the completed image to video RAM after
nique will be
it is complete. In fact, this very tech-
used in the program that we develop in the next chapter. On such occa-
sions, the cls() function will be passed the address of the offscreen buffer, rather than
the address of video memory, so that the offscreen buffer can be cleared instead of the
display. You'll notice that the same technique of passing a pointer to the screen is used
in many of the screen-oriented functions we'll develop later.
The first three instructions of the procedure set up the BP register as a pointer to
the parameters on the stack and save the DI register. (None of the other registers,
except for BP, need to be saved.) The LES DI,SCREEN instruction loads the full 32-bit
screen pointer into the ES:DI register pair.
The next instruction, MOV CX,SCREEN_WIDTH/2*SCREEN_HEIGHT, may look
a little odd to you, since it includes such high-level operators as the division (/) and
the multiplication (*) operators. These are actually assembler directives. The object of
this instruction is to copy the total number of pixels on the display, divided by 2, into
the CX register. Instead of calculating this number ourselves, we use the two symbols
we created earlier to calculate this value. The instruction that will actually be assem-
bled here is
mov CX,32000
In a moment
register.
it
will become clear why we are loading this value into this particular
Next, a value of zero is copied into the AX register and a REP STOSW instruction
is executed. What is
a REP STOSW instruction? It is one of the 80X86 string instruc-
tions, which are actually extremely tight and fast assembly loops for manipulating
strings of data in the computers memory. The instruction creates a string of identical
16-bit numbers in a sequence of memory addresses starting with the address pointed
to by the ES:DI registers. The count of 16-bit numbers in the string is whatever has
74 ®
Painting in 256 Colors
Ew
been stored in the CX register and the actual 16-bit number placed in that sequence
of addresses is the number in the AX register. In this case, the REP STOSW instruc-
tion will store 32,000 16-bit zero values in the address we have passed in the para-
meter screen. Since we are storing 16-bit values rather than 8-bit values, that’s equiv-
alent to 64,000 addresses, precisely enough to fill up video memory, or an offscreen
buffer, with zeroes, effectively clearing it. This procedure is called from C++ like this:
cls(screen_address);
where screen_address is a far pointer to an array of type char.
We wrote this function in assembly language rather than C because there may be
times when we need to clear the display in a hurry and the STOSW instruction is gen-
erally faster than a for loop in C. How much faster? That depends on which model of
80X86 processor the program is running on. On the earlier processors in the 80X86
series, such as the 8088 and 8086, string instructions were much faster than alterna-
tive forms of loops. On later processors, such as the 80286 and 80386, the string
instructions are still faster, but by a smaller margin. On the 80486 processor, there are
alternative loop forms that are as fast or faster than the string instructions, so writing
such a routine in machine language becomes less important on the 80486, though we
have no guarantee that our C compiler is actually producing the most efficient possi-
ble machine language translation of our loop. Until most game players own machines
based on the 80486 processor, it will be advantageous to use string instructions such
as STOSW for potentially time-consuming operations like clearing the screen. Of
course, at the rate things are going, it may not be long before this event comes to pass.
HE BE BE EE B=
75
Build Your Own Flight Sim in C++
"EE
EEE
76 ®
Painting in 256 Colors
mm > | >
_setpalette PROC
ARG regs:DWORD
push bp ; Save BP
mov bp,sp ; Set up stack pointer
Les dx,regs ; Point ES:SX at palette registers
mov ah,10h ; Specify BIOS function 10h
mov al,12h ; ...subfunction 12h
mov bx,0 ; Start with first register
mov cx,100h ; Set all 256 (100h) registers
int 10h ; Call video BIOS
pop bp ; Restore BP
ret
_setpalette ENDP
To change all 256 registers to the color values in the char array colregs, you would
write
setpalette(color_regs);
where color_regs is a far pointer to an array of char containing the color descriptors.
but
And that’s all there is to setting the VGA palette. Well, okay, maybe
it’s
it
isn't that easy,
conceptually simple. Creating an array with 256 color descriptors in it isn't as
trivial as I've made it sound, but there is an easy way to do it.
77
. —
Build Your Own Flight Sim in C++
program appears
as RAINBOWEXE,
SW Listing 3-4
//
//
RAINBOW.CPP
A
program
in Listing 3-4.the(This
located in
—
program is available on the
BYOFS\GENERAL
RAINBOW.CPP
palette
ESE BE
EEE
accompanying disk
B——
#include <stdio.h>
#include <conio.h>
#include <dos.h>
#include "screen.h"
void main()
{
setgmode(oldmode) ;
78 ®m
Painting in 256 Colors m
NNN
m m
First, we clear the screen by calling the cls() function in SCREEN.ASM, and then
we set the video mode to 13h by calling the setgmode() function (from the same file).
To obtain access to video RAM, we create a far pointer called screen that points to the
start of vidram.
Then we use a pair of nested for loops to access vidram. The first loop uses the
index i to iterate through all 200 lines of the video display, while the second loop uses
the index j to iterate through the 320 pixels on each line. The statement
screen[i*320+j]=j calculates the position of the next pixel on each line and sets it
equal to the value of j. The result is a series of 256 vertical stripes from the top of the
display to the bottom, each in one of the 256 default palette colors. The output of the
program is shown in Figure 3-7, albeit in shades of gray rather than color. Not only
is the result rather pretty, but it shows you what the default VGA palette colors look
like. Because there are only 256 colors in the VGA palette, we've only colored the first
256 vertical lines on the VGA display. If this strikes you as a bit unbalanced, you can
color the entire display by changing the second for loop to read
for (int j=0; j<320; j++);
Now the first 64 colors will be repeated as the last 64 stripes on the display.
What we've done here is to create a bitmap in video RAM in which each of the 256
VGA colors is represented in a repeating pattern. Now lets look at a way to create
bitmaps that are a little more complex.
Storing Bitmaps
It’sgood to know how to create a bitmap in video RAM, but if
that bitmap doesn’t con-
tain a picture of something, it doesn't help a lot. Painting the entire VGA palette on
the display as a series of multicolored vertical lines is an interesting exercise, but it
doesn’t do us much good in creating computer games. So how do you go about cre-
ating the bitmap?
ee
Build Your Own Flight Sim in C++
— "EE
You need a program that allows you to paint images on the screen of the comput-
er and store them on the disk as bitmaps. There's not enough space in this book to
develop such a paint program and its a bit far afield from our subject matter.
Fortunately, there are a number of excellent paint programs available commercially.
The artwork used in programs in this book was developed with a program called
Deluxe Paint II Enhanced (DP2E) from Electronic Arts. I highly recommend this pro-
EB
gram, though there are a number of other programs on the market of equal worth.
Figure 3-8 shows a typical screen from DP2E. If you are serious about developing
computer games of commercial quality, you should consider purchasing such a pro-
gram. And if you plan to use the paint program with any of the utilities we create in
this chapter, make sure the program is capable of generating PCX files (which I'll dis-
cuss later in this chapter) or that you have a utility that will convert other files to PCX
files. (Many paint programs come with such conversion utilities.)
Compressing Bitmaps
In what form do paint programs store bitmaps on the disk? You might guess that a
paint program would store an image on the disk as a 64,000-byte file containing a
byte-by-byte bitmap of the image. This, however, is rarely the case. Disk space
ited and a straightforward bitmap is rarely the most efficient way to store an image.
is lim-
>
m =m
0,0,0,00, oe 0,00
.B
Uncompressed Storage
eyCompressed Storage
We then
run. For instance, we could store our 45s as a pair of numbers: 100 and 45.
use a decompression program later to expand these bytes back into their original
form. Figure 3-9 illustrates the translation of runs of bytes in a file into RLE pairs,
depicting a cat.
It's not hard to imagine a file containing a bitmap that consists of nothing but pairs
of bytes like the above. For instance, if we wished to store a bitmap that consisted of
a run of 37 bytes of value 139, 17 bytes of value 211, and 68 bytes
of value 17, we
could create
a file consisting
extremely efficient way to
of only 6
store a bitmap!
bytes: 37, 139, 17, 211, 68, and 17. Thats an
Actual bitmaps, however, don’t compress quite this easily. Although the typical
bitmap created with a paint program (as opposed to a bitmap created by digitizing a
photograph, for instance) may contain many runs of single-colored pixels, bitmaps
also frequently contain sequences of differently colored pixels. RLE is not a particu-
of 6 bytes
larly efficient way to encode single byte values. For instance, if a sequence
in a bitmap consisted of the values 1, 2, 3, 4, 5, and 6, an RLE scheme would store
them as 1,1,1,2,1,3,1,4,1,5,1,6, where each byte is encoded as run
a of only a single
value. This actually doubles the amount of disk space necessary to store these bytes!
What we want is a way to turn RLE off and on in the file as we need it, so that we
can store some bytes as a straightforward bitmap and use RLE compression on
other
uid Your Own Flight Sim in C++ — &
".e
bytes. When we encounter a run of multiple bytes of the same value in a file, we can
use RLE to store it. But when we encounter a sequence of dissimilar bytes, we can turn
RLE off.
We could invent our own scheme to do this, but theres
| no reason to do so—not
quite yet, anyway—because there are a number of existing methods that do the job
well enough. (However, we'll still need a compression scheme in some
cases to han-
dle the data once it has been loaded from the disk file.)
|
Although we'll make little use of the PCX header in our programs, we can define
as a C structure so that we can access the individual fields within the header should
it
that become necessary. Heres a C structure for the PCX header:
|
struct pcx_header {
char manufacturer; // Always set to 0
82 m
Painting in 256 Colors
"Ew
char version; // Always 5 for 256-color files
char encoding; // Always set to 1
char bits_per_pixel; // Should be 8 for 256-color files
int xmin,ymin; // Coordinates for top left corner
int xmax,ymax; // Width and height of image
int hres; // Horizontal resolution of image
int vres; // Vertical resolution of image
char palette16L48]; // EGA palette; not used for
// 256-color files
char reserved; // Reserved for future use
char color_planes; // Color planes
int bytes_per_Lline; // Number of bytes in 1 Line of
// pixels
int palette_type; // Should be 2 for color palette
char filler[581; // Nothing but junk
};
Most of the members of this structure are irrelevant for our purposes, though man-
file and not
ufacturer should always be 0 (indicating that we are dealing with a PCX
some other type of file) and version should be 5 (telling us that the file supports 256-
color graphics).
You'll notice that the header data do not require that the size of the PCX bitmap be
a full screen. There are fields within the header (xmin and ymin)
that can be used to
define an upper left coordinate for the image other than 0,0 and fields (xmax and
This
ymax) that can define a size for the image other than the size of the pixel matrix.
allows us to create PCX images that consist of a single rectangle plucked from a larg-
However, as we noted earlier, we're going to ignore these data fields for now
er image.
and work only with PCX files containing a full screen image. 1f you try to use the PCX
routines developed in this chapter to load an image that doesn't start in the upper left
corner of the display and fill the whole screen, you'll get decidedly odd
results.
A PCX Structure
Once a header structure is defined, itcould be incorporated into a larger structure that
can hold a complete PCX file, header and all:
struct pcx_struct {
PCX Limitations
This raises a couple of interesting questions: Most obviously, how does the PCX for-
mat encode bytes of an uncompressed bitmap that are in the
range 192 to 255?
Wouldn't these bytes be mistaken for run-length values? Yes, they would, so the PCX
encoding scheme (that is, the program that created the PCX file
in the first place) must
encode these bytes as though they were single-byte runs. This is done by
preceding
the value of the byte with a value of 193. What makes this scheme clever
don't have to worry about this special case when writing a PCX decoding
is that we
program.
The routines that we write for decoding runs of bytes will handle these
single-byte
runs automatically, the same way they handle longer runs of bytes.
Here's another question that might come to mind: If 192 must be subtracted from
a byte to obtain a run-length count, how does a PCX fileencode runs longer than 63
bytes? The answer is that a PCX file can’t encode longer runs. Rather, longer runs of
bytes are broken up into multiple run-length pairs. A run of 126 identical bytes,
for
instance, would be encoded as two runs of 63 bytes.
To sum up, we can decode the
bitmap portion of a PCX file with this algorithm:
Read a byte from the bitmap. If the value of the byte is in the
range 0 to 191, transfer
the byte into the bitmap buffer unchanged. If the byte is in the
range 192 to 255,
Painting in 256 Colors m
mm
subtract 192 from the value of the byte. The result is the run-length count for the fol-
lowing byte; read another byte from the file and repeat it
that number of times in the
bitmap buffer. Repeat this process until the entire bitmap has been decompressed.
class Pcx
// class for loading 256-color VGA PCX files
{
private:
// Private functions and variables for class Pcx
pcx_header header; // Structure for holding PCX header
int infile; // File handle for PCX file to be loaded
protected:
unsigned char far *image; // Pointer to a buffer holding
// the 64000-byte bitmap
unsigned char far *cimage; // Point to a buffer holding
// a compressed version of
// the PCX bitmap
unsigned char palette[3*2561;// Array holding the
continued on next page
. Build Your Own Flight Sim in C++
int clength;
// Bitmap loading
int load_image();
// Palette loading
// Length of the compressed bitmap
function:
function:
// 768-byte
~~
4 HH
palette
EEE
void load_palette();
public:
// Public functions and variables for class Pcx
Pex ();
“Pcx();
// Function to load PCX data:
int load(char far *filename);
// Function to compress PCX bitmap
int compress();
// External access to image and palette:
unsigned char *Image()
{ return image; }
unsigned char *Palette()
{ return palette; }
};
All of those parts of the definition declared as private are exclusive to class Pcx and
can only be directly used within the Pcx functions. Functions and variables declared
as public, on the other hand, are available to all other modules that are linked with
this one. As far as the other modules are concerned, the class header consists of six
functions, Pcx(), ~Pex(), load(), compress(), Image(), and Palette() because these are
the only parts of the class accessible to them. Of these, Image() and Palette() are
defined inline since they are for access to pointers in the pex structure.
The constructor will set the buffer pointers to 0. Then when the object is destroyed,
we can test the pointers to see whether they are still 0 or not. If they have been
assigned a pointer address, the allocated memory can be released:
}
image = cimage = 0; // set initial pointers to 0
// Function to PCX.
load PCX data from file FILENAME into
// structure
// Open file; return nonzero value on error
if (Cinfile=open(filename,0_BINARY))==-1)
return(-=1);
lseek(infile,OL,SEEK_SET);
pcxloader.load("pcxfile.pcx");
-
“Build
:
return 0;
continued on next page
Build Your Own Flight Sim in C++
Lseek(infile,~-768L,SEEK_END);
the buffer, but no new byte is read. When a transition occurs from BYTEMODE to
RUNMODE—that is, if
a byte read in BYTEMODE is in the range 192 to 255—the
variable bytecount is
set to the value of the byte read from the file minus 192. This
value is then decremented on each subsequent execution of the loop, until the run is
completed and bytecount is
restored.
The data is read from the disk using the library function read(). This function reads
a large amount ofdata from the disk into a RAM buffer. This buffer must then be mon-
itored by the code processing the data. In this case, the buffer is imaginatively named
buffer. It is 5K in length. The variable bufptr monitors the current position of the next
character in the buffer to be processed. When the value of bufptr reaches the end of
the buffer, another read() is executed. The function knows when it has reached the
end of the file because the read() function returns 0.
The load_palette() function performs the much simpler task of reading the 256
color descriptors into an array of type char. This is done with a single read() into the
palette member variable. The numbers in the buffer must then be shifted left two bits
to make them consistent with the format required by the palette-setting functions in
the BIOS.
A PCX Viewer
Painting in 256 Colors
"sw
We can use the Pcx class to create a simple program to read a PCX from the disk file
and display it on the screen. The one thing missing from our Pex class is a function
which actually displays the data. A powerful part of C++ allows us to add a function
to the already described class by way of inheritance. We can easily derive a new class
called PcxView which uses the original Pex class as a base class. Our new class has
access to all of Pcx public and protected members. The text of the main program mod-
ule, PCXVIEW.CPP, appears in Listing 3-7.
cerr << "Cannot load PCX file." << endl; // Can't open it?
exit(0); // Abort w/error
}
pcxview walkbg.pcx
92 m
Painting in 256 Colors m m m
This will give you an advance look at a PCX file we'll use in a program we'll develop
in Chapter 4. (If you don't like having surprises spoiled in advance, you can skip this
exercise.)
93
~~" Build Your Own Flight Sim in C++
"mE BE
EE EE
Since there are two kinds of runs in this RLE scheme—runs of zero values and
nonzero values—we need a way to distinguish between these two kinds of runs. This
means we need two kinds of run-length bytes, ones for runs of zeroes and ones for
other runs. To distinguish between the two, we'll limit both types of runs to a length
of 127 and add 128 to the value of the run-length bytes for runs of zeroes. Thus, the
decoding scheme will be able to recognize runs of differing pixels because the run-
length byte will fall in the range 0 to 127—or 1 to 127, since zero-length runs won't
be allowed—and runs of zeroes because the run-length byte will fall in the range 128
to 255. (The decoding function will need to subtract 128 from this latter value before
it can be used.) See Figure 3-10 for an illustration of this scheme.
It starts off, as most functions do, by declaring some handy variables for later use:
int value,runlength,runptr;
unsigned char *cbuffer; // Buffer for compression
94 =
Painting in 256 Colors m
"a.
Because data will be read from image and compressed into cimage, counters will be
needed to mark the current position in both of those buffers.
long index=0; // Pointer to position in PCX.IMAGE
long cindex=0; // Pointer to position in PCX-CIMAGE
A large amount of scratch pad memory will be needed for performing the decom-
pression. The function allocates 64 K for that purpose and assigns the address thatof
block to the pointer variable cbuffer:
if ((cbuffer=new unsigned char[655301)==0) return(-1);
The new keyword command calls the operating system to allocate the memory block
and returns a pointer to that block. If the memory isn't available, then the pointer will
be zero, the function aborts, and it returns an error value to the calling function.
Now that the preliminaries are out of the way, we begin compressing data, looping
through the bytes in image until we have read all 64,000 of them (though we will rep-
resent the length of the buffer by the constant IMAGE_SIZE, so that it can be changed
more easily should this become necessary). A convenience pointer is
used to hold the
address of image and assigned by the inline Image() function:
unsigned char * image_ptr = Image();
while (index<IMAGE_SIZE) {
Compressing the data is a matter of reading bytes out of image_ptr and writing the
appropriate runs of values into cimage. Since the variable index accesses the next
value in image to be read, we can check to see if this value is a zero. If it is, then we've
encountered a “run” of zeroes:
if (image_ptrLindex1==0) { // If next byte transparent
Since this is the start of a run, the run-length variable must be initialized to zero:
runlength=0; // Set length of run to zero
We then read as many consecutive zeroes as we can find (up to 127) out of image_ptr,
counting the number of zeroes in the run with the variable runlength:
while (image_ptr{index1==0) {
index++; // Point to next byte of IMAGE
runlength++; // Count off this byte
if (runlength>=127) break; // Can't count more than
// 127
if (index >= IMAGE_SIZE) break; // If end of IMAGE,
// break out of Loop
}
Note that 128 is added to the value of runlength to indicate that this is a run of zeroes.
If the next byte in image_ptr is not a zero, then this is a run of differing pixels. As
before, we initialize the value of runlength to zero:
else {
cbufferCcindexJl=image_ptrLindex];
cindex++; // Point to next byte of CIMAGE
index++; // Point to next byte of IMAGE
runlength++; // Count off this byte
if (runlength>=127) break; // Can't count more than
// 127
if (index >= IMAGE_SIZE) break; // If end of IMAGE,
3
// break out of loop
Finally, we place the run-length byte back at the position marked by runptr:
Painting in 256 Colors m uu
cbufferCrunptrl=runlength; // Put run length in buffer
}
}
And we're done. The complete text of the compress() function is in Listing 3-8.
// Begin compression:
while (index<IMAGE_SIZE) {
if (image_ptrLindex1==0) { // If next byte transparent
continued on next page
HE EB BE BE = 97
|
}
// break out of Loop
cbufferCcindex]=image_ptrLindex];
cindex++; // Point to next byte of CIMAGE
index++; // Point to next byte of IMAGE
runlength++; // Count off this byte
if (runlength>=127) break; // Can't count more
// than 127
if (index >= IMAGE_SIZE) break; // If end of IMAGE,
}
// break out of loop
cbufferCrunptrl=runlength;
}
// Put run length in buffer
}
If you know how motion pictures work, then you already understand
the basic principles of computer animation. Pick up a strip of motion picture film and
look at it carefully. You do have a strip of motion picture film lying around, right? If
of a series
not, look at the strip of motion picture film in Figure 4-1. The film consists
of frames, each containing a still picture. When these still pictures are projected in
rapid sequence on a white screen, they give the impression of motion.
Where did this motion come from? There’ obviously no motion in the still frames
that make up the strip of film. Yet somehow we perceive motion when we see this
minds.
same sequence of frames projected on a screen. The motion is, in fact, in our
Fach of the still pictures on the strip of motion picture film is slightly different from
the one before it and our minds interpret these differences as continuous motion. But
what we are seeing is a sequence of still pictures.
For instance, the film in Figure 4-1 shows a figure walking from right to left across
the frame. In the first frame, the figure is near the right edge of the frame. In the sec-
ond, it is nearing the center. In the third it is in the center.
If we were to view this film on a projection screen, it would appear that the figure
was gliding smoothly from one side of the screen to the other, when in
fact the figure
is actually moving across the frame in a series of discrete jumps. But our brains, which
fill in the blanks in the sequence. We
expect to see continuous movement, obligingly
create motion where no actual motion exists.
103
~~ Build Your Own Flight Sim in C++
"
"HL
Foe
me
Early movie animators like Max Fleischer and Walt Disney understood that the
image in a motion picture frame didn't even have to be a real picture. It could be a
drawing that bore only a passing resemblance to things that one might see in the real
or imagined world. If these images moved in a realistic manner, the audience would
accept them as real. The trick to making these drawn images seem to move was to
draw the image in a slightly different position in each frame, so that an illusion of
motion is produced in the minds of the viewers. If these animators could make fan-
tastic images come to life on the screen, we should be able to make equally fantastic
images come to life on a computer screen.
104 m
Making It Move m wm mm
we'll discuss them briefly in order to illustrate the animation principles that we will be
using.
What is a sprite? In the generic sense, a sprite is a tiny figure on the computer’
video monitor that can be moved against a background bitmap without disturbing
that bitmap. Mario the carpenter, hero of a popular series of games available on the
Nintendo video game consoles, is a sprite. Some computers have special hardware
that will move sprite images about on the screen with minimal effort on the part of
the CPU (and surprisingly little effort on the part of the programmer). Alas, the IBM
PC has no such hardware, so we'll have to write a special set of routines for sprite
animation.
Sprites are ideally suited for implementation using object-oriented techniques,
since sprites are objects on the computer screen. In this chapter, we'll develop a Sprite
class that we can use to animate sprite images on the computer’ video display. The
Sprite class will be a full-fledged piece of object-oriented programming (OOP), illus-
trating many of the concepts of OOP code. All details of the sprite implementation
will be hidden—encapsulated—in the Sprite class, so that the calling routine need
merely invoke an object of the Sprite class and a sprite will appear magically on the
screen.
Bitmaps in Motion
At the simplest level, a sprite is nothing more than a rectangular bitmap, like the ones
we've discussed earlier. The sprite bitmap is generally quite small, much smaller than
the screen. Some impressive animation has been produced on computers such as the
Commodore Amiga using sprites that take up a substantial portion of the video dis-
play. The larger the sprite, the longer it takes to animate, especially on a computer that
offers no special hardware support for sprite drawing. Programs with big sprites tend
to run much more slowly than programs with small sprites. In this chapter we'll use
relatively small sprites.
Figure 4-2 shows an enlarged image of a sprite as a walker man. (The original,
unexpanded sprite is seen in the upper left corner.) Although shown here in grayscale,
the sprite in video memory. The trick is to save the background before we draw the
of it,
sprite, and then erase the sprite by drawing the saved background over the top
as in Figure 4-4.
HE BE BE BE B®
107
Build Your Own Flight Sim in C++
EB
// sprite bitmaps
char far *savebuffer; // Pointer to array for saving
// sprite background
int width, height; // Width and height of sprite in pixels
int x,y; // Current x,y coordinates of sprite
};
With C++, however, these fields can be encapsulated as private variables within a class
instead of public variables within a structure. As many instances of that class can then
be declared as there are sprites. Let's see how this is done.
private:
char far **image; // Pointer to sprite bitmaps
int nsprit; // number of sprite pointers
int width, height; // Width and height of sprite
// bitmap
int xsprite,ysprite; // Current x,y coordinates of
// sprite
char far *savebuffer; // Pointer to array for saving
Making It Move m m =m
// sprite background
public:
//Constructor for sprite class:
Sprite(int num_sprites,int width,int height);
// Destructor:
“Sprite();
// Sprite grabber function:
void grab(unsigned char far *buffer,int sprite_num,
int x1,int y1);
// Function to put sprite on display:
void put(int sprite_num,int x,int y,
unsigned char far *screen);
// Function to erase sprite image:
void erase(unsigned char far *screen);
};
The constructor, Sprite(), is
the simplest of these member functions. (Remember
that the constructor for a class is called automatically whenever an object of the class
is declared.) In this case, however, it
will also be necessary to pass several parameters
to the constructor, telling it how many sprite images need to be stored and what the
width and height of those images will be. The constructor then uses these numbers to
determine how much memory to set aside for the image and savebuffer arrays. Here’
the text of the constructor:
// Sprite constructor
// Allocates memory for NUM_SPRITES sprite structures
Sprite::Sprite(int num_sprites,int w,int h)
{
image=new char far *[num_spritesl;
width=w;
height=h;
savebuffer=new charfLwidth*heightl;
nsprit = num_sprites;
// initialize pointers
for(int i1i=0; ii < nsprit;ii++)
imageliil = 0;
The pointer array is initialized to 0 so that the destructor can free valid memory
pointers. The code for the destructor can then be written:
Sprite::"Sprite()
{
for(int 1i=0; ii < nsprit;ii++)
{
if(imageCiil )
delete [1 imageliil;
}
delete savebuffer;
delete image;
- ~
Build Your Own Flight Sim in C++
That was simple enough, but now things start getting complicated.
together a function to draw a sprite on the video display.
It’s
"EE EE
time to put
EEN
In this fragment of code, image is the two-dimensional array that contains the
sprite images, sprite_num is the number of the sprite image that we wish to display,
to
screen|] is a pointer either the video screen or an offscreen buffer in which the video
image is being constructed, and x and y are the screen coordinates of the upper left
corner of the sprite.
After the video memory location of the upper left corner of the sprite is calculated
in the first line, the data for the sprite bitmap is copied to video memory with a pair
of For loops, which use the height and width variables to determine how
many pix-
els to copy. The final line of this routine copies the current pixel values stored in the
sprite_num element of the image array to the current screen position. In this way, the
sprite bitmap is copied to video memory.
There are two problems with this simplistic approach. The first is that it doesn't
save the background. This can be remedied by adding a line to the code that reads:
// Copy background pixel into background save buffer:
savebufferLrow*width + columnl=screenfoffset + row*320
+ column];
It's not enough, therefore, to move the bytes of the sprite bitmap into video mem-
ory. We must check every byte before we move
it to make sure that it isn’t zero. If it
is, we simply skip over it, leaving the background pixel intact. To accomplish this, we
rewrite the last line of the sprite drawing code as two lines; the first reads the current
if
pixel from the image array and the second writes the pixel value to the display only
the value is nonzero:
// Get next pixel from sprite image buffer:
int pixel=imageLsprite_numllrow *width + columnl;
down the CPU unnecessarily. As written, the sprite code that we've
seen so far is rea-
sonably fast, but nowhere near as fast as it can be. For instance, we recalculate the
position of each pixel, both in the image buffer and on the screen, for each iteration
of the loop. We really only need to do this once. At the start of the
routine, we can
establish one variable that can serve as an offset into the image buffer (we'll call this
variable soffset, for “sprite offset”) and a second variable that serves as
an offset into
video memory (which we'll call poffset, for “pixel offset”). Now we
simply increment
these variables on every iteration of the loop, giving the video
memory offset an extra
increment atthe end of each line of pixels to point it to the beginning of the next line.
This is done by subtracting the width of the sprite from 320 (the width of
the display)
and adding the difference to the video memory offset, like this:
poffset+=(320-width);
We are now freed from having to recalculate the
position of the pixel on every pass
through the loop.
Putting Sprites
Listing 4-1 contains a complete sprite-drawing function, which we've called put() and
have declared as a member function of class Sprite, which
incorporates all of these
changes:
112 ®
Making It Move m mm
for(int row=0; row<height; row++) {
We've added some code at the end of this function to record the current coordinates
of the sprite in
the xsprite and ysprite variables. These will be used by the erase() func-
tion to calculate the position of the sprite in order to erase it from the display.
The put() function is called with four parameters: the number of the sprite in the
sprite array; the x,y coordinates at which the sprite is to be displayed;
and the address
of the video screen. (Once again, this allows us to draw into an offscreen buffer,
should we so desire. We'll have more to say about this in a moment.)
Erasing Sprites
The erase() function is a lot simpler than the put() function. Its not concerned with
the background pixels saved
transparent pixels or background pixels; it merely copies
by the put() function
Listing 4-2 shows the
over the
erase()
sprite image,
function:
effectively removing
it
from the screen.
This function should be called before a new image of the sprite is drawn in a
new posi-
tion or the sprite will leave a trail of old images behind
it.
(This effect can be interest-
ing under certain circumstances, but it’s not the effect we usually wish to produce.)
Grabbing Sprites
The job of the sprite grabber is to grab a rectangular bitmap out of a
larger (64K)
bitmap and store it in an array. We can create the larger bitmap by using the Pcx.load()
function that we created in the last chapter to load a PCX file from the disk. When
we
call the sprite grabber, we'll pass
it
the address of the buffer holding the large bitmap,
the position in the sprite array in which to store the sprite bitmap, the coordinates of
the sprite rectangle within the larger bitmap, and the dimensions of the
sprite. Listing
4-3 presents the text of the sprite grabber.
'
Listing 4-3 The sprite::grab function
Sprite::grab(unsigned char far *buffer,int sprite_num,
int x1,int y1)
{
Thats our Sprite class. The members of the class are summarized in Table 4-1. We'll
store the class declaration in the file SPRITE.H and the text of the member functions
in SPRITE.CPP. Now let’s write some code that uses these functions.
pp
~~"Build Your Own Flight Sim in C++
ow
The Walkman Cometh
Earlier, we suggested you peek into the file WALKMAN.PCX using the PCX viewer
that we developed in the last chapter. If you did, you saw seven pictures of a tiny
walker. To demonstrate our Sprite class, we'll use three of those pictures (we'll
save
the rest for the next chapter) to create an animation of a man walking across the
screen.
We'll grab those three pictures (the fifth, sixth, and seventh images, from left to
right) using our sprite grabber. Then we'll put those images on the mode 13h display
using the put() function of the Sprite class. We can create the illusion that the sprite
is moving from the left side of the display to the right by
putting the first image near
the left side of the display, erasing
it a second later, putting the second image a few
pixels further to the right, and so forth. By switching back and forth between images
of the sprite with its legs extended and images of the sprite with legs together, we can
also create the illusion that the sprite is striding as it moves. That's why we've called
this sprite WALKMAN.
Since we won't necessarily want to move the entire contents of the video buffer into
video memory, this function allows us to specify only a portion of the screen, though
func-
that portion must extend fully from the left side of the display to the right. The
the address of the video buffer, the vertical coordinate of
tion takes three parameters:
the uppermost line of the screen to move, and the number of lines of pixels to move.
does
In this program, we'll just move the section of the screen in which the walkman
his walking.
Constructing a Sprite
Most of the rest of what our WALKMAN program will do involves calling the mem-
ber functions of our Sprite class. We'll first create an object of the Sprite class that
we'll
call walksprite. The Sprite class constructor will be automatically called when we
declare this object, so we'll need to pass the constructor a parameter telling it that we'll
be grabbing three images of the sprite:
Sprite walksprite(NUM_SPRITES,SPRITE_WIDTH,
SPRITE_HEIGHT); // Sprite object
NUM_SPRITES
Early in the program, we'll need to declare an integer constant called
and set it equal to 4, like this:
const int NUM_SPRITES=4; // Number of sprite images
that
The general plan is to load the WALKMAN .PCX file into a Pex class object buffer
we'll name walkman which is declared near the beginning of the program. Later, walk-
man's loaded image (buffer) is used in a repetitive call to the walksprite grabber to
extract all three sprites from the same bitmap, using a for loop, like
this:
HE BE EH BE
= 117
Bud Your Own Flight Sim in C++
for(;;) {
// Loop indefinitely
forCint j=0; j<15; j++) // //Display fifteen frames
across screen
for(i=0; i<4; i++) ( // Loop through four
// different sprite images
// Put next image on screen:
walksprite.put(walk_sequencelil,j*20+i*5,100,
screenbuf);
// Move segment of video buffer into
// video memory:
Move m
Making It
“ow
putbuffer(screenbuf,100,SPRITE_HEIGHT);
for(long d=0; d<DELAY; d++); // Hold image on the
// screen for count
walksprite.erase(screenbuf); // Erase image
if (kbhit()) break; // Check for keypress,
// abort if detected
}
if (kbhit()) break; // Check for keypress,
// abort if detected
}
if(kbhit()) break; // Check for keypress,
// abort if detected
We have to check three times to see if a key has been pressed, because the main
part of the codeis nested three levels deep in For loops. The outer loop repeats indef-
initely (until the user escapes with a keypress), the next innermost loop repeats once
for every four frames as the sprite walks from left to right, and the innermost loop
deter-
cycles through the four animation frames, using the walk_sequence[] array to
mine which frame to display next. A check is made with the kbhit() function
value if
to see if
a key has
a key has been pressed. As we saw earlier, kbhit() returns a nonzero
been pressed by the user. It will continue returning a nonzero value until the keyboard
value is read, which is never done in this program. Thus, the kbhit() value can be read
every time break is used to escape from a level of the loop, yet it will still return nonzero
each time.
Notice that the loop is deliberately slowed with a fourth delay loop, which iterates
for the number of repetitions determined by the constant delay. This constant will be
set at the beginning of the program and can be used to
fine-tune the speed of the ani-
mation for a specific machine. If
you find that this animation runs too quickly on your
value of delay.
:
~~% ®
Incidentally, before any of the above can be done, a background image must be
loaded for our sprite to be animated against. Without
a
HE
machine, increase the value of the constant delay. If it runs too slowly, decrease the
background, we couldn't be
if
sure if the transparent pixels in our sprite image were truly transparent, or the image
was being properly erased and the background properly saved. We'll load this image
NH
Em
from the file WALKBG.PCX, which is on the disk that came with this book.
const int
const int
NUM_SPRITES=4; // Number sprite images
of
const int
SPRITE_WIDTH=24; // Width of sprite in pixels
SPRITE_HEIGHT=24; // Height of sprite in pixels
const long DELAY=40000; // Delay factor to determine
// animation speed. (Adjust
// this factor to find the
// proper speed for your machine)
// Function prototypes:
void putbuffer(unsigned char far *screenbuf,int y1, int height);
// Global variable declarations:
unsigned char far *screenbuf; // 64000 byte array to hold
int
// screen image
walk_sequencel1={0,1,2,1}; // Sequence of images for
// animation
Pcx walkman,walkbg; // Pcx objects to load bitmaps
// A
Sprite object:
Sprite walksprite(NUM_SPRITES,SPRITE_WIDTH, SPRITE_HEIGHT);
void main()
{
int oldmode; // Storage for old video mode number
//Load PCX file for background, abort if not found:
Making It Move m
SN
wm
=
if (walkman.load("walkman.pcx")) {
cout << "Cannot load WALKMAN.PCX file." << endl;
}
else {
When you run this program, you'll see the WALKMAN sprite walk
across the
screen against a textured background. Press any key when you're tired of watching the
little fellow strut his stuff. Although this may not look as spectacular
as
a full-fledged
video game, it
embodies many of the principles used to produce such
games. If you're
of a mind to write a Super Mario Brothers-style
game for the PC, these sprite routines
will get you off to a good start. Should you want
to
start off a bit smaller and simply
tamper with the WALKMAN program, load the WALKMAN. IDE project file into the
Borland C++ IDE and use
it
to recompile this program.
Although 3D animation doesn't use sprites, it follows the same principles that
we've illustrated in this
program. Our 3D animations will be constructed as sequences
of frames, in which each frame
is
constructed in an offscreen video buffer and moved
into video memory to avoid flicker. Each frame will depict
of an animation sequence, creating an illusion of motion.
a
slightly different portion
122 ®
Talking to
the Computer
Now you know how to animate a tiny figure on the video display of your
stufft Well, you are, but you've
microcomputer. You probably think you're pretty hot
still got a few things to learn. All the animation in the world can’t produce a great com-
unless the user of the computer has a way to interact with that animation.
puter game
We need to give the user a means of reaching into the world inside the computer and
taking control over what he or she finds there. To put it in the relatively mundane ter-
minology of computer science, we need to receive some input.
If you've been programming computers for more than 30 seconds, you probably
know a few things about this subject. You'll no doubt know how to enable the user to
such Getch() and Gets(). But game programmers
input characters using C functions as
need to know more about data input than the average COBOL programmer. Game
devices as joysticks and
programmers must be able to program such esoteric input
mice, which aren't supported by the standard C libraries of 1/0 routines. Game pro-
that the action on the computer
grammers must process this input so transparently
screen never slows down when a key is pressed or a joystick button is pushed.
for
In this chapter, we'll put together a package of low-level input/output routines
Then we'll write a class
dealing with the mouse, the joystick, and the keyboard.
such
known as an event manager, which will process the input from these devices in
know what device it’s receiving
a way that the rest of our program code will never even
125
a ee
"Build ics
Your Own
;
—C++
Flight Sim in
Ef
input from, much less any of the dirty details of the input/output hardware that our
low-level routines need to deal with.
Let's start out with the most esoteric of
When Geraldo Rivera opened the safe of the SS Titanic on nationwide television,
rumor had it that it might contain information on programming the PC joystick.
Goodness knows, the information doesn’t seem to be available anywhere else.
If you've ever browsed through libraries and bookstores
looking for even a hint of
information on this subject,
as 1 have on occasion, you probably came away frustrated.
If you were lucky, you found a brief description of the two ROM BIOS
|
routines that
support the joystick. (See Table 5-1 for a brief description of these routines.) But you
|
|
probably found nothing at all on programming the joystick hardware. Well, fear not.
I'll show you how to program the joystick and
give you sample code for doing so.
Joystick programming is essential in writing flight simulators because the joystick
is the ideal meansof controlling an airplane. In fact, the joysticks used on microcom-
puters are modeled after the joysticks used by airplane pilots. A flight simulator with-
out joystick control is like a day without sunshine. Or something like that.
Because most PC clones don't have a port for plugging in a joystick, at least not
they come out of the factory, not all PC game players have joysticks. The
as
gameport, as
the joystick portis called, must be purchased as an add-on board and placed in one
of the PC’ spare slots. Fortunately, some popular sound boards, such
as the Creative
Labs Sound Blaster, have joystick ports built in. Most serious PC
game players have
joystick ports on their machines, either as part of the sound board or on a
separate
board.
The original PC ROM BIOS, however, did not offer
support for joystick input.
Neither did the BIOS on the PC XT. It wasn't until the PC AT that BIOS
support for
the joystick was added. So that you won't have to
worry about what generation BIOS
your joystick routines are running on (and so you can see what's actually going on
inside these routines) we'll write joystick routines that work directly with the
joystick
hardware and bypass the ROM BIOS altogether.
126 m
Table 5-1 BM Rom Bios Joystick Routines
INT 15H
Function 84H
SubFunction 0
Reads status of joystick buttons
IN:
AH = 84H
DX = 0
ouT:
Carry 1: No gameport connected
carry 0: Gameport connected
AL: Switch settings:
Bit 7: Joystick 1's first button
Bit 6: Joystick 1's second button
Bit 5: Joystick 2's first button
Bit 4: Joystick 2's second button
INT 16H
Function 84H
SubFunction 1
0ouT:
Carry 1: No gameport connected
Carry 0: Gameport connected
AX: X-position of joystick 1
BX: Y-position of joystick 1
CX: X-position of joystick 2
DX: Y-position of joystick 2
whether the joystick button (or buttons) is pressed. An analog joystick, on the other
and how
hand, can both report to the computer which direction the stick is pointing
far the stick is pointed in that direction providing much more subtle and complex
information to a program. But it can also provide more headaches for the programmer.
Most game programs don’t require any more information than a digital joystick can
give them. Super Mario surely doesn't need to know anything more
than the direction
he’s supposed to be walking and whether or not he should be jumping. Our friend
Walkman, whom we met in
the last chapter, doesn't either. Flight simulators, on the
other hand, benefit from added information about how far the joystick is being
pushed, since this is similar to the information that real airplanes
receive from real joy-
sticks (such as the amount of thrust the pilot wishes to apply).
LoA
Build EEes Te —
Your Own Flight Sim in C++
Every I/O port has a number. The numbers of I/O ports range from 0 to 65,535,
though on a typical PC the vast majority of these ports are unused. The number for
the gameport is 0201h. There are a couple of ways to receive data from an input port,
but the common one used is to put the number of the port into the 80X86 DX regis-
ter and use the IN instruction to input data from that port into the AL register pair.
To input data from port 0201h, one must first put the number of the port into the DX
register like this:
mov dx,0201h
Then data can be received from the port into the AX register like this:
in al, dx
This tells the CPU to input data through the port specified in register DX and place
the value in the AX register.
To receive data from port 0201h, you must first transmit data to that port. It does-
n't matter what data you transmit; sending any value at all to the gameport tells the
joystick hardware that you'd like to receive data from it. Thus, you need only to exe-
cute these assembly language instructions to read the port:
mov dx,0201h ; Put gameport address in DX
you are performing Boolean operations. The C++ logical AND operator, which
resented by a double ampersand (&&), is used to tie together a pair of expressions
is rep-
(known as Boolean expressions) that are either true or false into a compound
expres-
sion that will be true only if both of the individual expressions are true. Similarly, the
C++ logical OR operator, represented by a double bar (I), can tie together a
pair of
Boolean expressions into a compound expression that will be true either of the indi-
if
|
The trick to understanding bitwise Boolean operators
is to think about the con-
cepts of true and false just as George Boole did: as the numbers 1 and 0, respectively.
Seen in that light, it
|
0 AND 1 Ooo
0 AND O nm
4 A mE NN
in which (you'll note) all bits except bit 4 are Os. (If you're not familiar with the man-
ner in which binary digits are numbered, you might want to skip ahead to the
“Decoding the Gameport Byte” section of this chapter and read the first few para-
graphs for an explanation.) Thus, the resulting byte will have zeroes in all positions
except bit 4, which will have the same value as the original digit in the byte you are
masking. Assuming the byte of unknown value was
11011100
then the result will be
00010000
But if the byte of unknown value was
01001111
the result will be
00000000
Simple, right? The byte 00010000 with which we ANDed the byte of unknown value
is referred to as a bitmask or, more simply, as a mask, because it is used to mask
cer-
tain bits in a byte while zeroing the rest.
Thus, if you perform a NOT on a byte, every bit in that byte that was 1 becomes 0
and every bit that was 0 becomes 1. And on that note, let's return to
our regularly
scheduled discussion, already in progress.
132 ®
Talking to the Computer
CL
Decoding the Gameport Byte
The byte of data that is received through port 0201h is composed of eight bitfields—
that is, each bit in this byte conveys an important piece of information:
Bit 0 Joystick A X-Axis
Bit 1
Joystick A Y-Axis
Bit 2 Joystick B X-Axis
Bit 3 Joystick B Y-Axis
Bit 4 Joystick A Button 1
I should briefly explain here what the bit numbers mean. Bit 0 is the rightmost bit
within any binary number and also known as the least significant bit. The other bits
is
The NOT instruction reverses the individual binary digits in the AL register, flipping
Os to Is and 1s to 0s. Now we need to get rid of the digits that we don’t want. Then
we can use the AND instruction to mask out the bits that we're not interested in, iso-
lating, for example, the button 1 bit for joystick A:
and al,164
The AND instruction compares two numbers on a bit-by-bit basis—in this
case, the
number in the AL register and the immediate number following it in the instruction.
Every bit in that corresponds to a 0 bit in the second number isitself zeroed. Only
AL
those bits in AL that correspond to a 1 bit in the second number are left untouched.
(If they are already Os, they remain Os. If they are 1s, they remain 1s.) In this
case, we
are ANDing AL with the number 164, which looks like this in binary: 0000010000.
Note that only bit 42 is a 1. Thus, only bit 42 in the AL register left unzeroed. This,
is
by no coincidence, is
the bit that represents button 1 on joystick A. We can also iso-
late the bit for button 2 like this:
|
assembly language subroutine to
isolate the bit for either button, depending on what
masking value we place in the register before we perform the AND instruction.
At the beginning of this assembly language procedure, we tell the assembler (via
the ARG directive) that we will be passing a single parameter called BMASK to it. The
BMASK parameter contains a number, which should be either 164 or 328, that is used
as a bitmask to isolate the bit for one of the joystick buttons.
The resulting value is
placed in the AX register, where
The calling routine can then test
itbecomes
this value
the
to
value
determine
returned
if it
to
is
C++ by the function.
nonzero. If it is, then
the button is being pressed. We can call this function from C++ like this:
joystick
int button=readjbutton(JBUTTON1);
This sets the int variable button to a zero value if no button is being pressed and to a
define the constant
nonzero value if a button is being pressed. We'll also need to
JBUTTONI to represent the mask value for button (which, as noted earlier,
1 is 4).
A Bit of Information
Now we come to a somewhat more complex subject: reading the x and y positions
of the stick. You may have noticed earlier that only 1 bit in the byte returned from
the gameport is devoted to each of these positions. Yet the analog joystick can be
be contained in a
placed in a large number of positions. How can this information
single bit?
It can’, exactly. When we send a value over port 0201h, the bits representing the
stick positions are all set to 1, no matter what position the respective sticks are in.
After a certain amount of time, the bits revert to Os. The amount of time it takes for
this to happen tells us what position the stick is in. Thus, in order to read the posi-
0.
tion of the stick, we must time how long it takes for this value to revert to
For the horizontal (x-axis) stick position, for instance, it takes less time for this
number to revert to 0 when the stick is pulled all the way to the left than when it is
intermediate positions, including the cen-
pulled all the way to the right. Timings for
For
ter position, fall somewhere between the timings for the two extreme positions.
the vertical (y-axis) stick position, it takes less time for this number to revert to 0
when the stick is pulled all the way up than when it is pulled all the way down.
Loop2:
in al,dx ; Read joystick bits
test al,ah ; Is requested bit (in bitmask) still 12?
Loopne Lloop2 ; If so (and maximum count isn't done) try again
We first
get the bitmask into the AH register. Then we output a value through GAME-
PORT (which we defined in the earlier routine as a constant
representing the value
0201h) to tell the joystick that we'd like to receive its status. Then we set loop
aup
by placing a value of 0 in the CX register.
Counting Down
Finally, we read the joystick status in a loop. The IN instruction receives the
joystick
status from the port. The TEST instruction ANDs the value in AL with the bitmask in
AH, setting the CPU flags accordingly (but
throwing away the result, which we don't
need). The LOOPNE instruction decrements the value in CX and then loops back to
the beginning of the loop if the result of the AND instruction wasn’t 0 (i.e., if the
joy-
stick bit is still 1) and the CX register hasn't counted back down to 0. (Yes, the value
in the CX register started out at 0, but the LOOPNE instruction performs the decre-
ment before it checks the value in CX, so the first value it sees in the register is
OFFFFh, the machine language equivalent of -1.) This way, the loop terminates either
when the joystick bit finally becomes 0 or when we've tested it
65,536 times, which
is certainly time to give up. (This latter event should
to hedge our bets.)
never happen, but its
a good idea
When this loop terminates, the CX register should contain a value indicating how
many times the loop had to execute before the joystick status bit returned to zero. This
value will be negative, however, because the CX register has been
counting down from
0 rather than counting up. To get the actual count, is
it
necessary to subtract this value
from 0, which can be done by placing a 0 in the AX register and
performing a SUB
(subtract) instruction, like this:
mov ax,0 ; Subtract CX from zero, to get count
sub ax,cx
That leaves the result of the count
the AX register is
in
the AX register—which
is
where it belongs, since
where values must be placed to be returned to C by assembly lan-
guage functions.
ARG bmask:WORD
push bp
mov bp,sp
cli ; Turn off interrupts, which could affect timing
mov ah,byte ptr bmask ; Get bitmask into ah.
mov al,0
mov dx,GAMEPORT ; Point DX at joystick port
mov ¢x,0 ; Prepare to loop 65,5361 times
out dx,al ; Set joystick bits to
Loop2:
in al, dx ; Read joystick bits
test al,ah ; Is requested bit (in bitmask) still 1?
Loopne Loop2 ; If so (and isn't done), try again
maximum count
sti ; Count is finished, so reenable interrupts
mov ax,0
sub ax,cx ; Subtract CX from zero, to get count
pop bp
ret
turn off processor interrupts with the CLI (clear inter-
Before we begin the loop, we
when we're done with a STI (set
rupt bit) instruction and we turn them back on again
sent to the CPU periodically (i.e., sev-
interrupt bit) instruction. Interrupts are signals
eral times a second) by external devices, reminding the CPU to stop whatever its
doing and attend to some important task, such as reading the keyboard or updating
While
the real-time clock. Interrupts slow the processor down in unpredictable ways.
this slowdown is normally almost imperceptible, it can make our count inaccurate, so
We can call this function
we don’t want interrupts to take place while we're looping.
from C++:
jx = readstick(JOY_X);
be sent to C++ to
Here we've defined a constant to represent the bitmask that must
to C++, which
remove the unwanted bitsin the joystick status byte. The
the
value
number
returned
of times we had to loop
in this case is assigned to the variable jx, represents
before the bit for
a
specific joystick
value for the x axis, so the returned
axis returned
value tells us
to 0.
the
In this case,
position of
we're
the x
requesting
axis.
the
_disppointer PROC
_rempointer PROC
Table 5-2 MW
Mouse Driver Functions
INT 33H
Function OOH
Reset mouse driver
IN:
AX: OOOOH
ouT:
AX: Initialization status
FFFFH: Successfully installed
0000H: Installation failed
BX: Number of mouse buttons
Function 01H
Displays the mouse pointer
IN:
AX: 0001H
ouT:
Nothing
Function 02H
Remove mouse pointer
IN:
AX: 0002H
ouT:
Nothing
Function 03H
Get pointer position and button status
IN:
AX: 0003H
out:
BX: Button status
Bit 0: Left mouse button (1 if pressed)
Bit 1: Right mouse button (1 if pressed)
Bit 2: Center mouse button (1 if pressed)
CX: X
coordinate
DX: Y
coordinate
Function OBH
Get relative mouse position
IN:
AX: 000BH
ouT:
CX: Relative horizontal distance (in mickeys)
DX: Relative vertical distance (in mickeys)
Talking to the Computer m
mm
Button-Down Mice
Function 3, the mouse driver function that reads the mouse button, returns a value in
the BX register indicating whether or not a button is being pressed. Bit 0
represents
the status of the left button, bit 1 represents the status of the right button, and bit 2
represents the status of the center button (if the mouse has one). If a bit is 1, the cor-
responding button is being pressed. If the bit is 0, the corresponding button is not
being pressed.
This routine also returns values representing the position of the mouse itself, but
we're going to ignore these in favor of the values returned by function Obh. Listing
5-4 is an assembly language procedure that returns the button status to C++.
When we call this procedure from C++, we can mask the result using the bitwise AND
(&) operator to read only the bits for the button we're interested in, like this:
int b = readmbutton(); // Read mouse button
// If mouse button 1 (left button) pressed,
// perform desired action:
if (b & MBUTTON1) do_it();
Once again, we've defined a constant for the mask. (All of these constants that we've
been defining will turn up eventually in the header files MIO.H and EVNTMNGR.H.)
141
bid— Your Own Flight Sim in C++
— ~~
®%
numbers returned from the joystick. A value of 0 in either direction always means that
the mouse hasnt moved since the last call to function Obh.
The trickiest part of writing a routine for reading the relative mouse position is in
EA HW
EEE
returning the values to C++. Ordinarily, we return the result of a function in register
AX. However, this method generally allows us to return a single value only; here, we
want to return two values, the changes in the x and y positions of the mouse. Instead
of returning these as function values, we'll pass the addresses of two variables, which
we'll call x and y, from C++ to a function written in assembler. Then we can change
the values of those variables directly from the assembler. We set up the variables with
the ARG directive, like this:
ARG x:DWORD,y:DWORD
Next, we use the LES command to place the address of the x and y parameters in the
appropriate CPU registers:
Les bx,x
LES loads the address of the parameter x into the ES register and the register specified
immediately after the command—in this case, the BX register. Thus, this command
creates a pointer to x. We can then use this pointer to
store a value in x:
mov [es:bxl,cx
The brackets around ES:BX tells the assembler that ES and BX together hold an
address—the address stored in the x parameter. If everything has been set up proper-
ly on the C++ end of things, this should be the address of the variable that stores the
relative change in the x coordinates of the mouse. The MOV instruction will move the
value in the CX register into that address. We can then do the same thing with the y
parameter.
All Keyed Up
The keyboard is the Rodney Dangerfield of input devices: It gets no respect. Every
computer has one and every computer user is
looking for something better—a mouse,
a
a joystick, touch screen. But, in the end, everybody comes back to “old reliable.” Just
about any kind of input that a program needs can be done through the keyboard.
Even mouse-oriented programs generally allow the user to fall back on keyboard
commands for commonly used functions—and more than a few allow the mouse cur-
sor to be positioned via the keyboard cursor arrows. Only the most incorrigible of
keyboard haters would try to input large quantities of text without one.
With more than 100 keys on the current standard model, the keyboard allows far
more complex and subtle input than any other device, though the mouse is also pret-
ty versatile. The most important thing about the keyboard as a game input device,
though, is that its
the only input device that every user of a PC game is
guaranteed to
own. Some users may not have a joystick, others may not have a mouse, but every-
body’ got a keyboard.
The C++ language offers substantial support for keyboard input. Most of the key-
board-oriented commands, however, are inadequate for the rapidfire key access that
a game program requires. Once again, we're going to bypass the C++ library and add
HE BE BE
EB 143
“Build Your Own Flight Sim in C++
—
= EE BEE
some keyboard commands to our growing MIO.ASM file. This time, we'll access the
keyboard via the ROM BIOS.
Managing Events
Now that we've got a set of assembly language procedures for dealing with input
devices, what do we do with them? In general, we don’t want our program to get too
close to the input hardware being used to control it. Referencing the assembly
events to terminate the program. We'll ignore the other events, but we'll include them
in the event manager because they could be useful in other simple games (and it seems
silly to exclude them, even if we know we aren't going to use them).
The event manager that we create here won't be an event manager in the technical
sense. A real event manager is interrupt driven, which means that it is always lurking
in the background watching for input, even while the program is executing. The event
manager we devise will essentially go to sleep when we aren't calling it. Some refer to
this as a pollable event manager because you must poll, or explicitly ask it to check
for events. On the other hand, it will rely heavily on at least two genuinely interrupt-
driven event managers—the mouse driver that came with the mouse and the key-
board driver that’s built into the BIOS. So it isn't really that much of a stretch to call
this module an event manager, even if that isn't quite right.
The event manager will call the routines in MIO.ASM, watching for certain kinds
of input that could represent the six events listed in the previous paragraph. What
kinds of input events will it look for? Well, moving the joystick on the four main axes
could represent LEFT, RIGHT, UP, and DOWN events, while pressing the joystick but-
tons could represent LEFT BUTTON and RIGHT BUTTON events. Similarly, moving
HE BE BE
BB 147
—1
~~
ee i a
Build Your Own Flight Sim in C++
the mouse in the four cardinal directions could also represent LEFT, RIGHT, UP, and
DOWN events, while pressing the left and right mouse buttons could represent LEFT
BUTTON and RIGHT BUTTON events. And pressing the four cursor arrows on the
keyboard could represent LEFT, RIGHT, UP, and DOWN events, while pressing two
BE
EEE BE
(T)
other arbitrarily chosen keys (we'll use the (E) and keys) could represent LEFT
BUTTON and RIGHT BUTTON events.
class EventManager
{
private:
int x,y; // ALL purpose coordinate variables
int xmin,xmax,xcent,ymin,ymax,ycent; // Joystick calibration variables
int lastkey,keycount; // Keyboard variables
protected:
void
public:
init_events(); // Initialize event manager
EventManager();
void setmin(); // Set minimum joystick calibrations
void setmax(); // Set maximum joystick calibrations
void setcenter(); // Set center joystick calibrations
int getevent(int); // Get events from selected devices
};
148 ©
Talking to the Computer m mm
lastkey = keycount = 0;
init_events();
}
void EventManager::init_events()
{
We then need three routines for calibrating the joystick. These routines will wait
for the user to move the joystick into a requested position—upper left corner, center,
or lower right corner—and then set six variables to represent the values received from
the readstick() function in those positions. Those values can determine where the joy-
stick is pointing. In this program, we'll only be using the values for the joystick cen-
ter position. The calibration routines are in Listing 5-8.
void EventManager::setmax()
// Set maximum joystick coordinates
{
while (!'readjbutton(JBUTTON1)); // Loop until joystick button pressed
xmax=readstick(JOY_X); // Get x coordinate
ymax=readstick(JOY_Y); // Get y coordinate
}
while (readjbutton(JBUTTON1)); // Loop until button released
void EventManager::setcenter()
// Set center joystick coordinates
{
while (!'readjbutton(JBUTTON1)); // Loop until joystick button pressed
xcent=readstick(JOY_X); // Get x coordinate
ycent=readstick(JOY_Y); // Get y coordinate
}
while (readjbutton(JBUTTON1)); // Loop until button released
manager an integer parameter in which each of the three lowest bits represents an
input device. If bit 1 is a 1, then mouse events are requested. If bit 2 is a 1, then joy-
stick events are requested, and if bit 3 is a 1, keyboard events are requested. We'll
declare three constants—mouse_events, joystick_events, and keyboard_events—in
which the appropriate bits are set to 1. To request a combination of events from the
event manager, these constants need only be added together to form a parameter in
which the appropriate bits are set. For instance, this statement requests events from
all three devices:
event=getevent(mouse_events+joystick_events
+keyboard_events)
The value returned by getevent() is also an integer in which the individual bits are
important. In this case, the bits represent the events that have occurred. Since we're
only monitoring six events, the sixteen bits in an integer are more than adequate to
tell the calling routines whether or not these events have occurred. We'll declare six
constants in which the appropriate bits are set, so that they can be tested against the
value returned from getevent() to determine if the event has happened. These con-
stants are LBUTTON (for LEFT BUTTON events) RBUTTON (for RIGHT BUTTON
events), UP, DOWN, LEFT, and RIGHT. We'll also include a constant called
NOEVENTS in which none of the bits are set to 1s. We can test the value returned from
getevent() by ANDing it with these constants and seeing if the result is nonzero, like
this:
if (event & RIGHT) do_it; // If RIGHT event, do it
That’ easy enough, right?
}
return(event_return);
The only part of the event manager that’ at all complicated is the way in which it
handles keyboard events. The problem with using the BIOS routines for keyboard
input is that the BIOS doesn't tell us when a key is held down continuously.
Unfortunately, this is precisely the information we need in order to
use the keyboard
to control a game. The BIOS will allow a key to repeat it’s if
held down long enough,
but even so it will look to any program calling our scankey() function as though the
key is being pressed repeatedly rather than being held down continuously, since the
scan code for the key will be returned only intermittently by the BIOS. The only solu-
tion is to write routines that work with the keyboard hardware. This is a complex job,
however, so I've saved it for the flight simulator. For now, we've jury-rigged a setup
that makes it look like a key is being held down continuously. Basically, the event
manager will return the same key for 20 calls in a row to getevent(), unless another
key is pressed in the meantime. This makes it seem as though most keys are held
down longer than they actually are, but it does simulate continuous key pressing.
The complete text of the event manager module appears in Listing 5-10.
void EventManager::init_events()
{
initmouse(); // Initialize the mouse driver
rempointer(); // Remove mouse pointer from screen
}
void EventManager::setmin()
// Set minimum joystick coordinates
{
while (!readjbutton(JBUTTON1)); // Loop until joystick button pressed
xmin=readstick(JOY_X); // Get x coordinate
ymin=readstick(JOY_Y); // Get y coordinate
while (readjbutton(JBUTTON1)); // Loop until button released
}
void EventManager::setmax()
Talking to the Computer
Ew
// Set maximum joystick coordinates
{
while (!readjbutton(JBUTTON1)); /1/ Loop until joystick button pressed
xmax=readstick(JOY_X); // Get x coordinate
ymax=readstick(JOY_Y); 17 Get y coordinate
}
while (readjbutton(JBUTTON1)); // Loop until button released
void EventManager::setcenter()
// Set center joystick coordinates
{
while (!readjbutton(JBUTTON1)); // Loop until joystick button pressed
xcent=readstick(JOY_X); Get x coordinate
ycent=readstick(JOY_Y); // Get y coordinate
while (readjbutton(JBUTTON1)); 17 Loop until button released
}
Walkman Returns
Now that we've got a working event manager, let’s put it
through its paces. To do the
honors, we'll call back our friend Walkman from Chapter 4. You may recall, if you
used the PCX viewer to take a peek at that it,
the WALKMAN.PCX file on the disk
actually contains seven images the of little guy, even though we only used three of
them in the program. Now we'll use the other four. In addition to seeing Walkman
walk to the right, we'll now see him walk to the left and stand still. And you'll be able
to use the keyboard, joystick, and mouse to make him do so.
This will require a number of changes to the Walkman program, however. For
instance, instead of a single array containing the sequence of frames involved in the
Walkman animation, we'll need three arrays, one for the sequence of frames involved
in walking right, one for the sequence of frames involved in walking left, and one for
the single frame involved in standing still. Here’ the definition of the structure that
will hold these sequences:
struct animation_structure { // Structure for animation sequences
int seq_length; // Length of sequence
int sequencelMAX_SEQUENCE]; // Sequence array
};
The sequence array, called sequence, will contain the sequence of frames. Although
these sequences can be of variable length, it makes initializing the arrays somewhat
easier if we fix their length. The constant MAX_SEQUENCE represents that fixed
length. We'll define MAX_SEQUENCE elsewhere in the program as 4, since that’s the
154 ®
Talking to the Computer
"Ew
longest animation loop that we'll need in our program. Here's the initialization for the
actual animation sequences:
struct animation_structure walkanim[31=(
// Animation sequences
4,0,1,2,1, // Walking left sequence
1,3%,0,0,0, // Standing still sequence
4,4,5,6,5 // Walking right sequence
Because there are three different animation sequences, we declare an array of three
animation structures, each of which contains a sequence array. The first number in
each initialization sequence is the number of frames in the sequence; the following
numbers are the sequences of frames themselves.
At the beginning of the program, we'll declare the event manager object and give
the user a choice of the three possible input devices. The user will type in a number
representing which input device he or she will use, then we'll use that number to set
the appropriate bit in the byte that will be passed to the getevent() routine. We won't
do anything fancy to request this information from the user, just print a string on the
screen with the insertion operator (<<) and cout and read a typed character with the
extraction operator (>>) and cin. We'll need to loop until a number from 1 to 3 is
typed (representing one of the three input devices), then use a switch statement to set
the correct bit in the mask. Heres the routine:
EventManager evelyn; // Initialize event manager in constructor!
// Select control device:
cout << "Type number for input device:" << endl <<
" (1) Keyboard" << endl <<
" (2) Joystick" << endl <<
" (3) Mouse" << endl;
char key=0;
while ((key<'1') || (key>'3')) cin.get(key);
switch (key) {
case '1': event_mask=KEYBOARD_EVENTS; break;
case '2': event_mask=JOYSTICK_ EVENTS; break;
case '3': event_mask=MOUSE_EVENTS; break;
}
If the user chooses the joystick as an input device, we'll need to calibrate it. We can
do this by calling the three calibration routines in the event manager:
Now we'll perform the same initialization that we did in the last chapter, loading the
PCX
files for the sprite images and the background. The difference this time, is that
we'll load all seven sprite images, like this:
// Grab seven sprite bitmaps from PCX bitmap:
for(int i=0; i<7; i++) walksprite.grab(walkman.Image(),i,
i*SPRITE_WIDTH,0);
The main animation loop will now put a single frame of the animation on the screen
and call the event manager. it
finds that there has been user input,
If
it
will change the
variables involved in the Walkman’s movement to
reflect the input. For instance, the if
Walkman is walking left and the event manager reflects either a RIGHT event or no
event at all, the animation code will switch to the standing-still animation sequence.
If the Walkman is walking right and a LEFT event (or no event) is detected, the stand-
ing-still sequence will also be initiated. But if
the Walkman is standing still and a LEFT
or RIGHT event occurs, the animation code will switch to the appropriate walking
sequence. This will make the Walkman appear to stop walking when no input
received and to do a full 180-degree swivel when the joystick direction is reversed.
is
Here's the animation code:
// Animation loop:
animation_structure *anim=&walkanimlcur_sequencel;
// Put next image on screen and advance one frame:
walksprite.put(anim->sequencelcur_frame++1,
xpos, ypos,screenbuf);
// Check if next frame is beyond end of sequence:
if (cur_frame>=anim->seq_Llength) cur_frame=0;
// Advance screen position of sprite, if moving
// and not at edge of screen:
if ((cur_sequence==WALK_RIGHT) &&
((xpos+XJUMP)<(320-SPRITE_WIDTH))) xpos+=XJUMP;
Talking to the Computer
"En.
if (Ccur_sequence==WALK_LEFT) && ((xpos-XJUMP) > 0))
xpos—==XJUMP;
cur_sequence=STAND_STILL;
cur_frame=0;
}
HE BE BE BE BE
157
|
cur_sequence=WALK_LEFT;
cur_frame=0;
}
cur_sequence=STAND_STILL;
cur_frame=0;
}
}
cout << "Center your joystick and press button one" << endl;
endl <<
evelyn.setcenter(); // Calibrate the center position
cout << "Move your joystick to the upper Llefthand corner " << endl <<
"and press button one." << endl;
evelyn.setmin(); // Calibrate the minimum position
cout << "Move your joystick to the lower righthand corner" << endl <<
"and press button one." << endl;
evelyn.setmax(); // Calibrate the maximum position
}
// Animation loop:
animation_structure *anim=8&walkanimlcur_sequencel;
walksprite.put(anim->sequencelcur_frame++1,
Xpos,ypos,screenbuf);
// Check if next frame is beyond end of sequence:
if (cur_frame>=anim->seq_length) cur_frame=0;
((xpos+XJUMP)<(320-SPRITE_WIDTH))) xpos+=XJUMP;
if ((cur_sequence==WALK_LEFT) && ((xpos=-XJUMP) > 0))
xpos==XJUMP;
cur_sequence=STAND_STILL;
cur_frame=0;
}
}
Your Own Flight Sim in C++
if(
}
screenbuf
}
)
a
//
if
delete [1 screenbuf;
setgmode (oldmode) ; //
cur_sequence=WALK_LEFT;
cur_frame=0;
}
cur_frame=0;
}
//
}
cleanup
Restore old video
memory
mode
EEE
or from walking right to standing
(cur_sequence==WALK_RIGHT) {
cur_sequence=STAND_STILL;
still:
characters,
of
will be part
all
a larger world. In that world, there will be objects, scenery, perhaps
of which can be viewed in any way that
tance. Rather than just creating a scene, we are going to build a
can create a nearly infinite number of potential scenes.
users of our programs the ability to choose which scenes they actually see
even
we like, from any angle or dis-
Bitmap Scaling
We could have given him a kind of third dimension, though, by increasing our file of
and smaller,
sprite images to include dozens of images of the Walkman growing larger
toward Then it would have been fairly
as though he were moving away from or us.
around in three-dimensional
simple to make it appear as though he were walking a
the
universe. Or we could have used bitmap scaling techniques to reduce and increase
size of the Walkman’ bitmaps as he meandered about in
the third dimension. (Bitmap
the program is
scaling techniques involve removing and adding bitmap pixels while
running, to make the bitmap become larger and smaller.)
Bitmap scalingis the approach to three-dimensional computer graphics used in sev-
eral popular computer games, including the highly successful Wing Commander series
from Origin Systems. In these games, numerous bitmaps of spaceships are stored in the
computer's memory showing the ships from several angles, and then
the bitmaps are
scaled to make it appear as though the ships are moving away from and toward the
viewer as they bank and turn.
The problem with this technique is that it requires a lot of bitmaps before the three-
dimensional motion of characters and objects appears realistic. Bitmaps eat up a lot of
themselves to a relative few
memory, so programs like Wing Commander must restrict
bitmapped objects lest there not be enough memory left over for such frills as program
code.
CL
!
All Wired Up
Rendering Techniques
The more popular (and versatile) approach to
creating three-dimensional scenes
is to store a mathematical description of a
scene in the computers memory and
have the computer create the bitmap of the scene on the fly, while the
program is
running. There are many such techniques, known collectively as rendering tech-
niques, because the computer itself is rendering the images. In effect, the
computer
becomes an artist, drawing animation frames so rapidly that
they can be displayed
in sequence much as we displayed the Walkman bitmaps in
sequence in the last
chapter.
One of the most popular rendering techniques is called
ray tracing. Suprisingly sim-
ple on a conceptual level, ray tracing requires that the computer treat each pixel on the
video display as a ray of light emanating from the imaginary scene inside the
computer.
By tracing the path of each of these light rays from the viewers
eye back into the
scene, the computer can determine the color of each pixel and place an appropriate
value into video RAM to make that color appear on the display.
Scenes rendered by ray tracing can look startlingly real. The technique is
com-
monly used to create movie and television special effects of such vivid realism that they
can look better than scenes made up of actual objects. This, in fact,
is the key to rec-
ognizing ray-traced animation when you see it: The scenes look impossibly vivid,
filled with sparkling reflections and bright colors. Ray-traced images look almost too
good, more perfect than the real world could ever be, and they often lack the organic
warmth of real-world scenes.
Can we create a computer game using ray-traced animation? Unfortunately,
not on
today’s PCs. Ray tracing just takes too long, So personal computers must
get a lot faster
than they are now (or somebody must discover a superfast ray-tracing algorithm)
before ray tracing will be useful. A typical full-screen
ray trace can take hours
on the average microcomputer, even with the aid of a math coprocessor. Ray-traced
to create
CI 167
~
en en
Build Your Own Flight Sim in
od
C++
— — A Hm EH
EEN
rendering an image, it is also far and awaythe least realistic. With some special excep-
tions, wireframe graphics techniques are unacceptable in current PC game software.
(Nonetheless, we're going to use wireframe techniques in this and the next chapter
because they are easy to understand and they utilize many of the basic principles that
apply to polygon-fill graphics.)
How do wireframe and polygon-fill graphics work? Basically, wireframe graphics
build an image from a series of lines, thus giving the object the appearance of having
been constructed from wires. Figure 6-1 shows a simple wireframe image of a house.
Polygon-fill graphics build an image from a series of solid-colored polygonal shapes,
thus giving objects the appearance of faceted jewelry. Neither technique is realistic—
few objects in the real world are made from either wires or polygons—but both tech-
niques have the advantage of being very, very fast.
Another advantage of both wireframe and polygon-fill graphics is
the simplicity and
efficiency with which descriptions of objects can be stored in memory prior to
® ® = =
build Your Own Flight Sim in C++
—
Figure 6-4 The ends of a line can be
considered vertices, although
technically they are not
Vertex Descriptors
If we are going to describe a shapeas a list of vertices, then we are going to need some
kind of vertex descriptor: a method of describing a vertex as a set of numbers in a data
170 =
CMiWiredUpw
mow
represents the y axis of such a system, then any pixel on the display can be given a pair
of coordinates based on its position relative to these axes. (See Figure 6-6.) For
instance,
a pixel 72 pixels from the left side of the display and 51 pixels from the
top can be des-
ignated by the coordinate pair (72,51). As in a true Cartesian coordinate system, the x
coordinate is always given first and the y coordinate given second. But the numbers in
this video display coordinate system differ from those in a true Cartesian coordinate
sys-
tem in that they grow larger rather than smaller as we move down the axis.
y
If we were to draw a straight line of pixels on the video
display, each pixel in that
line could be described by a coordinate pair. If we were to save that list of coordinate
pairs, we could reconstruct the line later by drawing pixels at each of those coordinate
positions. However, it’s not necessary to save the coordinates of every pixel on a line in
order to reconstructit; we need only the pixel coordinates of the two endpoints of the
line. We can then redraw the line by finding the shortest line between the two
points
to
and setting the pixels that fall closest
a
that line. Thus, line can be described by two
coordinate pairs. For instance, the line in Figure 6-7, which runs from coordinate
position (189,5) to (90,155) can be described by the two coordinate pairs
(189,5)-(90,155).
Figure 6-8 shows a square constructed out of four lines on the video display. We can
extend our coordinate pair system to describe this square as a series of four shared ver-
tices. This particular square can be described by the four coordinate pairs (10,10),
(20,10), (20,20), and (10,20). We can reconstruct the square by drawing lines between
these coordinate positions, like a child solving a connect-the-dots puzzle. (Wireframe
computer graphics bear a distinct resemblance to such puzzles.)
To demonstrate some of the basic principles of three-dimensional
graphics, we'll
develop a program that will draw two-dimensional wireframe images based on the
video display coordinates of the vertices of those images. First we'll learn to draw a line
between any two vertices. Then we'll add functions to perform some clever tricks
on
wireframe images, like moving them to different places on the screen, and
shrinking
enlarging them, and even rotating them clockwise and counterclockwise.
HE EE BE
EB 171
o
Build Your Own Flight Sim in C++ Ll. pmAEeE
A Two-Dimensional Wireframe
Package
Wireframe graphics are constructed by drawing lines to connect vertices on the video
display. The first thing we'll need in our package of wireframe functions, then,
is a
function that will draw lines between any two coordinate positions on the display.
Such a function isn't as easy to write as you might think, especially if it’s to run as
fast as possible. To see why, let's look at the way lines of pixels are drawn on the video
display.
Pixels on the video display are arranged in matrices of horizontal rows and vertical
columns. Anything that is drawn on the display has to be drawn using those pixels and
it has to be drawn within the limitations of this matrix arrangement. This is less than
there are
ideal for drawing straight lines between any two coordinate positions, because
no straight lines between most pairs of coordinate positions, not fit
that into the pixel
matrix anyway. And if something doesn't fit the pixel matrix, it can't be drawn. The
172 ®
All Wired Up m
"ow
only truly straight lines that can be drawn on the display are those that are
perfectly
horizontal, perfectly vertical, or perfectly diagonal, since those
just happen to fit the
pixel matrix. All others must meander crookedly among the rows and columns of
pixels in order to reach their destinations. Thus, most coordinate pairs on the video
display will be connected by lines that are at best approximations of straight lines. For
instance, the line in Figure 6-9(a) could be approximated by the
sequence of pixels in
Figure 6-9(b).
This is an unavoidable limitation of
computer graphics displays. Drawing a line on
the video display therefore becomes a matter of finding the best
approximation of a
straight line in as little time as possible. That's what makes computer line drawing so
tricky—and so interesting.
The key to finding the best approximation of a straight line
the
concept of slope.
is
a
line tells us how sharply it is angled
CI
with pixels
173
a EEE
——— Ho Hm
relative to a horizontal line. A horizontal line, like flat ground, has no slope all, thus
at
we say that ithas a slope of zero. A vertical line has as much slope as a line can ever
have, so it’s tempting to say that it
has a slope of infinity,
if
but in fact the
think
slope
of it as
of a ver-
infinite
tical line is mathematically undefined. However, you want to
have that they
slope, you certainly can. Lines that are nearly vertical slopes so large
might as well be infinite.
To calculate the slope of a
line, you need to know the coordinates of two different
know the coordinates of at
points on that line. This is convenient, since you will always
least two points on any line you draw: the two endpoints. Once you have the coordi-
the change in both
nates of two points the slope can then be determined by calculating
the x and y coordinates between these two points, then calculating the ratio of the y
change to the x change. If
that sounds complicated, it
really shouldn't. The change in
the x coordinate of the first
the x coordinate can easily be determined by subtracting
endpoint from the x coordinate of the second. Ditto for the y coordinate. Then the ratio
can be determined by dividing the y difference by the x difference. Thus, if a straight
line runs from coordinate position (x1,y1) to coordinate position (x2,y2) the slope can
be calculated with the formula:
slope = (y2-y1) / (x2-x1)
6-10 runs from coordi-
It’seasy to apply this formula to a real line. The line in Figure
is sub-
nate position (1,5) to coordinate position (4,11). When the starting y position
tracted from the ending y position, the difference is 6. When starting x position is
the
subtracted from the ending x position, the difference is 3. The first difference (6)
divided by the second difference (3) is 6/3 or 2. This can be expressed more effi-
ciently as:
slope = (11 - 5) / C4 - 1) = 6 / 3 = 2
174 ®
All Wired Up m
"a.
slope? (This is perfectly legal.) Even worse, what if it has a fractional slope? Alas, most
lines have fractional slopes and half of all lines have negative slopes. How will we draw
lines such as these?
There are actually several algorithms available for drawing lines of arbitrary slope
between arbitrary points. But the choice of algorithm for a wireframe animation
HE BE BE
EB 175
ow
Build :
Your Own Flight Sim in C++
"EE EEE
package iswireframe
constrained by the need to draw the lines as quickly as possible, in order to
animate in
shapes real time. Thus, the use of fractions should be avoided as
much as possible, because fractions tend to slow calculations greatly, especially on
machines without floating-point coprocessors. (We'll talk more about this in the chap-
ter on optimization.) And we want to minimize the number of divisions that must
be
Bresenham'’s Algorithm
A few pages ago, we said that horizontal and vertical lines are the easiest to draw. Why?
Because every point on a horizontal line has the same y coordinate and every point on
a vertical line has the same x coordinate. To draw a horizontal line, you need simply
draw a pixel at every x coordinate between the starting x coordinate and the ending x
coordinate, without ever changing the y coordinate. Drawing a horizontal line between
(x1,y1) and (x2,y2) is as simple as this:
y=y1;
for (int x=x1; Xx<x2; x++,y++) drawpixel(x,y);
If the starting coordinate is larger than the ending coordinate in one of the two dimen-
sions,the line has a negative slope. (Try calculating such a slope and see.) In that case,
the value of the coordinate is decreased on each pixel rather than increased.
Lines of an arbitrary slope are trickier. Fortunately, every possible line can be drawn
by incrementing one coordinate, either x or y, once for each pixel in the line; thus,
It’s also quite easy to determine
every possible line can be drawn with a for loop.
which coordinate can be used as the loop counter and be incremented in this manner.
If the absolute value (that is, the value with its sign removed) of the slope is less than
1, then it is the x coordinate that is incremented for each pixel. the absolute value is
If
176 ®
ww
AllWired Up
m
greater than 1,then it is the y coordinate that is incremented on each pixel. If the
slope
is precisely 1 then the line is perfectly diagonal and either coordinate can be used as the
loop counter.
But what about the second coordinate? When is it incremented? Well, in the
case of
horizontal and vertical lines, the second coordinate is never incremented and in the
case of diagonal lines
it is always incremented. But for other types of lines,
when the second coordinate is incremented is the crux of the problem. Bresenham’s
deciding
~~ HE BE BE
B= 177
Build Your Own Flight Sim in C++
—
Figure 6-12 Comparing two types of
lines
orequalto 1
equal to 1
178 ®
Wired Up
mmm
NSN
All
// direction
ydiff=-ydiff; // ..get absolute value of difference
y_unit=-320; // ..and set negative unit in
// y dimension
tise y_unit=320; // Else set positive unit in
/7/ y dimension
int xdiff=x2-x1; // Calculate difference between
if (xdiff<0) (
// x coordinates
// If the line moves in the
// negative direction
xdiff=-xdiff; // ...get absolute value of
// difference
Xx_unit=-1; // ...and set negative unit
}
// in x dimension
else x_unit=1; // Else set positive unit in
// y dimension
int error_term=0;
if (xdiff>ydiff) {
// Initialize error term
// If difference is bigger in
// x dimension
int length=xdiff+1; // ...prepare to count off in
for (int i=0; i<length; i++)
// x direction
{
// through points
Loop
// in x direction
screenloffsetl=color; // Set the next pixel in the
17 Line to COLOR
offset+=x_unit; // Move offset to next pixel
// in x direction
error_term+=ydiff; // Check to see if move
// required in y direction
if (error_term>xdiff) {
// If so...
error_term-=xdiff; // ...reset error term
offset+=y_unit; /7/ ...and move offset to next
// pixel in y direction
}
}
else {
// If difference is bigger in
// y dimension
int length=ydiff+1; 77 ...prepare to count off in
y direction
for (int i=0; i<length; i++) // through points
Loop
// in y direction
screenloffsetl=color; 117 Set the next pixel in the
// Line to COLOR
offset+=y_unit; // Move offset to next pixel
17 in y direction
error_term+=xdiff; 17 Check to see if move
77 required in x direction
if Cerror_term>0) {
// If so...
continued on next page
HE BE BB
Em 179
Build
-
Your Own Flight Sim in C++
There’ a lot going on here, so lets take a close look at this function. Six parameters are
passed to the function by the calling routine: the starting coordinates of the line (x1 and
y1), the ending coordinates of the line (x2 and y2), the color of the line (color), and a
far pointer to video memory or a video buffer (screen).
The variables xdiff and ydiff contain the differences between the starting and ending
x and y coordinates, respectively. If either of these differences is negative, the absolute
value of the difference is taken by negating the variable with the unary minus operator
(-). The variables x_unit and y_unit represent the amount by which the x and y coor-
dinates are to be incremented in their position in video RAM. The x_unit value is
always 1 or -1, depending on whether the x is
coordinate going from a lower value to
a higher one (i.e., if the line is sloping down the screen) or from
the
a higher value to a
y_unit value is always
lower one (i.e., if the line is sloping up the screen). Similarly,
320 or -320, the number of addresses between a pixel on one line and a pixel one line
(i.e., one y coordinate) below it or above it. We determine which way the line is slop-
ing in the x and y directions by checking
before taking their absolute values.
to see if xdiff and ydiff are positive or negative
The drawing of the line is performed by two loops, one for the case where the
absolute value of xdiff is greater than the absolute value of ydiff and one for the oppo-
site case. In the former, the value of x_unit is added to the pixel offset after each pixel
is drawn. In the latter case, the value of y_unit is added. The variable error_term is used
for tracking the error term. Depending on which loop is being executed either xdiff or
ydiff is added this value. The actual drawing proceeds as we described above.
to
®W¥
Listing 6-2 LINETEST.CPP
#include <stdio.h>
#include <dos.h>
#include <conio.h>
#include "bresn.h"
#include "screen.h"
All Wired Up
mm u
Notice the stairstep-like quality of the line that is drawn by this program. (See Fig-
ure 6-13.) That’ the result of approximating the line with pixels that don't fall precisely
on the line. This stairstep effect is known technically as aliasing. There is a set of
graphics techniques known as antialiasing techniques that can be used to minimize this
effect, but they are difficult to apply while drawing a line on the fly. Basically, the
techniques involve making the distinction fuzzier between the line color and the back-
ground color by drawing parts of the line in a color partway between the background
color and the line color.
functions (prototyped in STDLIB.H and TIME.H) to generate random endpoints for the
lines. The compiled program, SRNDLINE.EXE, can be found in the BYOFS\WIREFRAM
directory.
181
. 3
— ®H HH EE HEH B®
Don't blink when you run this program. The line-drawing routine is fast (as it needs to
be in order to produce convincing animation). Within less than a second, the screen
will be filled with randomly criss-crossing lines. See Figure 6-14 for a screen shot. Press
any key to make it stop.
182 =
Figure 6-14 The lines drawn by the RANDLINE program
init_Lline:
mov dx,color 2 Put pixel color in dx
mov error_term,0 ; Initialize error term
mov ax,y2 Determine sign of y2-y1
sub ax,yl
jns ypos ; If positive, jump
mov y_unit,-320 2 Else handle negative slope
neg ax ; Get absolute value of YDIFF
mov ydiff,ax ; And store it in memory
jmp next
ypos:
mov y_unit,320 2 Handle positive slope
mov ydiff,ax 7 Store YDIFF in memory
next:
mov ax,x2 7 petermine sign of x2-x1
sub ax.x1
jns Xpos : If positive, jump
mov x_unit,~1 : Else handle negative case
neg ax - Get absolute value of XDIFF
mov xdiff,ax 3 And store it in memory
jmp next?
Xpos:
mov x_unit,1 7 Handle positive case
mov xdiff,ax 2 Store XDIFF in memory
next2:
cmp ax,ydiff J Compare XDIFF (in AX) and YDIFF
je yline 3 IF XDIFF<YDIFF then count
’ in dimension
Y
Ree
mov cx,ydiff ; Get line length in cx
inc [1
yline1:
mov es:[Cbx1,dlL ; Draw next point in Line
add bx,y_unit ; Point offset to next pixel
; in Y
direction
mov ax,error_term ; Check to see if move require
; in direciton X
add ax,xdiff
mov error_term,ax
sub ax,ydiff
jc yline2 ; If not, continue
mov error_term,ax
add bx,x_unit ; Else, move Left or right
; one pixel
yline2:
Loop yline1l ; Loop until count (in CX) complete
linedone:
mov sp,bp ; Finished!
pop bp
ret
_linedraw ENDP
END
RANDLINE.IDE project. The RANDLINE EXE target uses the assembly language ver-
sion of linedraw, while the SRNDLINE.EXE uses the C++ version.
Programmers not familiar with the features of 80X86 macro assemblers may be
interested in the use of the LOCAL directive at the beginning of this program. Similar
to the ARG directive, LOCAL creates labels for local variables on the stack which can
be addressed by name, and the assembler automatically replaces the names with the
stack addresses of the variables. To make this work properly, the stack address must be
placed in the BP register and space allocated on the stack by subtracting the size of the
local variable area (contained here in the assembler variable AUTO_SIZE) from the
stack pointer.
According to Turbo Profiler for DOS (a useful program that we'll discuss in more
detail in a later chapter) this version of the linedraw() function runs almost twice as fast
as the one we wrote in C++. (A resourceful programmer might be able to optimize it
still further.) This gain in speed could produce a noticeable difference when drawing
complex wireframe objects.
a ~" Build Your Own Flight
Drawing Shapes
8 Sim in C++
Now that we can draw lines, let's use the linedraw() function to draw more complex
shapes. Listing 6-5 draws a rectangle on the display.
#include <stdio.h>
#include <dos.h>
#include <conio.h>
#include "screen.h"
#include "bresnham.h"
void main()
{
// Draw rectangle:
linedraw(130,70,190,70,15,screen);
linedraw(190,70,190,130,15,screen);
linedraw(190,130,130,130,15,screen);
linedraw(130,130,130,70,15,screen);
while (!'kbhit()); // Loop until key pressed
setmode(oldmode) ; // Reset previous video
// mode & end
}
The output of this program is shown in Figure 6-15. There’ really not much to say
about how the program works. The rectangle is drawn with four calls to the linedraw()
function, each of which draws one line of the rectangle. The vertices of the rectangle—
(130,70), (190,70), (190,130), and (130,130)—are hardcoded into the function calls.
Listing 6-6 is a similar program that draws a triangle.
AlWiredUp m
\
m
~N
oo
=m
187
uw
~"
—
Build Your Own Flight Sim in C++
-— - Wm EH HE
EEE
better, we could make the shape into a class and tell the shape to draw itself!
Fortunately, that’s not hard to do. Here’ the definition of a simple data structure and
class that we can use for holding two-dimensional wireframe shapes:
// Variable structures for shape data:
struct vertex_type { // Structure for vertices
int x,y; // X & coordinates for vertex
Y
};
class shape_type { // Class to hold shape data
protected:
int color; // Color of shape
int number_of_vertices; // Number of vertices in shape
vertex_type *vertex; // Array of vertex descriptor
public:
shape_type(int num, vertex_type *vert, int col=15):vertex(vert),
number_of_vertices(num),color(col){;}
void Draw(char far *screen);
};
The structure type that we've called vertex_type holds a vertex descriptor—the x
and y coordinates of a single vertex of a shape. The class called shape_type contains
further information about the shape of which the vertex is a part, including the color of
the lines in the shape (color), the number of vertices in the
All Wired Up
shape (number_of_ver-
tices), and a pointer to an array of vertex descriptors (vertex). The class
constructor
"ew
assigns the arguments to the class members. The default color is 15, or white, such that
it is really only necessary to provide two parameters to the constructor. We
can now
initialize the data for a shape in a pair of data initialization
statements, one to create the
array of vertices and a second to create the shape_type instance of the class:
// bata for triangle:
vertex_type triangle_array[l={
160,70, // First vertex
190,130, // Second vertex
};
130,130 // Third vertex
// Triangle:
// 3 vertices and triangle_array is Pointer to vertex array
shape_type shape(3,triangle_array);
’
As
the variable names and comments imply, these structures contain the shape data for
a triangle. We can then draw the shape with the function in Listing 6-7.
aR
The function Draw() consists primarily of a For loop that iterates through
all of the ver-
tices in the shape. The variable p2 points to the next vertex after the one currently
under consideration. In the event that the current vertex is the last, this
variable wraps
around to 0, so that the final vertex is automatically connected to the first. The
linedraw() draws
a
line from the current vertex to the p2 vertex.
SCREEN.ASM, will draw a triangle on the mode 13h display using the linedraw()
function.
//
Wrap
Draw
to O:
if (p2>=number_of_vertices) p2=0;
All
"a
linedraw(vertex[il.x,vertex[il.y,
vertex[p2l.x,vertexCp2l.y,
color,screen);
}
Transforming Shapes
do something
Now that you've learned to draw two-dimensional shapes, you'll want to
learn to alter those shapes in
interesting with them. In the rest of this chapter, you'll
three-dimensional shapes you'll deal with in later
ways that will be relevant to the
chapters.
In Chapter 2, several operations were introduced that could be performed on two-
dimensional shapes. Among these were scaling, translating, and rotating.
Recall that scaling refers to changing the size, or scale, of the shape. Scaling can
be
used to make the shape larger or smaller or to alter the proportions of the shape.
We'll only use scaling in this chapter to alter the overall size of the shape, not its pro-
portions.
of three-
Translating involves moving the shape around. Later, in the discussion
dimensional graphics, we'll use translation to move objects through three-dimensional
two-dimensional shape around on the
space. For now, however, we'll use it to move a
screen.
In two dimensions
Rotating refers to changing the physical orientation of the shape.
that essentially means rotating the shape clockwise and counterclockwise about an axis
how to rotate three-dimensional
extending outward from the screen. Later, you'll see
shapes about other axes as well.
Local Coordinates
of
Until now, our programs have defined shapes in terms of their screen coordinates
their vertices. However, since the scaling, rotating, and translating operations that
base set
we're about to perform will change those coordinates, it’s necessary to have a
of coordinates to return to before we alter a shape’s coordinates. Thus, we need to give
each shape two sets of coordinates: local coordinates and screen coordinates.
The local coordinates will be the coordinates of the vertices of the object relative to
a specific origin point, usually the center of the object (though
it can be any other point
nen
For instance, using a local coordinate system, we can describe a triangle as having
vertices at coordinates (-10,10), (0,-10), and (10,10), relative to a local origin at the
center of the triangle, as shown in Figure 6-17. You'll notice that now we have negative
coordinates as well as positive coordinates. Just as standard Cartesian coordinate
Ssys-
tems include negative coordinates below and to the left of the (0,0)
origin point (see
Chapter 2), so vertices in a local coordinate system can have negative coordinates rel-
ative to the local origin of the object. This simply means that the vertex is above
or to
the left of the local origin.
| Since coordinates on the video display are generally treated as positive, we'll need to
translate the local coordinates on this triangle to points within the normal set of
screen
coordinates before it can be fully visible. Objects that have vertices with negative coor-
dinates can still be displayed, but all negative vertices will be off the edges of the
screen.
to
In order take local coordinates into account, we'll need to create a new data struc-
ture to describe shapes. Each vertex will require two descriptors, one for local coordi-
nates and a second for screen coordinates. We can initialize the first descriptor in the
program code, but the second will need to be initialized by the function that performs
the translation. Here’ the definition for such a structure:
// Variable structures to hold shape data:
struct vertex_type {
// Structure for individual
// vertices
continued on next page
mE BE BE
EEN he
};
class shape_type { // Class to hold shape data
protected:
int color; // Color of shape
int number_of_vertices; // Number of vertices in shape
vertex_type *vertex; // Array of vertex descriptor
public: int
shape_type(int num, vertex_type *yert,
col=15):vertex(vert),
number_of_vertices(num),color(col){;}
void Draw(char far *screen);
void Translate(int xtrans,int ytrans);
¥;
// Data for shapes:
vertex_type rectangle_arrayl1={ // Data for rectangle
{0,0, // First vertex
0,0},
{0,20, // Second vertex
0,20},
{20,20, // Third vertex
20,203,
{20,0, // Fourth vertex
20,0}
};
You'll note that only the vertex descriptor has been changed. The definition of
shape_type is the same as before.
Translating
To translate an object to a new position on the screen, we need two numbers repre-
senting the translation values for both the x and y coordinates of the object—that is,
the amount along the x and y axes by which the object will be moved. These transla-
tion values become the x and y coordinates of the local origin of the object and
all of
the vertices will be treated as though they were relative to this point. (See Figure
6-18.) Once we have the translation values, we can get the screen coordinates of the
vertices by adding the translation values to the x and y coordinates of each vertex, the
x translation value to the x coordinate and the y translation value to the y coordinate,
like this:
new_X=old_X + X_translation;
new_Y=old_Y + Y_translation;
Listing 6-9 introduces a function that will perform this translation for us.
194 ®
All Wired Up m
"a.
id
Eat SE Figure 6-18 The
triangle
fn
mi J from Figure 6-17, with its
Jota: sed | ici
od
nal] Bod
local origin translated to
coordinates (2,-3)
This function takes the local coordinates of the object (stored in the Ix and ly fields) and
adds the translation values to them to obtain the screen coordinates (stored in the
sx
and sy fields). We can call this routine from C++ to translate a shape to
screen coordi-
nates (xtrans,ytrans).
Listing 6-10 shows a program that translates a rectangle to coordinates (150,190).
WWF
Listing 6-10 TRANSLAT.CPP
#include <stdio.h>
#include <dos.h>
#include <conio.h>
#include "bresnham.h"
#include "screen.h"
continued on next page
Build Your Own Flight Sim in C++
if (p2>=number_of_vertices) p2=0;
// Draw Line from this vertex to next vertex:
linedraw(vertex[Lil.sx,vertexLil.sy,
vertexCp2l.sx,vertexCp2l.sy,
color,screen);
}
vertex[il.sx = vertex[il.lx+xtrans;
vertex[il.sy = vertex[Lil.ly+ytrans;
AlWiredUp m mm
}
// Rectangle:
// 4 vertices and
rectangle_array is Pointer to vertex array
// draw it
bright red (12)
in
shape_type shape(4, rectangle_array, 12);
void main()
{
char far *screen =
(char far *)MK_FP(0xa000,0); // Create pointer to
cls(screen);
// video RAM
int oldmode=*(int *IMK_FP(0x40,0x49);
// Clear screen
// Save previous
// video mode
setgmode (0x13);
shape.Translate(150,90);
// Set mode 13h
shape.Draw(screen); // Draw shape on
// display
while ('kbhit()); // Wait for key, then
// terminate
setgmode (oldmode) ; // Reset previous video
}
// mode & end
Scaling
Now that we've moved an image to a new position on the display, the next
step is alter-
ing its size. Scaling the image can be implemented almost as easily as translating it. First
you multiply the coordinates of each vertex of the image by the same scaling factor.
~~" Build Your Own Flight Sim in C++
mm EB
EER
new_X=old_X scale_factor;
*
new_Y=old_Y scale_factor;
*
{
for (int i=0; i<number_of_vertices; i++) {
vertexCil.sx=vertex[il.lx*scale_ factor;
vertex[il.sy=vertex[il.ly*scale_factor;
}
}
Listing 6-13 shows a complete program that scales and translates a rectangle.
class shape_type {
// Structure to hold shape data
private:
int color; // Color of shape
int number_of_vertices; // Number of vertices in shape
vertex_type *vertex; // Array of vertex descriptor
public:
continued on next page
Build Your Own Flight Sim in C++
if (p2>=number_of_vertices) p2=0;
// Draw line from this vertex to next vertex:
linedraw(vertex[il.sx,vertex[Lil.sy,
vertex[p2l.sx,vertexCp2l.sy,
color,screen);
}
}
vertex[Lil.sx += xtrans;
vertex[il.sy += ytrans;
}
}
200 =
All Wired Up
"Em
vertex_type triangle_array[J={
{0,-10, 17 First vertex
0,03,
{10,10, /7/ Second vertex
0,01,
{-10,10,
0,0}
// Third vertex
};
// Triangle:
// 3 vertices and triangle_array is Pointer to vertex array
shape_type shape(3, triangle_array);
void main()
{
char far *screen =
(char far *)MK_FP(0xa000,0); // Create pointer to
// video RAM
cls(screen); // Clear screen
int oldmode=*(int *)MK_FP(0x40,0x49); // Save previous
// video mode
setgmode (0x13) ; // Set mode 13h
201
Build Your Own Flight Sim in C++
Rotating
The final trick that we're going to ask our two-dimensional shape-drawing program to
perform is rotation, but this is also our most sophisticated trick. Rotating a shape is not
exactly an intuitive operation. A programmer without a knowledge of trigonometry
might be hard put to come up with an algorithm for rotating a shape to any angles
other than 90, 180, and 270 degrees.
We saw back in Chapter 2 that there are methods for rotating shapes to arbitrary
angles. They require that we use the trigonometric sine and cosine functions, which
fortunately happen to be supported by routines in the Borland C++ library MATH. H.
The formula for rotating a vertex to an angle of ANGLE radians is
new_x=old_x * cosine(ANGLE) - old_y * sine(ANGLE);
new_y=old_x * sine(ANGLE) + old_y * cosine(ANGLE) ;
(We looked at the use of radians in Chapter 2 as well.) Based on those formulae,
Listing 6-14 is a function for rotating a shape to an arbitrary angle.
{
int x,y;
// Rotate all vertices in SHAPE
The angle passed to this routine in the parameter ANGLE must be in radians. Listing
6-15 is a program that not only will translate and scale a shape, but will also rotateit
to
an arbitrary angle.
202 m
SW
//
//
A
Listing 6-15 ROTATE.CPP
program to demonstrate rotation of
shape to an arbitrary orientation
a two-dimensional
AlWiedUpw
w
w
#include <stdio.h>
#include <dos.h>
#include <conio.h>
#include <math.h>
#include "bresnham.h"
#include "screen.h"
int const DISTANCE=10;
vertex[il.sx += xtrans;
vertex[Lil.sy += ytrans;
}
}
vertex[il.sx = vertex[il.lx*scale_factor;
vertex[il.sy = vertex[il.ly*scale_factor;
}
}
vertexLil.sy = y;
}
}
204 ©
All Wired Up m
"wu
// Triangle:
// 3 vertices and
triangle_array is Pointer to vertex array
// to be drawn in bright cyan (11)
shape_type shape(3, triangle_array,11);
void main()
{
char far *screen =
(char far *)MK_FP(0xa000,0); // Create pointer to
cls(screen);
// video RAM
// Clear screen
int oldmode=*(int *)MK_FP(0x40,0x49); // Save previous
// video mode
setgmode (0x13); // Set mode 13h
shape.Scale(2); // double the size
shape.Rotate(0.261);
shape.Translate(150,90);
// rotate shape 30 degrees
shape.Draw(screen); // Draw shape on
// display
while C('kbhit()); // Wait for key, then
//
}
setgmode (oldmode)
; terminate
// Reset previous video
// mode end &
HE BE BH
B= 205
. —
Build Your Own Flight Sim in C++
operations in
the
and
cation operations—that is, there are matrices
same three
involve
game.)
a
rotating
that
lot
As
can
operations
|
are
be
we've
"EE
the
just
EERE
third coordinate for each vertex. This third coordinate, included only to make the math
work correctly, will be ignored by the drawing routines and will always be set to 1.
Here's the new data structure for holding this revised version of the vertex descriptor:
// Structure for individual vertices:
struct vertex_type {
Local coordinates of vertex
int lx,Lly,Lt; //
int sx,sy,st; // Screen coordinates of vertex
Y;
The only difference between this structure and the previous one is that we've now
added
a third field to each descriptor—It and st. These fields will hold the 1 values
required by the matrix routines that we'll be using. Here, for instance, is
the new data
for the rectangle shape:
vertex_type rectangle_arrayl[l={ // Data for rectangle
{-10,-10,1, // First vertex
0,0,12,
{10,-10,1, // Second vertex
0,0,13;
{10,10,1, // Third vertex
0,0,1),
{-10,10,1, // Fourth vertex
0,0,1}
};
Chapter 2. Now all we need are
We used the algorithm for multiplying matrices in
the matrices for performing specific operations. The matrix for translating a two-
dimensional shape is
206 =
1
0
xtrans
0
1
ytrans
0
0
1
descriptor
w
to translate the vertex to coordinates xtrans and ytrans. First, however, lets look at the
matrices for the other operations that we need to perform. Here's the matrix for
per-
forming scaling:
scale_factor 0 0
0
0
scale_factor 0
0 1
smatrix[21L01= 0;
smatrixC21C1]= 0;
smatrix[21[2]1= 1;
HE BE EB
Em 207
— EA EERE
Teee
SAL
—
RRES
This function used the matrix multiplication routines to multiply the scaling and
rotation-translation matrices (created in the first part of the routine) together, and
then to multiply the resulting transformation matrix by each vertex descriptor in the
shape. Now Listing 6-17 uses this routine to translate, scale, and rotate a rectangle.
208 =
"Ew
Al Wired Up
slListing
// ROTATE2.CPP
// A
6-17 Matrix transformation program
if (p2>=number_of_vertices) p2=0;
continued on next page
HE BE BE
EB 209
~~"
PieBuild Your Own Flight Sim in C++
— "- EN EEE
continued from previous page
smatrixC11C0]= 0;
smatrixC11C11= scale_factor;
smatrixC11L21= 0;
smatrix[21[0]1= 0 “ee
smatrix[21[1]1= 0 r-
smatrixC21[21= 1
’.
matrixCiJLjl1=0;
for(int k=0; k<3;k++)
matrixCilCjl+= smatrixCilCk] * rtmatrixCk1ICj1;
210 =
All Wired Up
"ow
for(int v=0; v< number_of_vertices;v++) {
// Rectangle:
// 4 vertices and rectangle_array is Pointer to vertex array
shape_type shape(4, rectangle_array);
void main()
{
char far *screen =
(char far *)MK_FP(0xa000,0); // Create pointer to
// video RAM
cls(screen); // Clear screen
int oldmode=*(int *)MK_FP(0x40,0x49); // Save previous
// video mode
setgmode (0x13); // Set mode 13h
continued on next page
HE BE BE BE B=
211
— :
"Build Your Own Flight Sim in C++
212 ®
From Two
Dimensions
to Three
Working with three dimensions much like dealing with only two. Things
is
the same. Its that
get a bit more complicated, but the concepts remain pretty much
interesting—and chal-
extra dimension that makes three-dimensional animation so
would be pretty flat, in more ways
lenging. Without the third dimension, 3D animation
than one.
In computer graphics, the third dimension is always depth, the
dimension that runs
perpendicular to the plane of the computer display even though the display is
itself flat,
available. And that, of course, is what
at least until Star Trek—style holodecks become
three-dimensional world
makes three-dimensional graphics so challenging: rendering a
that the viewer can still tell that the third
on a two-dimensional screen in such a way
the in which three dimensions will
dimension is there. Lets start by considering way
be represented in the internal memory of the computer.
The z Coordinate
in terms of the x and y
In Chapter 6, simple two-dimensional shapes were represented
with three-dimensional
coordinates of their vertices. We can do much the same thing
graphics, but we'll need a third coordinate to represent the
third dimension. In keeping
215
un
ee
~~" Build Your Own Flight Sim in C++
to start thinking
chapters, we introduced two
system used to draw objects on the display
and the local coordinate system of the objects themselves. (Recall
nates are plotted from a fixed origin point on the while
that screen coordi-
coordinates are
the center of an object.) Now we'll introduce a third
coordinate system that will bridge the three-dimensional
tems—the world coordinate system, which provides
gap between these two sys-
us with a way of referencing
points in space within an imaginary three-dimensional world we'll be
building inside
the computer.
There’ nothing new about the idea of a world coordinate
system; such systems have
been around a lot longer than
computers. It is traditional, for instance, to define points
on the surface of the earth in terms of a two-dimensional coordinate
system, in which |
Cubic Universes
is spherical—
In both of those examples, the space defined by the coordinate system
in one case, the surface of a globe and in the other case,
the orbital space surround-
to define cubical spaces, which
ing the sun. It’s much easier mathematically, however,
The worlds that we'll
are organized according to a three-dimensional Cartesian grid.
design in this book will always be arranged as perfect
cubes, with every point inside
the cube defined by its position relative to three Cartesian axes.
of all-powerful alien
Its not hard to imagine such a world. Let's suppose that a force
invaders decided to capture a portion of the earth’ surface inside an invisible force
field and tow it back to its home planet as a museum exhibit. Suppose
further that the
of the earth’s surface
force field measures 100 miles on each side, so that the segment
and 100 miles deep. If we define
captured inside is 100 miles wide, 100 miles long,
a point within this captive section of the
earth as the origin point of a coordinate sys-
three coordinates measured relative
tem, then any other point can be represented by
itself would be at coordinates (0,0,0)
to that origin. For instance, the origin point
3 miles north (where
while a position 10 miles west (where east-west is the x axis),
~~ Build Your Own Flight Sim in C++ -# BH BH
Em BB ms
north-south is
they axis), and 0.5 mile above (where up-down is the z axis) would be
at coordinates (10,3,0.5) as in Figure 7-3. (We could
just as easily define the z axis as
the north-south axis, the y axis as the up-down
axis, and the x axis as the east-west
axis. It really doesn't matter, as long as we use one
system consistently.)
That's how we'll design our worlds, as cubic sections of
space with an origin point
at dead center. All points in such a world can then be defined
by coordinates relative
to that origin. A point 400 units in the x direction from the
origin, 739 units in the y
direction, and 45 units in the z direction would be at coordinates (400,739,45). How
large are these units? That’ for us to decide. In the previous
example, we used miles
as our unit, but miles are too large to do the job in
many cases. More likely, our unit
will be something on the order of a foot or even an inch. In
fact, there’ no reason that
the unit has to be precisely equivalent to
any real-world unit of measurement; we can
always define a constant within our program that specifies the conversion factors
to
translate these world units (as we'll refer to them from
now on) into traditional units
of measurement. For instance,
if we choose a world unit that is
ters, we can define a floating-point constant called CENTIMETERS_PER_UNIT
equal to 7.3 centime-
and
set it equal to 7.3. We can then use this constant
world units and metric units of measurement.
to translate back and forth between
point of view so that it points north or south, east or west, up or down, or some point
in between; theres no reason that the world coordinates have to correspond to screen
coordinates. (In a later chapter, however, we'll learn a method for aligning the screen
axes with the world axes to simplify drawing a screen representation of our three-
dimensional world.) We can just as easily choose to place our world axes at any arbi-
trary angle relative to the traditional axes of our imaginary world. However, it’s proba-
bly best if
we align the world coordinates axes with some traditional axis—for instance,
we'll probably want to align the x axis with the north-south axis or the east-west axis
rather than the east-northeast—-west-southwest axis.
HE BE BE
EB 219
Build Your Own Flight Sim in C++
HE BE BE BE
BEB
Figure 7-4 The difference between local, world, and screen coordinates
(a) Airplane position measured relative to local coordinate system of field it is about to
land on
220
From Two Dimensions to Three
mm u |
(¢) Image of same airplane on computer video display, measured relative to screen
coordinate system of display
This data structure holds three sets of coordinates, as opposed to the two sets of coor-
dinates in our previous definition of vertex_type. They include the dummy t coordi-
nates that were added in the last chapter to make the matrix math come out right. The
first set (Ix, ly, Iz, and It) represents the local coordinates of the object, defined relative
to the local origin of the object (which, as you'll recall from the last chapter, is usual-
ly at the center of the object). The second set (wx, wy, wz, and wt) represents the world
coordinates of the object, which are the coordinates of the vertices of the object rela-
tive to the origin of the world in which we'll be placing the object. (More about this
later.) The third set of coordinates (sx and sy) are the screen coordinates of the ver-
tices. This last set only includes the x and y coordinates, since the video display
and only offers two dimensions for our coordinate system. Later, we'll develop a func-
is
flat
tion that translates the world x, y, and z coordinates of a vertex into screen x and y
coordinates.
With this new structure for holding three-dimensional vertices, it should be easy
to develop a class to hold three-dimensional wireframe shapes. In fact, that class will
look exactly like the one we used in the last chapter to hold two-dimensional wire-
frame shapes. The class (along with the structure for holding line descriptors) looks
like this:
0,0,0,1,
0,0,0,13,
{10,-10,10,1, // Vertex 1
0,0,0,1,
0,0,0:13,
€10,10,10,1, // Vertex 2
0,0,0,1,
0,0,0,13,;
{-10,10,10,1, // Vertex 3
0,0,0,1,
0,0,0,1},
{-10,-10,-10,1, // Vertex 4
0,0,0,1,
222 ®
0,0,0,1%},
{10,-10,-10,1, // Vertex
From Two Dimensions to Three
5
"" .
0,0,0,1,
0,0,0,1%,
{10,10,-10,1, // Vertex 6
0,0,0,1,
0,0,0,1%},
{-10,10,-10,1, // Vertex 7
0,0,0,1,
0,0,0,1%},
};
Line_type cube_Llines[]1={
{0,1}, // Line O
{1,2}, // Line 1
2,34, // Line 2
{3,0}, // Line 3
{4,5}, // Line 4
{5,6}, // Line 5
{6,7}, // Line 6
{7,4}, // Line 7
{0,4}, // Line 8
{1,5}, // Line 9
{2,6}, // Line 10
}
3,7) // Line 11
223
Build Your Own Flight Sim in C++
BH BH B®
B—
BH
224
Figure 7-5 The cube described by the data
shape descriptor
in the
®
This is a pretty familiar concept. When you watch a film in a movie theater, you
see images of a three-dimensional world literally projected onto the flat surface of the
movie screen. And, though this image is flat, you probably have no trouble accepting
that the world depicted in the image is three-dimensional. Thats because there are
visual cues in the flat image that indicate the presence of a third dimension.
The most important of these cues is perspective, the phenomenon that causes objects
viewed at a distance to appear smaller than objects that are nearby. Not only do objects
to
at a distance tend to seem smaller, but they also tend to move closer the center of your
visual field. You can demonstrate this for yourself by standing in a large open area, such
From Two Dimensions to Three
"" wo
as a field or a parking lot, and looking at a row of distant (but not too distant) objects.
Fix your gaze on an object in the center of the row and walk toward that
object. As you
do so, notice that not only do all of the objects in the row seem to larger (that is,
grow
to occupy more of your visual field), but objects toward the right andleft ends of the
row move farther and farther toward the right and left edges of your visual field, until
you have to turn your head in order to see them. (See Figure 7-6.) If you reverse the
process and walk backwards (which might be awkward in a crowded parking lot), the
objects at the left and right ends of the row will move back toward the center of
your
visual field.
In a wireframe image, we can approximate this effect by moving the origin of our
coordinate system into the center of the display (where it will approximate the center
of the users visual field) and altering the x and y coordinates of objects so that more
distant vertices will move toward the origin and closer vertices will move toward the
edges of the display. This will give us the effect of perspective. The formulas for mov-
ing the x and y coordinates of a vertex toward or away from the origin based on its
distance from the viewer are
SCREEN_X = WORLD_X / WORLD_Z
SCREEN_Y = WORLD_Y / WORLD_Z
In English, this tells us that the screen x coordinate of a vertex is equal to the world x
coordinate of that vertex divided by the world z coordinate of that vertex. Similarly,
the screen y coordinate of a vertex is equal to the world y coordinate divided by the
world z coordinate. This formula produces a perspective effect in this fashion:
Dividing the x and y coordinates by a positive z coordinate value will cause the x and
y coordinates to grow smaller—that is, to move closer to the origin of the coordinate
system. The larger the z coordinate, the smaller the x and y coordinates will become
after this division—that is, the closer they will move to the origin. Because we'll be
moving the origin to the center of the display, this will cause the vertices of distant
objects (those with larger coordinates) to move farther toward the origin than the ver-
tices of nearer objects (those with smaller z coordinates). This movement of vertices
toward the origin according to their z coordinates will produce the illusion of per-
spective, as shown in Figure 7-7. Note how the changing positions of the dots corre-
spond to the changing positions of the trees in Figure 7-6.
However, the above formulas tend to produce too much of a perspective illusion.
Vertices would zoom so rapidly toward the origin that most objects would be reduced
to mere pinpoints on the display. The reason for this is that the viewer is assumed in
these formulas to have his or her face smashed up against the video display, thus
approximating the viewpoint of a wide-angle camera lens. To move the viewer back
away from the display (and, not incidentally, to narrow the angle of our imaginary
lens), we must add an additional factor, VIEWER _DISTANCE:
SCREEN_X VIEWER_DISTANCE * WORLD_X / WORLD_Z
SCREEN_X VIEWER_DISTANCE * WORLD_Y / WORLD_Z
225
a" mE EEE
(¢) A close-up view of the center tree from Figure 7-6(a). Note that the trees at
both ends of the row have moved entirely out of the visual field
226 =m
From Two Dimensions to Three []
" u
Figure 7-7 Creating the illusion of
perspective
(b) The dots from Figure 7-7(a), moving away from the origin of the system
in both the x andy dimensions
HE BE BE
EB 227
|
build -— Your Own Flight Sim in C++
—
There is no fixed value for VIEWER_DISTANCE. Through some basic math and
trigonometry, it can be shown that when it is equal to one-half the
the field of view is 90 degrees. In the programs that follow,
field of
we'll
view
usually
larger
"EEE
The function is short and its inner workings fairly straightforward. The for loops
iterate through all of the vertices in the wireframe object pointed to by the parameter
shape, and the code within the loop translates the world x and y coordinates of each
vertex into screen x and y coordinates, using the formulas we looked at earlier. We cre-
ate a pointer to the vertex structure before the loop starts and reference the specific
coordinates relative to that pointer, which is called vptr. For instance, the world x coor-
dinate of the vertex is referenced as vptr->wx, where the -> operator is used to access
228 ®
From Two Dimensions to Three m
mm
individual fields within the structure pointed to by vptr: This
isnot only a convenience
pointer (one which saves the programmer typing), but it also makes the code slightly
faster. If we used the array operator, | |, and index into the
array, every occurrence of
the expression vertex[v] would require the computation of the address that
was v num-
ber of elements into the vertex array. That would happen three times in each of the two
lines in our code above, or six times every loop iteration:
vertex[vl.sx=distance*vertex[Cvl.wx/vertexCvl.wz;
vertex[vl.sy=distance*vertexCvl.wy/vertex[vl.wz;
Although six addition computations on today’s computers are done in the fraction of
a wink, the effect of saving any CPU processing time
is
cumulative. A little here,
tle there, and all of a sudden, we have a fast set of routines. Further
lit-
a
exploration of
optimizations will happen in depth in a coming chapter, but the use of a pointer as
replacement for an array iteration is fairly common, easy to read, and used in three of
the Shape class functions.
Using the Shape::Project() function, we can create a set of screen x and y coordi-
nates that can then be drawn on the video display. Before we start drawing 3D wire-
frame objects, however, lets create a set of functions to scale, translate, and rotate
those objects.
Transformation in Three
Dimensions
In the last chapter, we performed two-dimensional transformations on two-dimensional
wireframe objects. The three transformations performed in that chapter were scaling,
translation, and rotation. In this chapter, we'll perform those same three transformations.
However, the three-dimensional versions of those transformations need to be slightly dif-
ferent from the two-dimensional versions. For instance, two-dimensional scaling involved
changing the size of a shape in the x and y dimensions, while three-dimensional scaling
involves changing the size of a shape in the x, y, and z dimensions. Similarly, two-dimensional
translation involved moving a shape in the x and y dimensions. Three-dimensional
translation involves moving a shape in the x, y, and z dimensions.
The transformation that changes the most when extended into the third dimension
is rotation. In the last chapter, we rotated objects around their local point of origin.
There was really only one type of rotation that we could perform. In three dimensions,
however, three types of rotation are possible: rotations about the x axis, rotations
about the y axis, and rotations about the z axis.
Although we didn't use the term in the last chapter, the type of rotation that we
performed in two dimensions was rotation about the z axis. (We didn’t call it that
because we hadn't introduced the z axis yet.) The z axis, remember, can be imagined
Build Your Own Flight Sim in C++
" EE EEE
the
as a line extending through the screen of the display perfectly perpendicular to
surface of the display. An object rotating about the z axis
is
not rotating in the 2 dimen-
sion; in fact, that is the only dimension that itis not rotating in. An object rotating
about the z axisis rotating in the x and y dimensions, which are the only dimensions
available to a two-dimensional shape. (See Figure 7-8.)
Three-dimensional objects, however, can also rotate in the x and z dimensions and
in the y and z dimensions. The former type of rotation is a rotation about the y axis
and the latter is a rotation about the x axis. (See Figures 7-9 and 7-10.) We can com-
bine rotations in all three dimensions to form a single rotation that will put a three-
dimensional object into any arbitrary orientation.
When we performed these transformations in the last chapter, we used one master
function to perform scaling, translating, and rotating. Lets try a slightly different
approach this time around. Instead of one master function, let’ perform each of these
transformations with a separate function much in the same way that the functions
were introduced. The advantage of using a separate function for each is that we don't
have to perform all of the transformations on an object we if don’t want to. (We could
also use separate functions for x, y, and z rotation if we wanted to, but we won't go
quite that far in this chapter.) The disadvantage of using separate functions is that, if
we're not careful, we'll lose the advantage of creating a single master matrix, which we
could use to transform every vertex in an object with a single matrix multiplication.
An
a global multiplication matrix called
matrix that will be available to all functions in our package of wireframe transforma-
tion routines. Fach of the individual transformation functions will then create their
own special-purpose transformation matrices and concatenate them (that is, multiply
them) with the master transformation matrix. Thus, the master transformation matrix
will represent the concatenation of all the special-purpose transformation matrices. To
perform the actual transformation of the object, we will need a master transformation
function, which we'll call transform(), to multiply all of the vertices in an object with
the master concatenation matrix. Since this will operate on the vertices of the Shape
object, we'll make this master transformation function part of the Shape class. Before
we call this function, however, it will be necessary to call all of the special-purpose
transformation functions. We'll also need a function, which we'll call inittrans, to ini-
tialize the master transformation matrix to the identity matrix prior to the first con-
catenation. And, since we'll be doing a lot of matrix multiplication, it would be a good
idea to create a function to perform generic multiplication of 4x4 matrices (the only
kind of matrices that we'll be dealing with, other than the 1x4 vectors of the vertex
descriptors).
Listing 7-2 shows what the inittrans() function looks like.
This function doesn’t need much explanation. Recall from Chapter 2 that all positions
in an identity matrix are equal to 0 except those along the main diagonal, which are
equal to 1. The inittrans function sets matrix equal to such an identity matrix. This is
roughly equivalent to initializing an ordinary variable to 0.
232 ®
The generic 4x4 matrix multiplication routine
From Two Dimensions to Three
is in Listing 7-3.
CI
‘Listing 7-3 The matmult() function
void matmult(float result[41[41,float mat1L41C41],
float mat2[41C41)
{
This function uses a standard matrix multiplication algorithm involving three nested
for loops, similar to the one we looked at back in Chapter 2. The algorithm
accepts
three parameters. The first is the matrix in which the result of the multiplication is to
be returned to the calling routine. The second and third are the two matrices to be
multiplied.
For reasons that will become clear in a moment, we will also need a function that
will copy one matrix to another matrix. We'll call that function matcopy(). The text is
in Listing 7-4.
NE
The scale() Function
In the coming listings, you'll recognize much of the code in the transformation func-
tions from the last chapter. For instance, the scale() function creates a scaling matrix
and concatenates it with the master transformation matrix. The only difference between
the scaling code here and thatin the last chapter—aside from the fact that the code is
now isolated in a separate function—is that the actual multiplication is performed by
Le
our matmult() function. The code is in Listing 7-5. Its result is shown in Figure 7-11.
234 ©
From Two Dimensions to Three m
uo.
matmult(mat,smat, matrix);
matcopy(matrix,mat);
}
If you remember the scaling code from the last chapter, this function shouldn't require
any explanation. First,
a scaling matrix is created, using the parameter sf as a
factor, and then the matmult() function is called to concatenate it with the
scaling
master
transformation matrix. The result of concatenation is stored in a matrix
temporary
called mat. Finally, matcopy() is called to copy the contents of the temporary
mat back into the master transformation matrix.
matrix
235
on
Build Your Own Flight Sim in C++
HE BE
BE
EE
// Y
axis, and AZ radians on the Z axis
float mat1C41C4];
float mat2C41C41];
236 ®
From Two Dimensions to Three m
. n
xmatl[21C31=0;
xmatL3IC0J=0; xmatC31C11=0; xmatC31C2]1=0; xmat[31C31=1;
// Concatenate this matrix with master matrix:
matmult(mat1,xmat, matrix);
// Initialize Y rotation matrix:
ymatLOIL0J=cos(ay); ymat[L01C11=0; ymat[LOJL21=-sinCay);
ymatL0IL31=0;
ymatL11001=0; ymatC1IC11=1; ymatC11CL21=0; ymat[11[31=0;
ymat[21[0J=sin(ay); ymat[21C11=0; ymat[21[21=cos(ay);
ymat[21[31=0;
ymatL31L0J=0; ymatC31C11=0; ymat[31C2]1=0; ymat[31[31=1;
// Concatenate this matrix with master matrix:
matmult(mat2,ymat,mat1);
// Initialize Z rotation matrix:
zmatL01L0l=cos(az); zmat[OIC1l=sinCaz); zmat[0JIL21=0;
zmat[0IC31=0;
zmatL1JL0J=-sin(az); zmatL11C1]=cos(az); zmat[11[21=0;
zmat[11C31=0;
zmat[21L01=0; zmat[21C11=0; zmatC[21C21=1; zmat[21[31=0;
continued on next page
HE BE BE
EB 237
"EE EEE EE
= Build Your Own Flight Sim in C++
This function takes three parameters: ax for the angle of rotation on the x axis, ay for
the angle of rotation on the y axis, and az for the angle of rotation on the z axis. The
function creates a rotation matrix for each of these rotations. (See Chapter 2 for the
basic rotation matrices for x and y axis rotations.) One by one, these matrices are con-
catenated with the master transformation matrix.
vptr=>wx=vptr->lx*matrixC0ICL0J+vptr->Lly*matrixC11C0]
+vptr->lz*matrixC21C01+matrixC31L01;
vptr=>wy=vptr->Lx*matrixC0IC1I+vptr->Lly*matrixC11C11]
+vptr->lz*matrixC2IC11+matrixC31C1];
vptr=>wz=vptr->lx*matrixC01C2]+vptr->Lly*matrixC11L[2]
+vptr->lz*matrixC21C2]1+matrixC31C2];
}
Since this function is a Shape class function, the object which calls the function con-
tains a description of the three-dimensional wireframe object to be transformed. The
transform() function uses a for loop
to
iterate through all of the vertices of the object
and multiply each one by the master transformation matrix.
Because the master transformation matrix is unchanged by this function, we can
multiply several objects by the same transformation matrix. This can produce partic-
ularly efficient results
if
From Two Dimensions to Three m_
239
cs Sain C++ LL BB I Ee
=
:
To move the origin to this location, we merely need to add these values to the x
and y coordinates, respectively, of the screen coordinates before drawing lines between
these points. The three-dimensional Draw() function performs this task while calling
linedraw to draw the “wires” of the wireframe shape. It’s not necessary to perform any
specifically three-dimensional trickery while drawing our three-dimensional wire-
frame object, since the screen coordinates have already been translated into two
dimensions by the Project() function. Thus, the Draw() function is actually drawing a
two-dimensional shape that has undergone adjustments to create the illusion of 3D
perspective.
Drawing a Cube
Now let's write a calling routine that can be linked with the WIRE.CPP file and that
to
will use the above functions rotate and draw a three-dimensional wireframe object.
Such a routine is shown in Listing 7-10.
240 ®
From Two Dimensions to Three
m mm
This code assumes that we've already defined and declared the data for a three-dimensional
shape called shape (as we did earlier in this chapter, using the data for a cube). It then sets
up the video mode; calls the scale, rotate, and translate functions; transforms the shape; pro-
jects it into two dimensions; and calls Draw() to put it on the display. This program appears
on the disk as SDWIRE.EXE. Run it and you'll see a three-dimensional wireframe cube
appear on the screen, as in Figure 7-14. This shape may appear
since it5 difficult to tell immediately how a wireframe shape
is
a little bewildering at first,
oriented in space, but if you'll
stare at the image for a moment, you'll see that
relative to the viewer.
it represents a cube that is partially rotated
I recommend that
you load the file 3DWIRE.IDE into the Borland C++ project
manager and recompile this program using different parameters for the function calls.
In particular, you should fool around with the distance
parameter for the Project()
function and the various rotating, scaling, and translating parameters. Varying these
parameters will at times produce odd and displeasing results, and at others, produce
interesting effects. A couple of variations may produce runtime errors, most likely
because of divisions by 0; we'll add error-checking code in later chapters to guard
against this. Still other variations will cause parts of the wireframe shape to run off the
edge of the picture. Since we have yet to implement a system for clipping the image
at the edge of the window in which we have drawn it, this will cause the image to
wrap around to the other side of the display.
HE EB BE BE
= 241
. Build Your Own Flight Sim in C++
Ne
Ne
Listing 7-11 The putwindow() function
putwindow(xpos,ypos,xsize,ysize, offset, segment)
Move rectangular area of screen buffer at offset,
3 ® Nm =m
=
8B
Ne
segment with upper left corner at xpos,ypos, width
Ne
xsize and height ysize
_putwindow PROC
ARG xpos:WORD,ypos:WORD,xsize:WORD,ysize:WORD,\
buf_off:WORD,buf_seg:WORD
push bp
mov bp, sp
push ds
push di
push si
mov ax,ypos ; Calculate offset of window
mov dx,320
mul dx
add ax,x1
mov di,ax
add ax,buf_off
mov si,ax
mov dx,0a000h ; Get screen segment in ES
mov es, dx
mov dx,buf_seg ; Get screen buffer segment in DS
mov ds, dx
mov dx,ysize ; Get Line count into DX
cld
ploop1:
mov cx,xsize ; Get width of window into CX
shr ex; 1
This function takes five parameters: the x and y screen coordinates of the upper left
corner of the screen “window” to be moved, the width and height of the window in
242 ®m
From Two Dimensions to Three
CI "
pixels, and a far pointer to the video buffer from which the window is to be moved.
Now we can produce animation within a rectangular window in the offscreen display
buffer and move only that window to the real display, without wasting time copying
extraneous information.
HE BE BE BE Bm
243
: Pst
Build Your Own Flight Sim in C++
}
putwindow(0,0,320,200,screen_buffer);
setmode(oldmode) ;
if( screen_buffer)
//
//
Reset
// mode
Put on screen
-
previous video
& end
EE EERE
This program is on the disk as CUBE.EXE. Run it and you'll see a cube similar to
the
one displayed by 3DWIRE.EXE going through a continuous series of rotations. The
key behind the animation of the rotations is in the variable initialized at the start of
function main(). The variables xangle, yangle, and zangle represent the x, y, and z rota-
tion angles passed to the rotate() function in the main animation loop. If
these values
remained the same every time rotate() was called, however, there would be no ani-
mation, so the value of the variables xrot, yrot, and zrot are added to the x, y, and z
angles, respectively, on each pass through the loop. Thus, the rotation angles change
slightly on each pass through the loop, giving the impression that the cube
the value
is
rotating
of xrot,
in all three dimensions. If you recompile the program, try varying
yrot, and zrot to produce new patterns of rotation. All of this takes place within a
While loop that continues until a key is pressed. The actual drawing of the cube, you'll
note, takes place in an offscreen buffer and is only moved to the video display, by the
putwindow() function, when the drawing is complete. (As written, the putwindow()
function moves the entire buffer into video RAM. If you recompile this program, how-
ever, try adjusting the parameters passed to this function in order to move only the
rectangular window surrounding the rotating image of the cube. As you shrink the
image, you should notice a slight increase in the animation speed, since the putwindow()
function needs to move less data on each iteration of the loop.) It’s not necessary to erase
the previous data from the screen, since the putwindow() function completely draws
over it eachtime.
Drawing a Pyramid
We don’t have to restrict ourselves to drawing cubes. We can exchange the cube data
with data for other shapes, such as a pyramid:
0,0;0,1,
0,0,0,1),
{-10,10,10,1, // Vertex 2
0,0,0;1,
244 ©
From Two Dimensions to Three
mm
.
0,0,0,1%,
{-10,10,-10,1, // Vertex 3
0,0,0,1,
0,0,0,13,
{10,10,-10,1, // Vertex 4
0,0,0,1,
0,0,0,13
};
{0,3}, // Line 2
{0,4}, // Line 3
{1,2}, // Line 4
{2,3}, // Line 5
3,4), // Line 6
};
{4,1} // Line 7
Drawing a Letter W
Here’ the data for a three-dimensional wireframe version of the letter W (which
stands, of course, for Waite Group Press):
// Vertex data for W:
{-25,-15,10,1, // Vertex 0
0,0,0,1,
0,0,0,1%,
-{15,-15,10,1, // Vertex 1
0,0,0,1,
0,0,0,1%,
{-10,0,10,1, // Vertex 2
0,0,0,1,
0,0,0,13},
{-5,-15,10,1, // Vertex 3
0,0,0,1,
0,0,0,1},
{5,-15,10,1, // Vertex 4
0,0,0,1,
0,0,0,1%),
continued on next page
245
EEE
uid
~~ mm
Your Own Flight Sim in C++
{10,0,10,1, // Vertex 5
0,0,0,1,
0,0,0,13,
£15,-15,10,1, // Vertex 6
0,0,0.1,
0,0,0;1),
€25,-15,10,1, // Vertex 7
0,0,0,1,
0,0,0,13,
{20,15,10;1, // Vertex 8
0,0,0,1,
0,0,0,1%,
{7,15,;10,1, // Vertex 9
0,0,0,1,
0,0,0,12,
{0,0,10,1, // Vertex 10
0,0,0,1,
0,0,0,1),
{=7,15,10,1, // Vertex 11
0,0,0,1,
0,001), // Vertex 12
{-20,15,10,1,
0,0,0,1,
0,0.0,11},
{-25,-15,-10,1, // Vertex 13
0,0,0;1;
0,0,0,13,
{-15,-15,-10,1, // Vertex 14
0,0,0,1,
0,0,0,13,
{-10,0,-10,1, // Vertex 15
0,0,0,1,
0,0,0,13,
{-5,-15,-10,1, // Vertex 16
0,0,0,1,
0,0,0,1%},
<,-15,-10,1, // Vertex 17
0,0,0.1,
0,0,0.12,
{10,0,~-10,1, // Vertex 18
0,0,0;1,
0,0,0,1),
{15,-15,-10,1, // Vertex 19
0,0,0,1,
0,0,0,1),
{ 25,-15,-10,1, // Vertex 20
0,0,0,1,
0,0,0,1),
{ 20,15,-10,1, // Vertex 21
0,0,0;:1,
0,0,0,1,1
{7,15,-10,1, // Vertex 22
0,0,0,1,
246 ®
0,0,0,13,
{0,0,-10,1,
0,0,0,1,
//
From Two Dimensions to Three
Vertex 23
m
eewm
~~
0,0,0,13,
{-7,15,-10,1, // Vertex 24
0,0,0,1,
0,0,0,13,
{-20,15,-10,1, // Vertex 25
0,0,0,1,
0,0,0,1)
};
Line_type W_Llines[C1={
{0,1}, Line
{1,2}, Line
{2,3}, Line
WN=0O
3,41), Line
{4,5}, Line
{5,6}, Line
{6,7}, Line VNU
{7,8}, Line
{8,91, Line
{9,101}, Line
{10,11}, Line
11,12}, Line
{12,0}, Line
{13,14}, Line
{14,15}, Line
{15,16}, Line
16,17}, Line
{17,18}, Line
{18,19}, Line
{19,20}, Line
{20,21}, Line
{21,22}, Line
{22,23}, Line
{23,24}, Line
{24,25%, Line
{25,13}, Line
{0,13%, Line
{1,14}, Line
{2,15}, Line
{3,16}, Line
4,171, Line
{5,181}, Line
{6,19}, Line
{7,20}, Line
{8,211}, Line
{9,22}, Line
{10,23}, Line
{11,24}, Line
continued on next page
247
po
~~ Build Your Own Flight Sim in C++
This is the longest shape descriptor we've seen yet. The reason, obviously, is that there
are a lot of lines and vertices in a three-dimensional W. The executable code is on the
disk as WAITE.EXE. See Figure 7-15 for a glimpse of what it produces.
We'll be seeing these shapes again in later chapters. Next time, however, they'll have
acquired a new solidity. We're going to make the transition from wireframe graphics to
a much more realistic method of
three-dimensional rendering—polygon-fill graphics.
248 ®
Polygon-Fill
Graphics
HE BE BE BE
= 251
~~ Build Your Own Flight Sim in C++ — "- HB
EER
RE LR ET
Figure 8-1 Polygons with three, four, five, and eight edges
(a) Athree-sided polygon (triangle) (b) A four-sided polygon (rectangle)
252 Wm
Polygon-Fill Graphics m
"=
appearance constructed
The class that will be used in this book for describing polygons and objects made up
of polygons owes a great deal to the class that was used in the last two chapters to
describe wireframe objects. It departs from that class in some important ways, however,
so be prepared for some surprises. The complete class definition is on the disk in the
file POLY.H. In
the paragraphs that follow, we'll look at it
piece by piece.
HE EE BH
EB 253
in
a ~~" “Build
Build Your
Your Own
struct vertex_type {
// Structure for individual vertices
long lx,ly,lz,lt; // Local coordinates of vertex
long wx,wy,wz,wt; // World coordinates of vertex
long sx,sy,st; // Screen coordinates of vertex
};
For now, don't worry about the xmax and xmin fields and their equivalents for y and z.
We'll see what those are for in Chapter 10. You may also notice that the vertex
array is
defined as an array of pointers. Usually this means that we're dealing with a two-
dimensional array—an array of arrays. That's not the case here, however. We'll see in a
moment why the vertex array is
defined as an array of pointers.
254 ®
Polygon-Fill Graphics
"Ean.
The FRIEND keyword is used to instruct the compiler that another class will have
full access to the polygon_type class members, private and protected as well as public.
This is often used when two classes are closely related, such as our polygon class and
a class which is made up of polygons described next.
The polygon_type class has a constructor with no arguments which is defined as
curly brackets with a semicolon in between. This isthe default constructor—one with
no arguments, and we normally would not have to define it since C++ does this auto-
matically when no other constructor is
present in the class. However, this constructor
has a member initializer: vertex(0), which guarantees that the vertex_type pointer is
set
to 0 when a polygon_type object is made.
The three other public functions, the destructor, backface and DrawPoly, will be cre-
class object_type {
int number_of_vertices; // Number of vertices in object
int number_of_polygons; // Number of polygons ofin object
int X,¥Y,2; // World coordinates
// object's Local origin
polygon_type *polygon; // List of polygons in object
vertex_type *vertex; // Array of vertices in object
int convex; // Is it a convex polyhedron?
public:
object_type():polygon(0),vertex(0){;}
“object_type();
int Load();
void Transform();
void Project(int distance);
void DrawCunsigned char far *screen);
};
You'll notice that some this
of class is redundant. We've seen a few of these same
fields in the polygon_type class. Why do we need a list of vertices in both the polygon
class and the object class? The reason is that sometimes we'll want to treat a polygon as
a list of vertices; at other times we’ll as
want to treat an object a list of vertices. It would
be wasteful to store two separate lists of vertices, so we've defined one of these lists (the
one in the polygon class) as a list of pointers that point at the vertex descriptors in the
list maintained by the object class. This concept is illustrated in Figure 8-4.
The convex field might look mysterious to you. We'll discuss this field momentarily.
Again, a constructor is
defined inline which initializes the pointers to 0. And there are
HE BE BE BE BN
255
Bid Your Own Flight Sim in C++
"EE
EEE
fiveother public functions, three of which were seen in the Shape class: Transform,
Project, and Draw. The destructor and load function complete the class.
New Classes
Both the object_type class and the Shape class, introduced in
the last chapter, maintain
a list of vertices. The line_type structure, though, pointed to the vertices by indexing
into the array of vertices maintained by the Shape class. The polygon_type class, on the
other hand, maintains an array of pointers that points at the location of the vertex
descriptors in memory. Line_type held offsets into the array, polygon_type holds actual
addresses. Keep this difference in mind; it
could become confusing,
You might think that these are the only classes that we need for maintaining descrip-
tions of polygon-fill objects in the computers memory. However, we need to look
ahead to programs that we will be writing later. These programs will maintain multiple
objects in memory, in such a way that those objects constitute a world of sorts inside
the computer's memory. So we need a larger class in order to maintain lists of objects.
Let’ call that class world_type. The definition is simple:
256 ®
Polygon-Fill Graphics m mm
class world_type {
private:
int number_of_objects;
object_type *obj;
public:
world_type():obj(0){;}
“world_type();
int loadpoly(char *filename);
void Draw(unsigned char far *screen);
Y;
This class contains a list of objects and the number of objects in that list, and that’s all
we need. In this chapter we'll include only one object in the world_type class, but in
later chapters we'll include more objects. Once again, there is a constructor which ini-
tializes the pointer to 0 and a destructor. Two functions called loadpoly and Draw pro-
vide methodology for information to be read into the world_type and for that
information to be put to the screen.
The destructors for our three new classes will all look similar. Because we have cho-
sen to initialize the pointer fields with 0, the destructor will check if there has been
the array if so.
memory allocated (the pointer will be anything other than 0) and delete
In this way, each class is responsible for cleaning up after itself.
world_type:: “world_type()
{
if(obj)
delete [1 obj;
}
object_type::"object_type()
{
if(polygon)
delete [J] polygon;
if(vertex)
delete [J vertex;
}
polygon_type::“polygon_type()
{
if(vertex)
delete [J] vertex;
}
Polygon Drawing
Before we can draw filled polygons, however, it is necessary to
distinguish between two
different kinds of polygons: convex and concave. One way to distinguish between
HE BE BE BE ® 257
~~ Build Your Own Flight Sim in C++ mB EEE
these two types of polygons is to study the angles at which lines come together at each
of the polygon vertices. In a concave polygon, the internal angle—the angle measured
from the inside of the polygon—is greater than 180 degrees at one or more of the ver-
tices. (An angle of 180 degrees would appear
as a straight line.) In Figure 8-5, for
instance, the angle formed by the two edges that come together at vertex 3 form an
angle greater than 180 degrees, making this polygon concave. If it helps, you can
think of a concave polygon as having caved in at the corresponding vertex; the concave
angle is a kind of intrusion into the interior of the polygon.
There’ another way of defining a concave polygon. If it is possible to draw a straight
line from one edge of a polygon to another edge and have that line
pass outside the
polygon on the way, then the polygon is concave. Figure 8-6 illustrates this concept.
Lines 1 and 3 don't go outside the polygon, but line 2 does. Thus, this polygon is con-
cave. The polygon in Figure 8-7, on the other hand, is convex, since there's no way to
draw a line between two edges and have that line pass outside the polygon.
258 =
Polygon-Fill Graphics m "om
Figure 8-8 (a) A convex polygon (b) The convex polygon from Figure 8-8(a)
drawn as a series of horizontal lines
I'm dwelling on the differences between concave and convex polygons because it’s a
lot harder to draw a concave polygon than a convex polygon. Bear in mind that the PC
graphic display is organized as a series of horizontal and vertical lines of pixels. A
a
convex polygon can be drawn as series of horizontal lines, stacked one atop the other,
as in Figure 8-8. (It can also be drawn as a series of vertical lines, but horizontal lines
mE
EE EH 259
eB BE
a
>,
~~ Build Your Own Flight Sim in C++
I I
Figure 8-9 (a) A concave polygon (b) The concave polygon from Figure 8-9 (a)
drawn as a series of horizontal lines. Note that some of the lines are broken in two
of the lines
a
we cannot draw a concave polygon as series of stacked horizontal lines, because some
may be broken, as in Figure 8-9.
For this reason, we'll use only convex polygons to create
images in this and later
chapters. No concave polygons will be allowed. So how, you might ask, will we con-
struct objects that require concave polygons? We'll build the concave polygon out of
260 =
Polygon-Fill Graphics m mm
two or more convex polygons. Figure 8-10 shows how the concave polygon from Fig-
ure 8-9 could be constructed out of two convex polygons. This will increase the num-
ber of vertices that need to be processed, but it will greatly simplify the drawing of the
polygons, which (as you will see in just a moment) is complex enough already. If you
feel ambitious, don't hesitate to rewrite the polygon-drawing routine in this chapter to
draw concave polygons.
1. Determine which vertex is at the top of the polygon (i.e., has the small-
est y coordinate).
. Determine the next vertex clockwise from the top vertex.
WN
262 ®
Polygon-Fill Graphics m
LL
void polygon_type :: DrawPoly(unsigned char far *screen_buffer)
We'll need quite a few variables in this function. Many of them are the same variables
used in the Bresenham function back in Chapter 6, but now these variables come in
pairs, since we'll be doing two simultaneous Bresenham routines. Here are some of the
uninitialized variables declared at the beginning of the function:
int ydiff1,ydiff2, // Difference between starting
// x and ending x
xdiff1,xdiff2, // Difference between starting y
// and ending y
start, // Starting offset of line
// between edges
Length, // Distance from edge to 1
// edge 2
errorterml,errorterm2, // Error terms for edges 2 1 &
Many other variables will be declared in the course of the function, when they're ini-
tialized. For instance, heres the variable that will count down the number of edges in
the polygon:
int edgecount= number_of_vertices-1;
The value of this variable will be decremented each time a complete edge is drawn.
When the value reaches zero, the polygon will be drawn and the function will
terminate.
HE BE BE BE B®
263
Bul Your Own Flight Sim in C++
When this loops ends, the integer variable firstvert contains the number of
the topmost
vertex. The variable is called firstvert because the first
it
is
vertex that will be drawn in
the polygon.
The next job is to create a series of variables that point at the starting and ending
vertices of the first two edges of the polygon, which we'll call edge 1 and edge 2. For
the moment, it’s irrelevant which of these edges proceeds clockwise from firstvert and
which proceeds counterclockwise. We'll determine that when we actually draw the line
between them.
First, we need a pair of integer variables called startvert] and startvert2, which will
represent the positions, in the vertex list, of the starting vertices of edges 1 and 2,
respectively. Because the two edges initially start at the same place (firstvert), we'll
simply set both of these variables equal to firstvert:
int startverti=firstvert; // Edge 1
start
int startvert2=firstvert; // Edge 2 start
We'll need a separate set of variables to hold the x and y coordinates of startvertl and
startvert2. The x and y coordinates of startvertl will go into the integer variables xstart1
and ystartl, respectively. The x and y coordinates of startvert2 will go into the integer
variables xstart2 and ystart2, respectively:
int xstartl= vertex[startvert1l->sx;
int ystartl= vertex[startvert1l->sy;
int xstart2= vertex[startvert2l->sx;
int ystart2= vertex[startvert2l->sy;
264 ®
Polygon-Fill Graphics m
mm
must make sure that the result is not greater than the last vertex in the polygon’ vertex
around to 0. Finally, the x,y coordinates of endvert] will go
list. If it is, we must wrap it
in the integer variables xendland yendl. The x,y coordinates of endvert2 will go in the
integer variables xend2 and yend2:
// Get end of edge 1
and check for wrap at last vertex:
int endvertil=startvertl-1;
if (endvert1<0) endvertl1= number_of_vertices-1;
int xend1= vertex[Lendvert1l->sx;
int yend1= vertex[endvertl1l->sy;
// Get end of edge 2 and check for wrap at last vertex:
int endvert2=startvert2+1;
if (endvert2==( number_of_vertices)) endvert2=0;
int xend2= vertex[endvert2l->sx;
int yend2= vertex[endvert2l->sy;
offset1=320*ystartl+xstart1+FP_OFF(screen_buffer);
offset2=320*ystart2+xstart2+FP_OFF(screen_buffer);
Then we must initialize the error terms for edges 1 and 2 to 0:
errorterm1=0;
errorterm2=0;
We won't be using these error terms until somewhat later in the function, but it doesn’t
hurt to initialize them
now.
it
stored in ydiffl and ydiff2. If the value is negative, is negated to make
it positive. To
allow this operation to be performed in a single line of code for each edge, we use the
C++ convention of assigning a value to a variable and simultaneously testing that
value:
266 Wm
Polygon-Fill Graphics m
"om
in which edge 1 has a slope less than 1 and edge 2 has a slope equal to or greater than
1, and one for sections of polygons in which edge 1 has a slope greater than or equal to
1 and edge 2 has a slope less than 1. In a single polygon, it’s theoretically possible for
all four of these subsections to be executed, as the relationship between the edges of
the polygon changes. However, only one is executed at a time.
The function chooses between these subsections with a series of nested if state-
ments. The overall structure of these if statements looks like this:
// Choose which of four routines to use:
if (xdiff1>ydiff1) { // 1f edge 1
slope < 1
if (xdiff2>ydiff2) {
// If edge 2 slope <1
// Increment edge 1 on X and edge 2 on X
}
else { // If edge 1
slope >= 1
if (xdiff2>ydiff2) {
// 1f edge 2 slope <1
// Increment edge 1
on Y and edge 2 on X:
}
else { // 1f edge 2 slope >= 1
// Increment edge 1
on Y and edge 2 on Y:
We're only going to explore one of these subsections here, since it would take too
much time and space to look at all four (though I'll show you the complete function in
a moment so you can explore all
four). Fortunately, all of the principles involved in all
four subsections are visible in one. So lets look at the subsection for sections of poly-
gons in which edge 1 has a slope less than 1 and edge 2 has a slope greater than or
equal to 1.
"EE EEE
incrementing on the y coordinate, it
will be the y difference. In the example we've cho-
sen, it
will be both, the x difference for edge 1 and the y difference for edge 2:
This segment of code will continue executing until one or both of these
ished, so the actual drawing will be performed in a While loop:
edges is fin-
// Continue drawing until one edge is done:
while (count1 && count?) {
Now let’ find the next point on edge 1, so that we can draw a horizontal line from that
point to the next point on edge 2. This is a bit tricky, since edge 1 is being incremented
on the x coordinate. This means that the x coordinate can be incremented several
times before the y coordinate is incremented, which makes it difficult to determine
when we should draw the horizontal line. If we draw it every time the x coordinate is
just
incremented, we may end up drawing several horizontal lines over the top of one
another, which will waste a lot of time. Instead, we'll continue incrementing the x coor-
dinate until it is time to increment the y coordinate, and then we’ll draw the horizon-
tal line from edge 1 to edge 2.
But what if edge 1 is the left edge and x is being incremented in the positive (right)
direction? Or if edge 1 is the right edge and x is being incremented in the negative (left)
direction? In both of these situations, if we wait until the y coordinate is about to be
incremented to draw the line between the edges, theres a good chance that we will
miss one or more pixels along the edge of the polygon. To guard against this possibil-
ity, we'll set a pixel every time the x coordinate is incremented. When we draw the hor-
izontal line, we may well draw over the top of these pixels, but the redundancy will be
minor.
Then we will subtract from the variable count] (which is counting down the number
1
268 =
Polygon-Fill Graphics m
mm
if (countl--) { // Count off this pixel
offset1+=xunitl; // Point to next pixel
xstartl+=xunit1;
}
When the innermost While loop finishes executing, it’s time to increment the y coor-
Since edge 2 is incremented on the y coordinate, its actually a bit easier to handle, since
horizontal line.
we don't have to worry about missing any pixels when we draw the
if it’s time
Incrementing edge 2 is simply a matter of adding xdiff2 to errorterm2 to see
to increment the x coordinate yet. If not, we decrement count2 to indicate that we've
moved one more pixel in the y dimension and continue to the routine that draws the
horizontal line:
line length by subtracting one offset from the other, like this:
Cleaning Up
And thats it. Aside from terminating the loops, that’s all that needs to be done to draw
a segment of the polygon. The other sections of code operate similarly; we'll look
complete code in a second. When an edge terminates at a vertex, one or both of the
at the
count variables will go to 0 and the loop will terminate. Now we have to reset the
appropriate variables so that the four-edge filling routines can start with the next edge,
if any. First we check to see if edge 1 has terminated:
if ('count1) // If edge 1
is complete...
If so, we count off one edge:
270 =
NS
*
Polygon-Fill Graphics
"Ew
-edgecount; // Decrement the edge count
of the new
Then we make the ending vertex of the last edge into the starting vertex
edge:
startverti=endvert1; // Make ending vertex into
// start vertex
did
And calculate the number and x,y coordinates of the new ending vertex just as we
at the beginning of the program:
"E BE BE
BB 27
. xdiffl,xdift2,
start,
—
int ydiff1,ydiff2,
—
y
Ew
|
// between edges
Length, // Distance from edge to 1
// edge 2
errorterml,errorterm2, // Error terms for edges 2 1 &
offset1,offset2, // Offset of current pixel in
// edges 2 1 &
|
// Initialize count of number of edges drawn:
int edgecount=number_of_vertices-1;
// Determine which vertex is at top of polygon:
// Start by assuming vertex 0 is at top:
int firstvert=0;
int min_amt=vertex[0l->sy;
// Search through all vertices:
for (int i=1; i<number_of_vertices; i++) {
int endverti=startvertl-1;
if (endvert1<0) endvertil= number_of_vertices-1;
int xend1= vertex[endvert1l->sx;
int yend1= vertex[endvertl1l->sy;
// Get end of edge 2 and check for wrap at last vertex:
int endvert2=startvert2+1;
if (endvert2==( number_of_vertices)) endvert2=0;
int xend2= vertex[endvert2l->sx;
int yend2= vertex[endvert2l->sy;
// Draw the polygon:
while (edgecount>0) { // Draw all edges
offset1=320*ystartl+xstart1+FP_OFF(screen_buffer);
offset2=320*ystart2+xstart2+FP_OFF(screen_buffer);
// Initialize error terms for edges 1 & 2:
errorterm1=0;
errorterm2=0;
// Get absolute values of y lengths of edges:
if ((ydiffl=yend1-ystart1)<0) ydiffl=—ydiff1;
if ((ydiff2=yend2-ystart2)<0) ydiff2=-ydiff2;
// Get absolute values of x lengths of edges
// plus unit to advance in x dimension
if ((xdiffl=xend1-xstart1)<0) {
xunit1=-1;
xdiff1==-xdiff1;
}
else {
xunit1=1;
}
if ((xdiff2=xend2-xstart2)<0) {
xunit2=-1;
xdiff2==-xdiff2;
}
else (
xunit2=1;
}
if (xdiff2>ydiff2) {
// If edge 2 slope < 1
// Increment edge 1
on X and edge 2 on X:
// Find edge 1
coordinates:
// Don't draw polygon fill Line until ready to
// move in y dimension:
while ((errorterm1<xdiff1)&&(count1>0)) {
if (count1--) ( // Count off this pixel
offset1+=xunit1; // Point to next pixel
}
xstart1+=xunit1;
errorterm1+=ydiff1; // Ready to move in y?
if (errorterm1<xdiff1) {
// No?
while ((errorterm2<xdiff2)&&(count2>0)) {
if (count2--) {
// Count off this pixel
offset2+=xunit2; // Point to next pixel
Xxstart2+=xunit2;
}
274 ®
Polygon-Fill Graphics
EE
// Find length and direction of line:
length=offset2-offset1; // What's the Length?
if (length<0) { // If negative...
Make it positive
Llength=-length; // START
start=offset2; // = edge 2
}
else start=offset1; // Else START = edge 1
while (Cerrorterm1<xdiff1)&&Ccount1>0)) {
HE BE BE BE HN 275%
~~
—
P san
~~" Build re
Your Own Flight Sim in C++
// direction of line:
Find length and
if (xdiff2>ydiff2) {
// If edge 2 slope < 1
// Increment edge 1
on Y
and edge 2 on X:
276 ®
Polygon-Fill Graphics m
mm
// Find edge 1
coordinates:
errorterml+=xdiff1; // Increment error term
// 1f time to move in y dimension, restore error
// term and advance offset to next pixel:
if Cerrorterml >= ydiff1) {
errorterml-=ydiff1;
offsetl+=xunit1;
xstart1+=xunit1;
}
"En EER
of fset1+=320;
ystart1++;
offset2+=320;
ystart2++;
}
|
|
}
else { // If edge 2 slope >= 1
// Increment edge 1
on Y and edge 2 on Y:
errorterm1-=ydiff1;
offset1+=xunit1;
xstart1+=xunit1;
278 ©
}
if (length<0)
Length=-length;
start=offset2;
{ //
//
//
If negative...
Set
Polygon-Fill Graphics
...make it positive
START = edge 2
"ss
else start=offset1; // Else START = edge 1
-edgecount; // Make
Decrement the edge count
startverti=endvert1; // ending vertex into
// And
start vertex
-endvert1; // get new ending vertex
// Check for wrap:
if (endvert1<0)
endvert1= number_of_vertices-1;
// Get x &
y of new end vertex:
xend1= vertex[endvert1l->sx;
yend1= vertex[Cendvert1l->sy;
HE BE BE BE B®
279
~~
Build Your Own Flight Sim in C++
Manipulating Polygons
Of
is
course, drawing polygons not all there is to producing three-dimensional anima-
tions. Next we need to draw entire objects constructed from polygons. Then we need |
to manipulate those objects so that we can rotate, scale, and translate them.
To perform both of these tasks, a new version of the WIRE.CPP package from
Chapter 7 is presented over the next few pages. However, to reflect its new purpose,
has been renamed POLY.CPP (and is available in a file of the same name on the disk
it
that came with this book under the directory BYOFS\POLYGON). Many
of the routines
from the last chapter have remained unchanged in this new package, but since some
have changed, we'll present them here.
Theres also an entirely new function in the package this time around: Draw().
Huh? That was the same name as the function in the Shape class and it plays essentially
the same role as the Shape::Draw() function, except instead of running through a list of
lines in a wireframe shape and drawing them, it runs through a list of polygons in a
polygon-fill object and calls the DrawPoly() function to draw them.
Backface Removal
The DrawPoly() function needs a bit of explanation, though. It is not enough simply
to call it to draw all of the polygons in an object. Why? Because in a real object, some
surfaces hide other surfaces, so that not all surfaces are visible. If we draw the poly-
gons that make up an object without regard to this problem, we'll most likely wind up
with background surfaces showing through foreground surfaces. The result will be a
mess.
This is known as the hidden-surface problem and is a major consideration in draw-
ing three-dimensional objects. It is such a major consideration that we'll spend an
entire chapter on it later in this book. For the moment, we will take a simplistic solu-
tion to the problem. This solution is known as backface removal.
~~"
Art SR En pe
Build Your Own Flight Sim in C++ Sl - n =u un u B—
Al
A backface, simply put, isany surface that isn't facing us at the moment. When we
draw a polygon-fill object such as a cube on the video display, roughly half of its sur-
faces—half of its polygons—will be facing away from us. If the object is closed—if
there are no openings into its interior—these surfaces are effectively invisible and we
don't have to draw them. In fact, we don't want to draw them, because they are hidden
RC
surfaces that would mess up the display if drawn over the top of the surfaces that are
hiding them.
It would be nice if there were a way to identify backfaces so that we could avoid
drawing them. And, by an amazing coincidence, there is. By taking the cross-product
of two adjacent edges of the polygon, we can determine which way
it is facing. If it is
facing in a positive z direction, it
is facing away from us. Ifit is facing in a negative z
direction, itis facing us.
The cross product of the edges is determined using the rules of matrix multiplica-
tion. We need to multiply three vectors together, each containing the coordinates of
one of the vertices of the two edges. Fortunately, we don’t even need to perform the full
multiplication. We just need to perform enough of
the result. This equation will do the job:
itto determine the z coordinate of
z=(x1-x0)*(y2-y0)=-(y1-y0) *(x2-x0)
where (x0,y0,z0) are the coordinates of the first vertex, (x1,yl,z1) are the coordinates
of the second vertex, and (x2,y2,z2) are the coordinates of the third vertex. With this
equation, we can build a function called backface in the polygon_type class that deter-
mines, based on three of the polygon’ vertices, whether or not it is a backface. Such a
function appears in Listing 8-4. There’ only one catch. In order to use this function
properly, all polygons must be designed in such a way that, when viewed from their
visible sides, the vertices (as listed in the descriptor file) proceed around the polygon in
a counterclockwise direction.
282 =
=(v1=->sy-v0->sy)*(v2->sx-v0->sx);
return(z>=0);
}
In Chapter 13, I'll show you an alternate method of performing backface removal that
works better in some situations than this one does, though the method documented
above is adequate for the programs developed in this and the following chapter.
polyptr=>DrawPoly(screen);
}
}
else polyptr->DrawPoly(screen);
}
}
You'll note that the purpose of the convex field in the object_type class is to determine
whether we actually do want to remove backfaces from an object. For some objects,
such as open containers and single polygons, backface removal would be counterpro-
ductive; the backsides of objects that should be visible would instead mysteriously
vanish.
~~
ame
Build Your Own Flight Sim in C++
® WH
Although fairly simple to implement, backface removal has some severe limitations as
a method of removing hidden surfaces. One is that the object must be a convex poly-
hedron. To be convex, all internal angles in the polyhedron must be less than 180
mE EN
EB
degrees. Otherwise, there may be background polygons that are not backfaces but that
are nonetheless concealed, at least partially, by foreground polygons. The second lim-
itation is that backface removal only works on a single object. If we wish to construct
a scene with more than one object, backface removal won't help prevent background
objects from showing through foreground objects. Thus, we'll need a more powerful
method of hidden surface removal. We'll defer this complex topic until Chapter 10
however.
explain the purpose of the numbers. So, anything on a line after an asterisk, up to the
carriage return that terminates the line, will be ignored, just as anything on a line after
284 ©
Polygon-Fill Graphics m
uw
the double slash (//) is ignored in C++. The format of the numbers roughly follows the
format of object_type class. For example, an ASCII descriptor for a cube appears in List-
ing 8-6. This is included in the POLYGON.IDE project.
HE BE BE BE B=
285
E=
:
"=" EE
7 :
Build Your Own Flight Sim in C++
// parsing functions:
286 =
Polygon-Fill Graphics
Ew
int getnumber();
char nextchar();
};
static PolyFile _pf; // static PolyFile available to this module only
int object_type::load()
{
number_of_vertices=_pf.getnumber();
vertex= new vertex_typelnumber_of_verticesl;
if( vertex == 0)
return(-1);
for (int vertnum=0; vertnum<number_of_vertices; vertnum++) {
vertexCLvertnuml.lx=_pf.getnumber();
vertexCvertnuml.ly=_pf.getnumber();
vertexCvertnuml.lz=_pf.getnumber();
vertexCvertnuml.lt=1;
vertexLvertnuml.wt=1;
}
number_of_polygons=_pf.getnumber();
polygon= new polygon_typelnumber_of_polygonsl;
if(polygon == 0)
return(=1);
polygon_type *polyptr = polygon;
for (int polynum=0; polynum<number_of_polygons; polynum++, polyptr++) {
polyptr->number_of_vertices=_pf.getnumber();
polyptr->vertex= new vertex_type *[number_of_vertices];
if(polyptr=>vertex == 0)
return(=1);
for (int vertnum=0; vertnum< polyptr->number_of_vertices;
vertnum++) {
polyptr—>vertexCvertnuml= &vertex[_pf.getnumber()1;
}
polyptr=>color=_pf.getnumber();
}
convex=_pf.getnumber();
return(0);
}
if( objCobjnuml.load() )
return =1 );
continued on next page
HE BE BE BE B=
287
~~~ Build Your Own Flight Sim in C++
return(0);
¥
J] ded ded dedeok ded ok ok ok ok ok
kk dk kk de ok ok ok ok ok oe
grip = open(filename,0_RDONLY|O_TEXT);
return (grip == =-1);
}
int PolyFile::getnumber()
{
char ch;
int sign=1;
int num=0;
if ((ch=nextchar())=='-') {
sign=-1;
ch=nextchar();
}
while (isdigit(ch)) {
num=num*10+ch-'0"';
ch=nextchar();
}
return(num*sign);
char PolyFile::nextchar()
{
char ch;
while(!eof(grip))
<
do {
read(grip,&ch,1);
}while(isspace(ch));
if (ch=='%')
{
do {
read(grip, &ch,1);
YwhileCch !'='\n");
}
else return(ch);
}
return(0);
}
The function loadpoly() is called to read the file, and Draw() is called to draw the
entire world. The rest of the program is identical to the wireframe program in the last
chapter, except that it uses the argv variable to get the name of the file containing the
run with the CUBE. TXT command line argument. You can
Polygon-Fill Graphics
object descriptor from the command line. The current POLYDEM1 IDE project
change
Options - Environment Menu item followed by clicking on the word Debugger on
is
set to
this under the
the
left side of the dialog box. Type in the name of your object descriptor file in the Run
argument combo box edit window.
~
"EE
int oldmode=*(int
- 5
"EE
*)MK_FP(0x40,0x49); // Save previous
// video mode
setgmode (0x13);
while (!'kbhit()) (
// Set mode 13h
// Loop until key is
// pressed
cls(screen_buffer); // Clear screen
// buffer
inittrans(); // Initialize
// transformations
scale(1,1,1); // Create scaling
// matrix
rotate(xangle,yangle,zangle); // Create rotation
// matrices
xangle+=xrot; // Increment rotation
// angles
yangle+=yrot;
zangle+=zrot;
translate(0,0,600); // Create translation
// matrix
world.Draw(screen_buffer);
putwindow(0,0,320,200,screen_buffer); // Move buffer to
}
// video RAM
Not all computer programs need to be fast. In many cases, they only need
of
to be fast enough. Word processors and terminal packages, for instance, spend most
their time waiting for the next character to be received from the keyboard or the
modem, and it doesn’t matter how quickly they process that character as long as
they're finished by the time another character arrives.
Not so with games, 3D games in particular. The execution speed of three-dimen-
sional animation code is crucial and the programmer must always be looking for ways
to save one processor cycle here and another processor cycle there. The speed at which
your code executes will determine how many frames per second the animation can
he
achieve and the amount of detail your potential flight simulator pilot can see when
of the The faster the code, the higher you can
or she looks out the window airplane.
simula-
push the frame rate and the more detail you can display. Because today’s flight
tor buffs demand both a high frame
a
rate and great deal ofvisual detail, its crucial that
the speed of the code be optimized.
For clarity, I've stressed readable code so far as much as fast code, but in this chap-
of optimization: look-up tables,
ter, we'll spend most of our time discussing three types
fixed-point arithmetic, and unrolling loops.
In addition, we'll consider the possibility of writing code that only runs on 386, 486
and better processors. We'll examine the role of math coprocessors in flight simulation.
mE
EE EH BN
293
~~ Build Your Own Flight Sim in C++
And we'll figure out ways to arrange our screen display to create a faster frame rate
without making the CPU do any additional work.
First, though, we'll discuss the most important topic of all: how to decide which
parts of a program need to be optimized. We'll take a look at Turbo Profiler, a much
underutilized utility that comes with Borland C++ 4.5. Turbo Profiler can help
you
determine which parts of your program can benefit from optimal coding and which
parts are already working as fast as is necessary. Note that Borland did not include the
Turbo Profiler in version 4.0, and that the profiler which is
displayed in the Borland
Group Window is a Profiler for Windows programs. To use the DOS Turbo Profiler you
must exit Windows after compiling your program.
Nested Loops
Suppose you write a program that contains a series of nested For() loops like the
following;
for (int i=0; i<100; i++) {
// Several lines of code (A)
for (int j=0; j<100; j++) {
// Several lines of code (B)
for (int k=0; k<100; k++) {
}
// Several lines of code (C)
>
294 m
Faster and Faster m mm
The answer, of course, is section C. To see why, let’s trace the way in which this frag-
mentof code would execute. The outermost For loop (A), the one for which the vari-
able i is the index, is set to execute 100 times, stepping the value of the index from 0 to
99 before it stops executing. The body of this loop consists of two more nested For
is also set to exe-
loops. The next inner loop (B), for which the variable j is the index,
cute 100 times. However, because this loop is nested inside the first loop, it will not
simply execute 100 times and stop. It will execute 100 times for each time the outer loop
executes. Thus, if the outermost loop executes 100 times, this second loop
will execute
10,000 times. Those executions eat up a lot of CPU time.
A
determine if any user input has occurred since the last execution of the
EEE
loop, and then
updates the on-screen animation. This outer loop will contain any number of inner
loops, one after another rather than one inside another, and these in turn
may contain
you turn your attention first?
That's what Turbo Profiler is designed to tell you. TProf, it’s affectionately known
as
to Borland C++ programmers, will run your program, counting the number of times
various subroutines and lines of code are executed and noting how much time the
gram spends in
each. It will then display this information for
pro-
you in a number of ways,
so that you can determine which parts of your program are most urgently in need of
fine-tuning,
As mentioned previously, there are two version of profilers with Borland C++
4.5.
One is the Windows version, for profiling Windows
DOS version for DOS programs. Since the DOS profiler can only be from
is
programs, while the other
the
the
run DOS
command line (and won't run under Window’ DOS shell), we will have to exit Win-
dows
to it.
use
to
Since some prefer not live the nursery rhyme (“go in and out the win-
dow”), an alternative form of compilation can be used: the command line compiler. To
use this form, load the relevant project, then select Generate Makefile under the Project
menu in the IDE. This will create a make file of the same name as the project but with
the file extension MAK. Save this editor window from the IDE for later
use from the
command line.
Let's give TProf a quick test run. The C++
program in Listing 9-1, which is on the
enclosed disk under the name TPDEMO.CPP in the BYOFS\MISC directory, is work-
a
ing version of the three nested loops in the earlier example. Fach loop contains
gle integer assignment statement, just to
a sin-
give the CPU something to do. Let’s see what
TProf has to say about the execution profile of this
program.
First, we'll need to compile it. Enter the Borland C++ IDE, load TPDEMO.IDE, corn-
pile and run it. (Alternately, choose Run from the Run menu once the
project is
296 ®
Faster and Faster m mm
loaded.) Since the program has no output, the IDE will simply display the user screen
briefly, then return to the editor. On my system this process takes about three seconds,
so that’s the baseline for the unoptimized program.
(All references to program execution speeds in this chapter and elsewhere are based
on my system and will almost certainly be different on your system. For the record, this
book originally was written using a 16-MHz, 386SX-based machine, which is fairly
slow by current standards. The second edition update to C++ occurred on a 66-MHz,
486DX computer, which still slow when compared to the top of the heap. If you are
is
Executing TProf
Now let's see what Turbo Profiler tells us about the program. Exit Windows and return
to DOS. Change to the drive and directory where the TPDEMO.EXE program was
copied, BYOFS\MISC. Invoke Turbo Profiler with the following command:
>TPROF TPDEMO.EXE
This will boot TProf, pass the name of the TPDEMO.CPP program to it, and place you
inside TProf. You're ready to start profiling.
The Turbo Profiler display should be divided into two windows. The top window
displays the TPDEMO program. Each executable line of the program should be pre-
ceded by the symbol =>. This means that TProf will be counting the time that each of
these lines spends executing. Go to the Run menu and choose Run. TProf will display
the user screen and execute the program.
TPDEMO will take longer to execute under TProf than it did from the IDE. That’
because TProf is busily counting execution times for each line of the program, which
takes longerto perform than the individual lines take to execute. fact, it takes about
In
10 minutes for the program to execute on my system. Even on a 50-MHz 486, you
should notice a difference in execution time. You might want to go make a cup of cof-
fee or call a friend and come back when TProf is done.
Once profiling is complete, the program's execution profile will appear in the lower
of the two windows. (See Figure 9-1.) Take a moment to study this information. In the
left portion of the window, you'll see a column of line numbers. In the middle, you'll
see a column of numbers representing the number of seconds, or fractions of a second,
that each line spent executing while the program ran. And on the right, you'll see a bar
chart in which the execution time for each line is represented by a line of equal signs
(You'll notice that TProf doesn’t count the time thatit spent in the timing of these lines,
so that the total time for executing all lines comes to about the same amount of time
it
that took the BC++ IDE to execute the program—in my case, about three seconds.)
What should jump out at you is that the execution profile of the program is com-
pletely dominated by a single line—line 10, according to the column on the left,
297
~~"
— —
=[o]l=Module: RANDLINE
const COLOR=15
-
File: RANDLINE.CPP 11
et line color
mane
(15=white)
————
4 ® BH mH
EB
void main()
unless you tampered with the program back in the IDE. Which line is that? Place the
dark selection bar over that line of the profile and press (Enter). TProf will activate the
upper window, placing the cursor on the line in question. It turns out be the line
inside the innermost loop, just as we guessed. According to the profile, this line
to
accounts for more than 98 percent of the program's execution time. All the other lines
together amount to less than 2 percent of the program's execution time.
Reducing to Zero
Were we optimizing this program, it’s obvious where we'd want
to
put our optimization
resources. Sometimes, in deciding where to optimize, itsuseful to ask yourself how
much time you'd save if you could reduce the execution time of line to zero
a proces-
sor cycles. In this case, if we could reduce the execution time of every line in this pro-
gram except line 10 to zero processor cycles, we would speed up the program by less
than 1 percent, an optimization that wouldn't even be noticeable. Thus, theres no
point
in spending time optimizing any line other than line 10. (After we've optimized line 10,
it might be useful to optimize other lines or it might not be. Its difficult to make that
judgment until line 10 has been optimized and the program can be profiled again.)
Is it possible to optimize line 15 of this program? Well, since line 10 doesn't
actually
do anything meaningful, we can optimize it simply by eliminating it, thus achieving the
theoretical maximum optimization of reducing the number of
processor cycles used by
this line to zero. Select Quit from the TProf File menu, then return to Windows and
BC++ IDE. (Or
stay in DOS and load TPDEMO.CPP into a text editor if you plan on
using a command line Make file.) Place a comment symbol (//) in front of line 10 (the
298 =
Faster and Faster
"a. “
that cup of coffee. On my system, the program took roughly 10 seconds to execute this
(Y)
time. Reducing execution time from 10 minutes to 10 seconds is a substantial opti-
mization, even if we did cheat a bit.
The execution profile in the lower window looks quite different now. Line 9, the line
that establishes the innermost for loop is now taking up 74 percent of the programs time
(on my machine, least).
at
If additional optimizations were needed, you'd to
want begin
with this line. But considering how substantially the execution time of the program has
been reduced already, further optimizations probably aren't needed. (Its impossible to
answer this question, since the program doesn't actually do anything useful.)
When you find yourself in Turbo Profiler, POLYDEM1 will have been loaded. The first
line of every function will have the => symbol on it this time, rather than every line in
the program. Thats because the program is too big for TProf to profile every line.
That’ okay; we don’t need that much information. We just want to
know how the indi-
vidual functions perform, so lets run the program and find out.
Before we can run POLYDEM1, however, TProf needs an argument for the pro-
gram—we need to tell POLYDEM1 what three-dimensional object we wish to rotate.
Pull down the Run menu, choose the Argument option, and type in CUBE. TXT. This is
equivalent to typing an argument after the program name on the command line. TProf
will ask you if you wish to load the program again so that the argument will take effect.
Type for “yes.” Then choose Run from the Run menu.
Now the program will run. The cube will begin revolving slowly on the screen. In
fact, it will revolve a bit more slowly than it did back in the IDE, for the same reason
that TPDEMO ran slowly in TProf—the compiling of statistics by TProf takes time. It’s
best to let the program run for several minutes under TProf, preferably 5 or 10 min-
utes, in order to get an accurate profile. This will prevent the time the program spends
in its initialization procedures from overwhelming the amount of time spent thein
main portion of the program. (Alternatively, you could remove the => symbols from all
if
of the initialization procedures by clicking on them, but that’s not necessary you have
a few minutes to kill.)
HE BE BE EE B®
299
a "EEE EE
es
~~ Build Your Own Flight Sim in C++
When the time is up, press any key and the program will terminate. The program
profile will appear in the bottom window. You should see at a glance that four functions |
That gives us 16 binary digits to the left of the decimal point and 16 digits to
the right.
(This is equivalent to approximately 5 decimal digits on each side the point, which
of
is adequate for most of the code in this book.) Unfortunately, the problem isn't quite
that simple. To see why, let’s look at how arithmetic is done in fixed-point arithmetic.
long fnum1,fnum2;
Now we have two 32-bit variables ready to store values. Assigning those values to the
variables is a bit tricky. Lets assume that we've decided to place the decimal point after
bit 9, so that there are 9 binary digits to the right of the decimal point and 24 to the
left, like this:
10001110111101100011101.101101011
To assign an ordinary integer value to one of these fixed-point variables, we first must
shift the digits of that value nine digit positions to
the left, effectively moving its
deci-
mal point to the ninth position. To shift digits, the bitshift operators (<< and >>) are
used. These are identical to the stream output and insert operators of C++ but the com-
piler knows the difference according to the context and use of the operators (whether
a stream is the left hand operand). For instance, we wantif to
assign a value of 11.0 to
the fixed-point variable fauml, we would do like so
this:
fnum1l = 11 << 9;
Similarly, if int] is an ordinary integer variable containing a non-fixed-point value, we
would assign its value to the fixed-point variable fnum1 like this:
fnum1 = intl << 9;
The sum of two fixed-point numbers is
a third fixed-point number. When we add two
fixed-point variables, we do so just as though we were adding a pair of ordinary inte-
ger variables:
long fnum3 = fnuml + fnum2;
Similarly, we subtract two fixed-point numbers in the same way:
301
oe Build Your Own Flight Sim in C++
should that be binary point?) moves left to a digit position representing the combined
digit positions of both of the numbers being multiplied. When we multiply two of our
fixed-point numbers with decimal points in position 9, the result will be a number
with the decimal point in position 18.
1111.11
x «22
222222
222222
244 4442
sides of the decimal point, all information to the left of the decimal
point would be lost
joz m
Faster and Faster m mm
when we perform multiplication. By placing the decimal point in the ninth position,
after multiplication.
we have 14 bits of precision to the left of the decimal point, even
How can we incorporate fixed-point math into our code? The matrix routines are
the best target, since they are the only ones that make extensive use of time-consuming
be of type float, we can
floating-point operations. Instead of declaring the matrices to
for
declare them to be of type long—and treat them as fixed-point values. Here,
instance, isthe matmult() function revised to use fixed-point math:
void matmult(long resultC41C4],long mati [41041],
long mat2C41C41)
{
The parameters passed to the function are now of type long and the multiplication per-
formed between the two matrices is a fixed-point multiplication, with the
result
of the product back to the left. You'll note that a constant
adjusted shifting the digits
by
called SHIFT defines the number of points that the product is shifted. This allows us to
This constant will be
reconsider, if
necessary, where the decimal
we'll
point is
look
to be placed.
later in this chapter. If you'll
defined in a file called FIX.H, which get a at
look in the file OPTPOLY.CPP on the disk under BYOFS\OPTIMIZE, you'll see that the
other transformation functions and module global variables have been similarly
of float.
adjusted and now use long variable types instead type
#include "fix.h"
long matrix[41C4]; // Master transformation matrix
long smatC&41C41; // Scaling matrix
long zmatC41C41; // 1 rotation matrix
Is It Worth It?
worth the time that will be saved relative to floating-point
Is all this fixed-point effort
math? That depends to some degree on whether or not the user has a floating-point
math coprocessor installed. These coprocessors, which have become increasingly
HE HE EH BE BH
303
— Build Your Own Flight Sim in C++
—
4 mu EEE
popular in recent years, greatly decrease the time required for floating-point calcula-
tions by the lengthy subroutines used for this
purpose by machines without coproces-
sors. The Pentium microprocessor, used on most of the fastest IBM-compatible
computers currently available, has a math coprocessor built in.
Roughly speaking, the fixed-point routines we've discussed will
substantially
improve the execution time of three-dimensional code on machines lacking math
coprocessors. For machines with math coprocessors, though, the improvement will be
smaller—perhaps much smaller. (We could use Turbo Profiler to see just how
great the
advantage is, but, according to Michael Abrash, in Dr. Dobb's Journal, this might
not do
us any good. According to Abrash, TProf doesn't record time
spent executing floating-
point instructions. My own experiments tend to bear out Abrash’s contention. If this is
true, the matrix multiplication routines in our program are probably even slower than
our earlier foray into Turbo Profiler would seem to indicate.)
If you have a 386 or later microprocessor, and
are willing to write code that will not
run properly on a less capable machine, we can write even more efficient fixed-point
code than we have demonstrated in this chapter. The 386
processor features a set of
32-bit registers and 32-bit instructions that allows extremely efficient
fixed-point math.
Furthermore, because the 386 allows multiplication across two 32-bit
registers, for a
full 64 bits of precision, it is possible to
perform fixed-point multiplication on the 386
without losing the high-order bits.
To demonstrate, lets take the fixed-point statement:
joa =m
Faster and Faster m mm
305
Build Your Own Flight Sim in C++
portion of your program, whether the value is calculated by a built-in library function
or by your program itself, consider calculating the value in advance and
storing it in a
look-up table. The results can be gratifying, |
256 fixed-point sine and cosine values and stores them containing
in the file FIX.CPP It also gen-
erates a header file called FIX.H containing a pair of macros for
performing the sine
and cosine look-ups (called SIN() and COS(), (note the
uppercase letters) and several
relevant constants. To use the tables and the
macros, place FIX.CPP in your project
window as part of the target's dependents and #include
FIX.H in any file that uses the
macros and constants. FIX.CPP and FIX.H are shown in
Listings 9-3 and 9-4. To build
the MAKESINE.EXE, load the OPTDEMO.IDE into the
BCa+ IDE from the Project
menu. MAKESINE is the second target of the project window. Click with
the right
mouse button and select Build Node to create the makesine
program.
float radians=0;
// Create file FIX.H for constants and macros:
fstream finout("fix.h",i0s::0ut |
ios::trunc);
finout << "\n#ifndef FIX_H\n\n#define FIX_H\n";
finout << "#define ABS(X) (X<0?-X:X)\n";
finout << "#define COS(X) cos_table[ABS(X)&2551\n";
finout << "#define SIN(X) sin_table[ABS(X)&2551\n";
finout << "\nconst int NUMBER_OF_DEGREES =" << NUMBER_OF_DEGREES <<
finout << "const int SHIFT = " << SHIFT x< ":\n": 2\n";
finout << "const long SHIFT_MULT = TL<<SHIFT;\n\n";
finout << "extern Long cos_table[NUMBER_OF_DEGREES1;\n";
finout << "extern long sin_tableCNUMBER_OF_DEGREESI;\n";
finout << "\n#endif\n";
finout.close();
// Create file FIX.CPP for sine and cosine tables:
finout.open("fix.cpp"”,ios::out |
finout << "\n//FIX.CPP\n" << "// ios::trunc);
Fixed point math tables\n\n'"<<
"#include \"fix.h\"\n";
Faster and Faster
" "ou
// Create cosine table:
"ee
finout << "long cos_table[NUMBER_OF_DEGREESI={\n
int count=0; i++) {
for (int i=0; i<NUMBER_OF_DEGREES;
(radians) *SHIFT_MULT) << ",";
finout << Long(cos
radians += 6.28/NUMBER_OF_DEGREES;
count++;
if (count>=8) {
//FIX.CPP
// Fixed point math tables
#include "fix.h"
Long cos_table[NUMBER_OF_DEGREESI={
504,
512, 511, 511, 510, 509, 508, 506, 477,
502, 499, 496, 493, 489, 486, 482,
473, 468, 462, 457, 451, 445, 439, 432,
425, 418, 411, 403, 395, 387, 379, 370,
362, 353, 343, 334, 324, 315, 305, 295,
284, 274, 263, 252, 241, 230, 219, 207,
112,
196, 184, 172, 160, 148, 136, 124,
100, 87, 75, 63, 50, 38, 25, 12,
0, -12, -24, -37, -49, -62, -74, -87,
-99, -111, -123, -136, -148, -160, -172, -183,
-195, -207, -218, -229, -240, -251, -262, -273,
-283, -294, -304, -314, -395, -333,
-324, -343, -352,
-361, -370, -378, -387, -403, -410, -418, continued on next page
~~
Te
Build Your Own Flight Sim in C++
—472,
page
-425, -432, -438, -445, -451, -457,
Long sin_table[NUMBER_OF_DEGREES]={
1,10, 23,
98; 110,
36, 48, 61, 73, 85,
122, 134, 147, 158, 170, 182,
194, 205, 217, 228, 239, 250,
282, 261, 272;
293, 303, 313, 323, 333, 342, 351,
360, 369, 378, 386, 394, 402, 410,
424, 431, 438, 444, 450, 417,
456, 462, 467,
472, 476, 481, 485, 489, 492, 496,
501, 504, 506, 507, 509, 499,
510, 511,: 511,
11, 511, 81%; 510, 509, 508, 506, 504,
502, 500, 497, 494, 490, 486,
473, 482, 478,
468, 463, 458, 452, 446, 440, 433,
426, 419, 412, 404, 397, 389, 380,
363, 354, 345, 336, 326, 372,
316, 306, 296,
286, 275, 265; 254, 243, 232, 220,
198, 186, 174, 162,150, 209,
138. 126, 114,
102, 89, 77, 65, 52, 40, 27, 14,
2, -10, =22, =35. -47,
-97, -109, -121, 134, -60, -72, -85,
-146, -158, -170, -181,
-193, -205, -216, -227, -239, -250, ~261,
-282, -292, -302, -312, -322, -332, =271,
-360, -368, -377, -385, -394, -401, -341, -351,
-424, -431, -437, -444, -450, -456, =-409, -417,
-471, -476, -481, -485, -489, -492, -461, -466,
-501, -503, -506, -507, -509, -510, -495, -498,
B31.
-511, -511, -511, -510, -509, -508, =506, =511,
-502, -500, -497, -494, -490, -487, -504,
-474, -469, -464, -458, -483, -478,
-427, -420, -413, ~405, -452, -446, -440, -434,
-397, -389, ~581, =372,
-364, -355, -346, -336, -327, -317,
-287, -276, -265, -255, -244, -232, -307, -297,
£221, =210,
Faster and Faster m
mm
#define FIX_H
#define ABS(X) (X<0?-X:X)
#define COS(X) cos_table[ABS(X)&2551]
#idefine SIN(X) sin_table[ABS(X)&255]
const int NUMBER_OF_DEGREES =256;
const int SHIFT = 9;
const Long SHIFT_MULT = 1L<<SHIFT;
extern long cos_table[NUMBER_OF_DEGREES];
extern long sin_tablelNUMBER_OF_DEGREES];
#Hendif
BB
void Lloopfunc()
{
int a=0;
for (int i=1; i<1000; i++) {
at++;
}
}
This program consists of a main() function which calls a second function, loopfunc(),
which consists of a For(loop in which there is a single instruction.
Compile (or run) this program and enter Turbo Profiler. Then, instead of profiling
the program, choose the Disassembly option from the View menu. (You can do this
from DOS Turbo Debugger; too.) This opens a window containing the actual assembly
language code produced by Borland C++ from this short program.
Look at the portion of this disassembly that represents the loop in loopfunc(). It
consists of four instructions:
HLOOPH#17:
INC DX
INC AX
CMP AX,03E8
JL #LOOP#17 (0021)
The first of these instructions, INC DX, increments the value of the variable a, which is
contained in the DX register. The second increments the variable i, the index of the
loop, which is contained in the AX register. The instruction CMP AX,03E8 compares
the value of the variable i with 1,000 (hexadecimal 03E8). The third jumps back to the
if
head of the loop this value has not yet been reached. The hex location for the jump
instruction may appear different on your machine since your memory configuration is
different from the example above.
at+;
at+;
at+;
at+;
at+;
at+;
at++;
at+;
at+;
at+;
}
By repeating the instruction only ten times and incrementing the index of the loop by
tens instead of ones, we achieve the identical effect without all the bother. If you dis-
assemble this loop, you'll find that the mechanism of the loop now represents a much
smaller percentage of the loop code and thus slows it down by a much smaller amount.
Removing the overhead for loop mechanics in this is
manner
If
often a good, quick-
and-dirty way to speed up time-critical program code. the contents of the loop are
slight (and thus likely to be overwhelmed by the loop mechanics), the acceleration
achieved by this unrolling the loop technique can be impressive, especially
is nested inside one or more outer loops.
if the loop
We can use this technique on the matmult() function. The result looks like this:
resultCilCjl=C(mat1CiIC0]*mat2C0ICj1)
+(mat1CilC11*mat2C11C51)
+(mat1CilJC21*mat2C21Cj1)
+(mat1CilC31*mat2C3ILj1))>>SHIFT;
—-
Build Your Own Flight Sim in C++
If you'll look back at our previous implementation of matmult() earlier in this chapter,
you'll see that we've replaced the innermost loop with a long series of additions, effec-
tively unrolling it. You also notice that we haven't bothered to unroll the two outer
loops. That's because a quick consultation with Turbo Profiler told us that, while
unrolling the innermost loop would produce a noticeable gain in execution speed,
unrolling the outer loops would produce almost no benefit at all. The SHIFT const is
used to correct the floating-point arithmetic. Likewise, the function argument is
shifted
before being stored in the scale() function:
void scale(int sf)
{
The rest of this function is about the same as it was before. Similar changes to trans-
late() are made to the parameters of that function for storage in the tmat[4][4] matrix:
void translate(int xt,int yt,int zt)
{
Jig mE
Faster and Faster
"Ew
known as a viewport) within the larger display, this function will execute much more
quickly than before.
We'll use simple calls to kbhit() and getch() to read the keyboard. The variables xrot,
yrot, and zrot, which determine how quickly the object will rotate, are altered accord-
ing to which key is pressed. The listing for the main body of OPTDEMO is on the disk
under the BYOFS\OPTIMIZE directory and in Listing 9-5. To load the program, use the
Open Project selection under the Project menu to load OPTDEMO.IDE. To run the
program from DOS, type:
OPTDEMO filename
where Filename is the name of the file containing the object description data that you
wish to use. Object files compatible with this program have the file extension .TXT at
the end of their filenames. One such file is included in the OPTDEMO project and log-
ically called OBJECTS. TXT. We can cycle between the three different objects in this
description file by pressing the key.
world_type world;
Pcx bgobject;
void main(int argc,char* argv[1)
{
int key;
int xangle=0,yangle=0,zangle=0; // X,Y&Z angles of
// object
int xrot=0,yrot=0,zrot=0; // X,Y&Z rotations
unsigned char *screen_buffer; // Offscreen drawing
// buffer
continued on next page
Build Your Own Flight Sim in C++
— "EE EE EE
continued from previous page
if (argc!=2) {
if (bgobject.load("3dbg2.pcx")) {
// video mode
setgmode (0x13); // Set mode 13h
setpalette(bgobject.Palette());
unsigned char *ptr = bgobject.Image();
for(long 1=0; i<64000; i++) // Put background
screen_bufferLil=*ptr++; // in buffer
int curobj=0; // First object
int scalefactor=1;
int zdistance=600;
// paint the background to the video screen:
putwindow(0,0,320,200,FP_OFF(screen_buffer), FP_SEG(screen_buffer));
while (key!=27) {
clrwin(10,8 ,WIND_WIDTH,WIND_HEIGHT,screen_buffer);
inittrans(); // Initialize transformations
scale(scalefactor); // Create scaling matrix
rotate(xangle,yangle,zangle); // Create rotation matrix
// Rotate object one increment:
xangle+=xrot;
yangle+=yrot;
zangle+=zrot;
// Check for 256 degree wrap around:
if (xangle>255) xangle=0;
if (xangle<0) xangle=255;
if (yangle>255) yangle=0;
314 =
Faster and Faster
CLL
if (yangle<0) yangle=255;
if (zangle>255) zangle=0;
if (zangle<0) zangle=255;
// Translate object:
translate(0,0,zdistance);
//Call the Draw world object
world.Draw(curobj,screen_buffer,XORIGIN,YORIGIN);
case 13:
zdistance+=30;
break;
case Tol
// "=": Decrease distance
if (zdistance>530) zdistance-=30;
Faster and Faster m
"wu
break;
} }
3} |
HE EE EE
EB 317
Hidden Surface
Removal
In these last few chapters, we've built up an arsenal of tools with which to
construct a polygon-fill world. But there's one aspect of the real world that we haven't
modeled yet—the simple fact that things can get in each others way, i.e., that one
object in the real world can obscure the view of another object. Although we take it for
granted in our everyday lives that we are unable to see through walls, in the programs
that we've written so far, we actually can see through walls. The problem is, for realis-
tic simulation, we don’t want to.
There must be an easy solution to this problem, right? After all, making objects
opaque actually involves less drawing than making them transparent. And anything
that requires less drawing must be easier than something that requires more drawing.
What could be more obvious?
Unfortunately, its not that simple. As illustrated in Chapter 9, where we talked
about optimization, less is definitely better in computer animation. The less our pro-
gram has to do, the faster it can animate our three-dimensional world. But our problem
here is not in doing less drawing: the problem lies in figuring out what needs to be
drawn and what doesn't. As we shall see in this chapter, theres really no satisfactory
solution to this problem, at least not for the majority of this generation of microcom-
puters. Essentially, we must choose an acceptable compromise.
321
NEB
hid - - BE BE
The Problem
When we draw a polygon on the video display, it represents a surface that is
at a spe-
cific distance from the viewer, as represented by the z coordinates of the vertices of the
polygon. Polygons with larger z coordinates are farther away than polygons with
smaller z coordinates. In the real world, if a closer object moves in front of a farther
object, the farther object is obscured by the closer object (assuming that the closer
object is opaque).
Although the polygons in our imaginary world have differing z coordinates, because
they
are at different distances from the viewer, the polygons drawn on the video display
to represent those polygons have no z coordinates at all. They differ only in their x and
y coordinates. That’s because the video display is flat. Thus, there’s nothing that says a
polygon representing a closer polygon will necessarily obscure a farther polygon, since
the video display has no way of knowing which polygon is closer and which
That must be determined by the program before the drawing is performed (or, at
is farther.
322 ®
Hidden Surface Removal
"EEN
The Painter's Algorithm
The idea behind the Painters Algorithm is simple: You draw all of scenes
a polygons in
back-to-front order, so that the polygons
in the foreground are drawn over the poly-
gonsin the background. That way, the closer polygons neatly obscure the farther
poly-
gons. The name of the algorithm comes from the notion that this is how a painter
constructs a scene, first drawing the background, then drawing the background objects
over the background, and finally drawing the foreground objects over the background
objects, as in Figure 10-1.
This would seem to solve our problem, but there’ a catch. Drawing polygons in
back-to-front order does indeed take care of hidden surface removal, but how do we
determine what the back-to-front order actually is? This is a more difficult problem
than you might guess.
The obvious answer is to sort the polygons in the reverse order of their z coordi-
nates, using a standard sorting algorithm. This operation is called a depth sort and is
the first step in the implementation of the Painter's Algorithm. But when sorting the
polygons by z coordinate, which z coordinate do you use? Remember that each poly-
gon has at least three vertices and that each of these vertices has its own z coordinate.
Bow om Wm Em
323
aBuild flores mE BE BE BE
EB
~~
Your Own Flight Sim in C++
(¢) Foreground objects (in this case, people) painted over the
background objects
324 =m
Hidden Surface Removal
"EE
In most instances, all three of these z coordinates will be different. Do you arbitrarily
choose one of the z coordinates, perhaps the z coordinate of the first vertex in the ver-
tex list, and then sort on that? Or do you look for the maximum z coordinate among all
the z coordinates of the vertices—or the minimum z coordinate among all the z coor-
dinates—and sort on that? Or do you use all of the z coordinates to calculate some
intermediate value, perhaps representing the z coordinate of the center of the polygon,
and sort on that?
One reasonable answer to these questions is that it doesn’t matter which of these
you sort on, as long as you use the same z coordinate for each polygon. Depth sorts
have doubtlessly been based on all of the above possibilities, as well as some that
haven't been mentioned here, and each has probably worked well enough to produce
a useable flight simulator.
Figure 10-3 Two polygons overlapping in the z extent. Note that the
maximum z coordinate of polygon A is greater than the minimum z
coordinate of polygon B
Test 1
Do the x extents of the two polygons overlap? (See Figure 10-4.) If not, it doesn't mat-
ter whether the two polygons are in the wrong order or not, since they aren't in front of
one another and can’t possibly obscure one another on the display. This can be deter-
mined by comparing the minimum and maximum x coordinates of the two polygons.
If the minimum x of B is larger than the maximum x of A or the maximum x of B is
smaller than the minimum x of A, the x extents don't overlap. No more tests need be
performed.
HE EB EE EE B=
327
Build Your Own Flight Sim in C++
HE BE BE BE
BEB
need be performed. (It may help, in performing tests 1 and 2, to imagine the polygon
as being surrounded by a rectangular shape—known technically as a bounding rectan-
gle—with one corner at the minimum x and y of the polygon and the opposite corner
at the maximum x and y of the polygon. If
the bounding rectangles of the two polygons
don't overlap, then the polygons can't possibly obscure one another on the screen.)
Test 3
Is polygon B entirely on the far side (i.e., the correct side) of A? (See Figure 10-6.) This
is a rather complicated test and involves some fairly esoteric mathematics. Later
in
this
chapter, I'll give you a standard formula for performing this test and explain the math-
ematics in considerably more detail. For now, simply imagine that polygon A is part of
an infinite plane that extends to all sides of it
and that we must test to see if polygon B
is entirely on the far side of that plane. If so, the polygons pass the test and no more
tests need be performed.
WoW om
WM 329
Build Your Own Flight Sim in C++
mE
EE EEE
Test 4
Is polygon A entirely on the nearer side
of polygon B? (See Figure 10-7.) This test is
much like test 3, except
plane of polygon B. If the
that
itchecks to see if polygon A is on the near side of the
two polygons pass either one of these tests, they are in the
correct order and no more testsneed be performed. (Interestingly, only tests 3 and 4
Hidden Surface Removal
"ow
are necessary to show that the two polygons are in the wrong order. The other three
to
tests check see if the polygons actually occlude one another on the display, but once
a pair of polygons flunks tests 3 and 4, we know that they are in the wrong order.)
Test 5
Do the polygons overlap on the screen? If we've gotten this far, the polygon pair must
have flunked all the earlier tests, so we now know that polygons A and B overlap in the
x,y, and z extents and are in the wrong order. All that remains is to determine whether
this matters—that is, whether one of these polygons is actually in front of the other rel-
ative to the viewer. Just because the x, y, and z extents overlap doesn't mean that one
polygon actually obscures the other, as shown in Figure 10-8. Spotting actual overlap
between polygons involves performing a rather time-consuming, edge-by-edge com-
parison of the two polygons. It might well be quicker to skip this test and swap the two
polygons at this point, since they are known to be in the wrong order.
Mutual Overlap
Once you've performed these five tests, can the polygons be safely drawn in polygon
list order without any hidden surfaces showing through? Not necessarily. There is one
situation that even a depth sort followed by these five tests can't handle. Its called
mutual, or cyclical, overlap and occurs when three or more polygons overlap one
another in a circular fashion, with each polygon overlapping the next and the last over-
lapping the first. In Figure 10-9, for instance, polygon A is overlapped by polygon B
which is overlapped by polygon C which is overlapped by polygon A. If
these polygons
Build Your Own Flight Sim in C++
"a Em
EE EER
Figure 10-8 Two polygons that overlap the x and y extents without
in
physically overlapping one another on the video display
were being sorted for the Painters Algorithm, there would be no correct order into
which they could be sorted. No matter what order we draw them in, atleast one poly-
gon will show through at least one other polygon.
What can be done about the mutual overlap problem? The only fully satisfactory
solution is to split one of the polygons into two polygons, thus eliminating the mutual
overlap. There would then be a correct order into which the polygons could be placed.
But this approach brings a number of problems of its own, not the least of which is the
need to allocate memory on the fly for the new polygon.
Time Considerations
As you can see, the Painters Algorithm can result in a lot of calculations. In particular,
the five tests can be quite time-consuming.
In a worst-case situation, execution time can go up as the square of the number of
polygonsin the scene. (Fortunately, it’s not necessary to compare every pair of polygons
in the scene, just those with overlapping z extents. If you start at one of the polygons in
the list and move forward from that polygon performing the tests on all polygons that
this
overlap polygon in the z dimension, it is not necessary to continue after the first
polygon that does not overlap in the z dimension, since none of the succeeding poly-
gons will overlap this polygon in the z dimension.)
iz2 m
Figure 10-9 Three polygons mutually overlapping one another
333
Build Your Own Flight Sim in C++
"BE EE EE
But surely that’s not possible. After all, there are an infinite number of points in a
plane, or even in a section of a plane, and a polygon is a section of a plane. There’ no way
we can sort an infinite number of points and determine which is nearest to the viewer.
Fortunately, it’s not necessary to sort an infinite number of points. It
is only neces-
sary to sort those points that are going to be drawn—i.e., those that correspond to
the
pixels in the viewport. Unlike the number of points on the surface of a polygon, the
number of pixels in the viewport is finite. If we had some way of keeping track of
whats being drawn at each pixel position in the viewport, we could assure ourselves
that only those pixels representing the points closest to the viewer get displayed. In
effect, we would be performing a separate depth sort for every pixel in the viewport
area of the screen.
And thats exactly what the Z-Buffer algorithm does. It determines which points on
which polygons are closest to the viewer for every pixel in the viewport, so that only
those points get displayed. It requires that the programmer set aside an integer array in
which each element corresponds to a pixel in the viewport. Every time a point on the
surface of a polygon is drawn in the viewport, the z coordinate of that point is placed
in the array’s element that corresponds to the pixel position at which the point was
drawn. The next time a pixelis to be drawn at that same position, the z coordinate of
the
the point represented by the new pixel is compared with the value currently in
array for that position. If the z coordinate in the buffer is smaller than that of the new
point, the new pixel is not drawn, since that point would be farther away than the old
point and is therefore part of a hidden surface. If the z coordinate in the buffer is larger
than that of the new point, the new pixel is drawn over the old one and the z coordi-
nate of the new point is put in the buffer, replacing the old one.
The Z-Buffer algorithm, if correctly implemented, works perfectly: No hidden sur-
faces will show through. An efficiently written Z-Buffer should require relatively few
lines of code and is certainly easier to implement than the Painter's Algorithm.
It has only one flaw—or, rather, two: time and memory. To implement a Z-Buffer in
our program we would have to throw out our current polygon-drawing algorithm
and replace it with a new one. And the new one would unquestionably be slower than
the old one, because we no longer would be able to use a tight loop to draw the hori-
zontal lines that make up the polygon. Instead, we would need to calculate the z coor-
dinate of each point on the horizontal line before we could draw it, compare that 2
coordinate with the one already in the buffer, and only then draw the pixel. Polygon-
drawing speed would slow to a crawl.
In addition, we would need to set aside a buffer containing enough integer elements
to store a z coordinate value for every pixel in the viewport. Assuming that the view-
port covered the entire display—not uncommon in flight simulators—and that a 16-bit
int value is sufficient for each element of the Z-Buffer, it would consume 128K of
memory. Even if we reduced the size of the viewport, the Z-Buffer would still almost
certainly be greater than 64K in size. With the PCs 640K of conventional memory in
great demand by other parts of our program, we simply may not be able to spare this
334 m
Hidden Surface Removal
"Ew
much RAM. So for a DOS-based flight simulator, the memory constraint pretty much
rules out the use of Z-Buffer. (Other platforms which have large available RAM space
would allow us to consider implementation of a Z-Buffer.)
Yet the Z-Buffer has a rather large advantage over just about all other methods of
hidden surface removal. As new polygons are added to a scene, the amount of time
consumed by the algorithm increases linearly, not exponentially—so if you double the
numberof polygons in the polygon list, the time required to perform the Z-Buffer also
doubles. With other algorithms, including the Painters Algorithm, the time might well
quadruple. Thus, above a certain number of polygons, the Z-Bulffer is actually faster
than the Painter's Algorithm. Alas, that “certain number” of polygons is probably well
into the thousands, far more than we'll use in any scene in this book. (The actual
break-even point between the Painters Algorithm and the Z-Bulffer algorithm is impos-
sible to determine precisely, since it would depend on how well the two algorithms are
implemented by their programmers.)
We've discussed the Z-Buffer algorithm in some detail here because it will probably
be an important element of future flight simulators, as processors become faster and
memory more abundant. Already we're starting to see machines capable of generating
an acceptable frame-rate using slower polygon-drawing functions, though the majority
of computer-game players are only starting to gain access to these machines. And
more programs are being designed to take advantage of the protected mode memory
management functions of the 80386, 80486, and Pentium microprocessors, thereby
being able to use all of the memory in a machine, not just the first 640K, which makes
it trivial to find the memory necessary for the Z-Buffer array.
If we were to implement a Z-Buffer algorithm in our program, how might we do it?
As noted a few paragraphs ago, it would have to be done in the polygon-drawing func-
tion. The variation on Bresenham’s algorithm that we use to draw the edges of the poly-
gon would need to be extended to three dimensions, so that a second error term could
keep track of the z coordinate at each point on the edge. Then, when drawing the hor-
izontal lines from the left edge to the right edge, yet another variation on Bresenham’s
could be used to calculate the changing z coordinate. This z value would be the one
placed in the Z-Buffer, to determine whether the pixels are to be drawn or ignored.
Readers interested in creating such a Z-Buffer-based variation on the flight simulator
in this book are encouraged to do so. I would be interested in hearing what sort of
results you achieve.
remember, the shape_type class from chapter 6 evolved into the polygon_type class of
Chapter 8. As our class improved and changed its functionality, we provided some pro-
gressive organization by renaming the class. One of the reasons why we renamed the
class was to avoid confusion in the source code modules across these different demon-
strations and projects. An alternative to this might have been to derive another class
from the shape_type class and override methods as needed. As previously said, this
would have led to some confusion in discussing objects. In our goal project, the final
flight-simulator program, we will be deriving classes within the polygon family. Our
fields will be shuffled around and divided between parent and child classes. Let's avoid
confusion—both in discussing classes and functions and in using them to write the
program. If we used our polygon_type class for the final big show, it would create data
structures in RAM which were unnecessarily large; fields would go unused but still take
up our valuable memory resources. We will take advantage of this opportunity to
streamline the code. Some of the member functions of these classes will not be used
because there will be fundamental changes which make the functions obsolete. Leaving
these functions in the code will take up memory space, an arguable point on comput-
ers today, but cleaning up the flotsam of an older version of a class assists in the over-
all organization of the program.
We will rename the classes into a notation that capitalizes the first letter of the
class name. The _type suffix will be dropped, such that world_type becomes World,
object_type becomes Object, etc. Some of these classes will be moved into their own
modules. These will be the classes used in the final flight simulator program. Unused
variables will be discarded.
The files referred to in this chapter can be found in the FSIM directory. The
POLY.CPP file has been promoted to POLY2.CPP (and POLY.H to POLY2.H). In chap-
ters that follow, as we explore lighting and scenery, a module filename may deviate
from its original name in order to show that changes were made to the class or func-
tion operation for a particular demonstration. Thus the WORLD.CPP file will be
named WORLD_FR.CPP in the fractal program and WORLD_TX.CPP in the texture
mapping program. The differences in these files and the final program are sometimes
miniscule, but it keeps any special class implementation for a particular project in its
own module.
class PolygonList {
private:
int number_of_polygons;
Polygon **polylist;
public:
PolygonList():polylist(0){;}
“PolygonList();
void Create(unsigned int polynum);
int GetPolygonCount() const
{ return number_of_polygons; }
Polygon * GetPolygonPtr(int polynum) const
{ return polylistLpolynuml; J}
PolygonList::"PolygonList()
{
if(polylist)
delete [J] polylist;
Access functions, GetPolygonCount() and GetPolygonPtr(), provide inline routes to
retrieve the list information. To perform the actual memory allocation of the list, the
Create() is called with a single parameter containing the number of polygons which the
world contains (as obtained from our doctored loadpoly routine). It would be a neater
design to have a constructor do this allocation for us via a constructor parameter, but
then this forces us to know how many polygons are in the world before we make a
PolygonList object. One of our future treatments for PolygonList is as a member field of
another class. We do not want to
require that class to be constructed with the world
polygon count (so that it can pass the parameter to the PolygonList constructor). The
allocation of the Polygon array therefore takes place in a separate function, Create()::
void PolygonList::Create(unsigned int numPoly)
{
Now we need a function that will assemble a polygon list from our existing world
database. As you see above, it
is called MakePolygonList(). In theory, this function will
be quite simple. All it needs to do is to loop through all of the objects in the world and
assign their polygons to the master polygon list. In practice, however, it’s desirable to
have it do more than that. For one thing, if we can eliminate some polygons at this
stage of the game, it will speed up things further down the line.
How can we prune the polygon list? There are two obvious ways. The first is to
apply backface removal at this point. In theory, half of the polygons in any scene are
backfaces. By allowing only “front faces” into the polygon list, we effectively cut the list
in half, which is a significant savings. This, in fact, is the main reason that backface
removal is used in programs that also use other methods of hidden surface removal: it
gets rid of the easy hidden surfaces with minimal effort.
The second way we can prune the polygon list is to look for polygons that are on
the wrong side of the screen—i.e., that are on the same side of the screen as the
viewer. These polygons can't possibly be in the scene, so there’ no reason to include
them in the list. The simplest way to spot these polygons is to look for polygons that
have a maximum z coordinate that is less than that of the screen, which we will set to
1. (Zero might seem a more logical number for the z coordinate of the screen, but that
can cause some problems with the perspective calculations, which have difficulty han-
dling z coordinate values that are either zero or negative.) This should eliminate
Hidden Surface Removal
"ow
another 50 percent or so of the polygons in the scene, so that our final polygon list
should only contain about 25 percent of the polygons in the world. This will save
much time down the road. (Of course, there will be other polygons that eventually turn
out notto be visible, either because they are hidden by other polygons or because they
are outside the viewport. But identifying these polygons is not easy and must be per-
formed after the assembling of the polygon list.)
The MakePolygonList() function takes one parameter, a reference to an instance of
class World. This parameter is
declared as a const since the instance itself will not be
changed. The function will use the parameter to create the polygon list, which will then
be available to the calling function:
void PolygonList::MakePolygonList( const World& world)
While assembling the polygon list, it is important to record the number of polygons
it
that actually make onto the list, so that later functions will know how many polygons
they must process. Thus, the function begins by initializing a counter to record this
value:
{
If the polygon isn't a backface, we now need to determine what its maximum and
minimum world coordinates are, for reasons that will shortly become apparent. The
first step in doing so is to set a dummy maximum and minimum for each x, y, and z
coordinate. We'll give these dummies the lowest values possible of a signed int type, so
that they will immediately be replaced by the first real values with which we compare
them:
pxmin=vptr->wx;
}
pymin=vptr->wy;
}
340 ®
Hidden Surface Removal
mm n
if (vptr=>wz > pzmax) {
pzmax=vptr->wz;
}
Now that we've determined the real maximum and minimum coordinate values, we
transfer them to the appropriate fields in the polygon structure, so that they can be
referred to later. There are two inline functions to perform this transfer, SetMin() and
SetMax():
if (pzmax > 1) {
polylistLcount++I1=polyptr;
}
The value of count is incremented as the polygon is added to the list, so that it now
contains the number of polygons currently in the list and can be used to point to the
next element in
the list. Then we wrap up all the loops, put the number of polygons in
the appropriate field of the PolygonList structure, and exit the function:
3a
. Ps
Build Your Own
}
Flight Sim in C++
polylist structure:
- mH
EEE
number_of_polygons=count;
}
J42 m
// Loopthrough all vertices in polygon, to find
// ones with higher and Lower coordinates than
// current min & max:
pxmax=(int)vptr->ax;
if (vptr-=>ax < pxmin) {
pxmin=(int)vptr->ax;
if (vptr->ay > pymax) {
pymax=(int)vptr->ay;
if (vptr=>ay < pymin) {
pymin=(int)vptr->ay;
if (vptr->az > pzmax) {
pzmax=(int)vptr->az;
if (vptr->az < pzmin) {
pzmin=(int)vptr->az;
}
HE BE BE EE B=
343
Bud Your Own Flight Sim in C++
mm EEE
Once the polygon list is assembled, the hidden surface removal can begin. The first
step in the Painter's Algorithm is to perform a depth sort on list. That process will
the
For clarity,the sort used here is a variation of the type known as a bubble sort, the
simplest variety of sort available. It’s also one of the slowest. For world databases con-
taining a relative few polygons, the slowness of the bubble sort won't make much of a
difference in overall execution time, since it doesn't take much time for any sorting
algorithm to put the polygon list in order. But as the world database grows larger and
more complex, the bubble sort requires an increasingly long time to sort the polygon
list. This gives you an easy way to optimize the flight-simulator code: replacing this sort
with a faster one to accelerate the process. Any good book on computer-programming
algorithms should contain instructions for implementing more efficient sorting tech-
niques. An immediate alternative to the bubble sort is to use the ANSI C++ sort
function—we’ll save this optimization for the final flight simulator version of z_sort.
For those not familiar with it, the bubble sort algorithm is quite straightforward. It
consists of two nested loops, the outer one a While loop that repeats an inner for loop
over and over until the sort is complete. The for loop, in turn, steps through all of the
items in the polygon list except the last, comparing each with the item following it to
see if the two are in the proper order relative to one another. (The loop doesn't step all
the way to the last item because the last item has no item following it.) When a pair is
found to be out of order, their positions are swapped. This process is repeated until
the for loop makes a complete pass through the list without finding a pair out of
order. The name of the algorithm comes from some programmer's observation that
out-of-order items tend to rise like bubbles through the list until they reach their
proper positions.
The variable swapflag indicates whether a swap has been performed on a given
pass through the list. At the top to the loop,it
is set to 0, indicating that a swap has not
been performed. The outside loop is written as a do-while loop, which forces the
inner loop code to execute at least once:
int swapflag;
do {
swapflag=0;
The for loop then searches the list for out-of-order pairs:
for (int i=0; i<(number_of_polygons=1); i++) {
344 ®
Hidden Surface Removal
In this case, since we are sorting in reverse order by the distance variable member of
each polygon, the code checks to see if the first polygon has a smaller distance than the
mm
.
second polygon. If it does, the polygons are in the wrong order; we want polygons with
higher distance values to come first in the list so they will be drawn first. Thus, we
swap the two polygons. The Polygon pointer temp holds the value of one of the poly-
gons while the swap is performed, so that it won't be written over by the value of the
other polygon:
Polygon *temp=polylist [i];
polylist-Lil=polylist Ci+1];
polylistlLi+1]=temp;
This swapping
isone of the advantages of using pointers instead of using an array of
Polygon objects. Instead of copying the memory for the entire Polygon object, the pro-
gram only has to copy the address of the object. Besides saving time (copying 4 bytes
instead of 16), it also could prove to be more flexible in the future, since swapping
pointers isnot affected if the class structure changes. To indicate that a swap has been
performed, swapflag is set to -1:
swapflag=-1;
Finally, the While statement closes the outer loop, allowing exit from the loop only if
swapflag is 0, meaning an entire pass through the inner loop was made with no objects
swapped.
Ywhile(swapflag);
And thats allthere is to performing a depth sort. Once the While loop terminates,
the polygon list will be in order—almost. The complete text of the z_sort() function
appears in Listing 10-2.
int swapflag;
do{
swapflag=0;
// Loop thorugh polygon List:
for (int i=0; i<(number_of_polygons=1); i++) {
// Are polygons out of order?
if (polylistLil->GetDistance()
< polylistlLi+1]->GetDistance()) {
// if so , swap them...
// we're swapping pointers and not the
// actual objects!
Polygon *temp = polylist[il;
polylistlil = =polylistLi+1];
polylistLi+1] temp;
continued on next page
345
EEE EEN
uid
:
If we are to resolve all of the discrepancies in polygon ordering left behind by the
depth sort, we must now perform the five tests described earlier in the discussion of
the Painters Algorithm. But it isn't strictly necessary that we resolve these discrepan-
fact, we are not going to do so in the finished flight simulator. It is pos-
cies—and, in
sible to create a commercial-quality flight simulator using only the depth sort (which
we just performed with the z_sort() function) for hidden surface removal. Doubtlessly
many flight simulators on the market now or in the past do precisely this. Without the
depth sort, the program is leaner and faster; fewer operations must be performed to
order the polygons for each frame.
The trick is not to include any concave polyhedra in the scenery database. You'll
note that this is precisely the case in the scenery databases of many flight simulators,
where buildings are represented by cubes and mountains by complex pyramids. As
long as only convex polyhedra are used and the objects are not huddled too closely
together, polygon ordering discrepancies simply will not occur. A depth sort per-
formed on top of backface removal will be adequate for hidden surface removal.
Even though we are not going to be using the five tests in our flight simulator, we've
provided in the file POLYLIST.CPP in directory BYOFS\FSIM on the distribution disk a
set of six functions tohelp the reader develop his or her version of the five tests—or, at
least, the first four. (The fifth isn't absolutely required, as noted earlier, though you may
want to try your hand at it anyway.) These functions are z_overlap() (which checks for
overlap between two polygons in the z extent), should_be_swapped() (which returns a
nonzero or true value if a pair of polygons are in the wrong order), and the three func-
tions that perform the first four tests (the first of these performs the first two tests in
one function): xy_overlap(), surface_inside(), and surface_outside(). In the rest of this
chapter, we'll describe the workings of these functions in some detail. However, since
they are not used in the final program, these functions, and their declarations within
their respective classes are surrounded by conditional compiler directives:
#ifdef HID_SURF_TESTS
#endif
This allows us to include the code in our program, but only to compile it when we
define HID_SURF_TESTS5 either under our Project Options - Compiler Defines or
within the program source files. In the next chapter when we revisit our class declara-
tions, this directive will appear within some of the classes. This provides us with con-
trol of the compiler to build the classes with, or without, the additional hidden surface
testing functions. Removing the tests from our program also allows us to remove the six
data fields which are stored in the polygon and hold the minimum and maximum
Hidden Surface Removal
uo. un
values for x, y and z coordinates. Since these fields are only stored for the additional
hidden surface test functions, they can be removed from the class with the same con-
ditional directives. The MakePolygonList() function will then be changed since we
only need to store this information if we are using the hidden surface test:
#ifdef HID_SURF_TESTS
//
1f we plan on using the 5 additional tests for
//
hidden surface removal, then
//
put mins & maxes in polygon descriptor:
polyptr->SetMin(pxmin, pymin, pzmin);
polyptr->SetMax(pxmax, pymax, pzmax);
#endif
The first test, z_overlap(), checks to see if the z extents of two polygons overlap,
returning a nonzero value they do, zero if they do not.
if
Ittakes one Polygon reference
as parameter. Although we have yet to discuss the Polygon class, we can assume that
the variables zmin and zmax are accessible as data fields within the class:
int Polygon::z_overlap(const Polygon& poly2)
{
Recall that checking for z extent overlap is a matter of checking to see if the minimum z
of polygon A (the instance which called the class function) is less than the maximum
z of polygon B or if
the minimum z of polygon B (the first parameter) is less than the
maximum z of polygon A:
if ((zmin>=poly2.zmax)
return 0;
|| (poly2.zmin>=zmax))
Should neither of these conditions be true, we want to return a nonzero value, indi-
cating z overlap:
return -1;
}
HE
5 BE
EN 347
Build Your Own Flight Sim in C++
"EEE EEE
continued from previous page
The z_overlap() function is short but to the point. The should_be_swapped() func-
tion, which returns a nonzero value if the two polygons are out of order, is a little
bulkier, but only slightly so. Itcalls three more functions to perform the first four of the
tests described earlier in the chapter. Should the two polygons pass any of these tests
(i.e., should any of these three functions return a nonzero value), the
should_be_swapped() function exits with a zero value, indicating that the two poly-
gons don't need to be swapped. Only if the two polygons flunk all three tests does the
function return a nonzero value. The function is so self-explanatory that, instead of
breaking it down line-by-line, we've simply printed the text in Listing 10-4.
348 ®
Hidden Surface Removal
"Ew
Listing 10-5 The xy_overlap() function
int Polygon::xy_overlap(const Polygon& poly2)
// Check for overlap in the x and y extents, return
// nonzero if both are found, otherwise return zero.
{
return -1;
}
The variables x, y, and z are the x,y,z coordinates of a point. (We'll have more to
say
about these coordinates in a moment.) The variables A, B, C, and D are called the coef-
ficients of the plane, which can be derived from the coordinates of any three points on
the surface of the plane (as long as all three points don't happen to fall on a single line).
If the coordinates of those three points are represented by the variables
(x,,y,,z,),
(x2,¥2,22), and (x3,y3,25), then the coefficients of the plane can be derived through these
four formulas:
A= yi lz, = 23) + y,(z5 = zy) + ys(z, = z;,)
B = z;(x; = x3) + z,(x;5 = xy) + z3(x; = Xx;)
C= x(y; = y3) + x(ys = y;) + x3(y; = y;)
D = =x1(y,2z5 = y32;) = X,(ysz; = y,z5) - x3(y12; = yp24)
Now that we know both the plane equation and the formulas necessary for deter-
mining the coefficients of a plane, we can use these tools to determine a point lies on if
a particular plane (and, if
not, on which side of the plane the point lies). We plug the
coefficients of the plane into the A, B, C, and D variables of the plane equation and the
x,y, and z coordinates of the point into the x, y, and z variables and solve the equation.
349
Build Your Own Flight Sim in C++
If the result is zero, the point lies on the plane. If the result is negative, the point lies on
the counterclockwise side of the plane, the side from which the three points that we
used to calculate the coefficients appear to be arranged in a counterclockwise manner.
(Remember that these points cannot be on a single line; they must have a triangular
relationship to one another and therefore form the vertices of a triangle.) If the result is
call the
positive, the point lies on the opposite side of the plane—what we might
clockwise side of the plane.
Now we can determine if polygon B (the polygon determined by the depth sort to
be the more distant from the viewer of the polygon pair) is on the far side of polygon
A, where it belongs. How? By using the plane equation to determine on which side of
the plane of polygon A each of the vertices of polygon B lies. (Each vertex, remember,
is simply a point in space with x, y, and z coordinates that can be plugged into the
plane equation.)
The logic of the processis a tad complicated, however. Recall from Chapter 8 that
the vertices of all the polygons in the world database have been designed so that,
when viewed from outside the object of which they are part, the vertices arrange in a
counterclockwise manner. This makes the backface removal operate correctly. By the
time the depth sort is performed, backface removal has pruned all polygons from the
list that do not have their counterclockwise faces turned toward the viewer. Thus, if a
point lies on the clockwise side of one of the polygons that remains, it must be on the
opposite side from the viewer.
It follows that we can tell whether polygon B is on the opposite side of polygon A
from the viewer (which is where it’s supposed to be after the depth sort) by testing to
see if all of its vertices are on the clockwise side of the plane of polygon A. Here’ the
procedure: First we calculate the coefficients of the plane of polygon A using the x, y,
and z coordinates of the first three vertices of polygon A as the three points on the
plane; next we plug the resulting coefficients into the A,B,C, and D variables of the
plane equation; then we use a for loop to step through all of the vertices of polygon B,
plugging the x, y, and z coordinates of each into the x, y, and z variables of the plane
equation. If the resulting plane equations evaluate to positive numbers for every one of
the vertices of polygon B, then they are all on the clockwise side of polygon A and thus
the entirety of polygon B must be on the clockwise side of polygon A. Since this is the
far side of the polygon, the two polygons are in the correct order and don't need to be
swapped. No more tests need be performed.
Let's look at the actual code that performs the test. The function surface_inside()
performs test 3, checking to see ifpolygon A is on the far (clockwise) side of polygon B.
It returns nonzero if it is. The function takes one Polygon parameter—poly2 (which
will be used in our code for polygon A):
int Polygon::surface_inside(const Polygon& poly2)
Before we can use the plane equation, we must calculate the coefficients of the plane of
poly2. For the three points on the plane, we'll use the first three vertices of poly2:
Hidden Surface Removal
"ew.
// Calculate the coefficients of poly2:
long x1=poly2.vertex[0l->ax;
long y1=poly2.vertex[01->ay;
long z1=poly2.vertex[0l->az;
long x2=poly2.vertex[11->ax;
long y2=poly2.vertex[11->ay;
long z2=poly2.vertex[1]1->az;
long x3=poly2.vertex[2]->ax;
long y3=poly2.vertex[21->ay;
long z3=poly2.vertex[2]->az;
long a=y1*(z2-23)+y2*(23-21)+y3*(21-22);
long b=z1*(x2-x3)+22*(x3-x1)+23*(x1-x2);
long c=x1*(y2-y3)+x2*(y3-y1)+x3*(y1-y2);
long d==x1*(y2*z3-y3%22)-x2*(y3*21-y1%23)-x3*(y1*22-y2%21);
Our numbers can get pretty large when making these calculations, so we use variables
of type long to guard against integer overflow, which can create difficult-to-find bugs in
the code. If your computer can handle floating-point arithmetic, you
may want to
use
type float for even better protection against overflow.
Now we need to loop through all of the vertices of polyl, plugging the x, y, and z
coordinates of each into the plane equation, along with the coefficients we just calcu-
lated. If the plane equations using these numbers evaluate to anything except a positive
result, the test has been flunked and we return a zero value to the calling routine. If
only positive values are detected, a nonzero value is returned, indicating that poly1 is
indeed on the far (clockwise) side of poly2:
int flunked=0;
for (int v=0; v<number_of_vertices; v++) {
if((a*vertexCvl->ax+b*vertex[Lvl->ay
+c*vertexCvl->az+d)<0) (
flunked=-1; // If less than 1, we flunked
}
// break;
}
return !flunked;
}
mE ENE
| Build Your Own Flight Sim in C++
int flunked=0;
for (int v=0; v<number_of_vertices; v++) {
if(C(a*vertex[vl->ax+b*vertex[Lvl->ay
+c*vertex[vl->az+d)<0) {
flunked=-1; // 1f Less than 1, we flunked
// >
break;
}
return !flunked;
}
Test 4 is pretty much the same as test 3, except that we are now checking to see if
polygon A is entirely on the near (counterclockwise) side of polygon B. To do this, we
simply reverse the role of the two polygons, taking the coefficients of this instead of
poly2 and using the for loop to step through the vertices of poly2. We are now looking
for negative values instead of positive values. The complete text of the surface_outside()
function appears in Listing 10-7.
352 ®
Hidden Surface Removal
Ew—
y2=vertex[1]->ay;
z2=vertex[1]1->az;
x3=vertex[21->ax;
y3=vertex[2]1->ay;
z3=vertex[21->az;
a=y1*(z22-23)+y2*(z23-21)+y3* (21-22);
b=z1*(x2-x3)+22*(x3-x1)+23*(x1-x2) ;
c=x1*(y2-y3)+x2*(y3-y1)+x3*(y1-y2);
d==x1*(y2%23-y3%22)-x2*(y3*z1-y1%23)-x3*(y1*z2-y2%21) ;
// Plug the vertices of poly2 into the plane equation
// of this poly, one by one:
int flunked=0;
for (int v=0; v<poly2.number_of_vertices; v++) {
if((a*poly2.vertexLvl->ax+b*poly2.vertex[vl->ay
+c*poly2.vertexCvl->az+d)>0)
flunked=-1; // If less than 1, we flunked
}
// break;
}
return !flunked;
We've avoided this limitation up until now by deliberately placing all ofthe action
in the 3D world directly in front of the window. We haven't missed
seeing anything
yet because there hasn't been anything to miss. What we saw was all there was tosee.
To be fully realistic, however, our three-dimensional world
must include features
that are not in front of the window. We aren't putting on a stage production here,
where the only real action takes place within the confines of a proscenium arch.
Neither are we making a movie, where the only action going on beyond the reach of
the camera is a crowd of bored crew members standing around drinking stale coffee.
We're building an entire world—and creating a window into that world.
As in the real world, objects will be moving in and out of view. This means that
our
program must somehow decide which parts of this simulated world appear in the
window and which do not. This is a relatively trivial problem. To determine which
357
uid Your Own Flight Sim in C++
mW EE EE
polygons are fully within thewindow and which are not, we need merely check to see
if their maximum and minimum x, y, and z coordinates, as calculated in the last chap-
of the window.
ter, fall within the maximum and minimum x, y, and z coordinates
(We'll deal with the concept of a window having a maximum and minimum z coor-
dinate in a moment, when we discuss view volumes.) The real problem arises when
we must deal with a polygon that falls partially inside the animation window and par-
that falls
tially outside of it. We must find a way to draw only that part of the polygon
within the window.
Determining which part of a polygon is inside the window is called polygon clip-
ping. Like the hidden surface problem that we discussed in the last chapter,
this is
it has
not a trivial problem. Fortunately, unlike the hidden surface problem, an
entirely satisfactory solution. Before we get to that solution, however, let’s look at the
problem in more detail.
view through your window is of an apartment building on the opposite side of the
street, then your view is more likely to be several hundred feet wide, as in Figure 11-2(b).
if
Still better, you are able to see to the horizon, your view is probably several miles
wide, as in Figure 11-2(c). And
if
it’s night and the stars are out,
your view is proba-
bly several light years wide, encompassing much of the local portion of the galaxy, as
in Figure 11-2(d).
As a rule, then, we can say that your view becomes wider and wider with distance.
You have a relatively narrow view of things that are close to the window but a wide
view of things that are distant from the window. This, of course, is a result of
per-
spective, which we've discussed already. The rays of light that carry the images of
objects seen through your window are converging toward your eyeballs, so that the
rays of light from objects at opposite ends of the window come together at
when they reach your eyes. The farther away the objects reflecting (or producing)
an angle
those rays of light are, the greater the area encompassed by this angle and the farther
apart the objects are in reality. Needless to say, all of this is true of the vertical dimen-
sion of your view field as well, though this is probably less obvious, since our world
is arranged in a relatively horizontal fashion.
HES HL Hai
(¢) Seeing to the horizon, your view will be several miles wide
Polygon Clipping m m m
(d) Seeing the sky, your view might be several light years wide!
Now imagine that you've been transported into the sky, looking down at your
building from far above. Further, imagine that you can see a beam of light shining out
of your window, representing the area that you can see through the window. This beam
will grow wider with distance, as in Figure 11-3(a). If
you examine the beam from the
it
side, you'll also see that grows widerin the vertical dimension too, as in Figure 11-3(b).
a
Altogether, the beam takes the form of pyramid, with its point at your eyes and its
bottom at the farthest distance you are capable of seeing. (In theory, this distance is
infinite, or at least many light years in length. In practice, however, we can treat the
bottom of the pyramid as being about 10 miles away, the distance of the horizon
viewed from near the ground.) The part of the pyramid that is entirely outside
your
window (and is truncated by your window pane, giving
it a
a flat top) is called
tum and represents your view volume—the volume of space within which you can see
frus-
things through your window. Anything that is completely outside of the view volume
is invisible from your window, even though it might be clearly visible to viewers on
the outside. Anything that is completely inside the view volume is potentially visible
through your window, though it may be blocked by other objects—the hidden sur-
face problem again. Objects that are partially inside the view volume will appear
to be
clipped off at the edge of the window, if they are not blocked by nearer objects.
Build Your Own Flight Sim in C++
-
(b) The area that you can see out your window, seen
from the side
362 m
Clipping against the View Volume
Polygon Clipping
"a.
The animation window on our computer display works exactly like the window in
your room. It allows us to see a pyramid- (or frustum-) shaped view volume within
the numerically described world that we are creating in the computer. Up to now, we
have carefully placed objects so that they are always within this view volume.
Eventually, however, we must allow objects to lie outside that view volume as well.
This means that we'll need some means of clipping polygons against the sides of the
view volume before we draw them. We'll do this by creating a function that we'll refer to
for now as clip() that will accept an unclipped polygon as
input and will return as out-
put a second polygon that represents the portion of the unclipped polygon that
the view volume. This partial polygon can then be passed to the polygon-drawing
is inside
to
function, DrawPoly(), be rendered on the display.
One potential obstacle to performing this task is the way in which the sides of the
view volume slant outward as they move away from the window. The manner in
which we need to clip a polygon varies according to the z coordinate of the polygon—
and, since a single polygon can have several different z coordinates (as we saw in
Chapter 10), determining where the polygon needs to be clipped could become a
tricky problem. Fortunately, there are several solutions to this problem. In the case of
the three-dimensional code that we are developing here, the problem has already been
solved. By passingall of the coordinates of the polygons through the perspective func-
tion Project() before we clip those polygons, we have effectively straightened out the
sides of the view volume. As far as clip() is concerned, the view volume will be a cube
rather than a pyramid, which simplifies things considerably.
In fact, as far as the left, right, top, and bottom sides of the view volume are con-
cerned, we can treat the polygons as two-dimensional shapes to be clipped against the
sides of a two-dimensional animation window. That makes the problem even simpler,
but not quite simple enough. We'll need to use different methods for the front and
back sides of the view volume. For the back side of the view volume, the base of the
pyramid, we won't do any polygon clipping at all. The entire object and all its poly-
gons will be designated as either being included or excluded from our scene. One
implementation might include a field in the Object class that will indicate the maxi-
mum distance at which the object is visible. Beyond that distance, we simply wouldn't
draw the object, so clipping the polygons in that object against the back side of the
view volume is unnecessary. Many games use this idea so that as you journey forward
in the game's 3D world, things seem to pop up on the horizon. For our flight simula-
tor, we will assume that we have clear weather conditions with unlimited visibility so
objects will always be visible (if they are in front of us). The front side of the view vol-
ume is another matter. We'll set the front of the view volume at a z distance of 1,
which will putit is
just in front of the screen. This the same as the MakePolygonList()
function. (Setting it at a z distance of 1 rather than 0 avoids possible problems with
division by 0 in the clip() function.) Unlike clipping against the sides of the window,
uid
—-
Clip polygon A
against the front of the view volume to produce
polygon B. (See Figure 11-4(a).)
Clip polygon B against the left side of the view volume to produce
polygon C. (See Figure 11-4(b).)
Clip polygon C against the right side of the view volume to pro-
duce polygon D. (See Figure 11-4(c).)
Clip polygon againstD the top side of the view volume to produce
polygon E. (See Figure 11-4(d).)
364 ®
Polygon Clipping m m
"
Clip polygon E against the bottom side of the view volume to
produce polygon F. (See Figure 11-4(e).)
Return polygon F. (See Figure 11-4(f).)
Clipping a polygon against an edge is a matter of building up, edge-by-edge, a new
polygon that represents the clipped version of the old polygon. We can think of this
as a process of replacing the unclipped edges of the old polygon with the clipped
edges of the new polygon. In order to determine the coordinates of the vertices of the
new polygon, each edge of the old polygon must be examined and classified by type,
(¢) Clipping against the right side of the view (d) Clipping against the top of the view
volume volume
(e) Clipping against the bottom of the view (f) The clipped polygon
volume
. Build Your Own Flight Sim in C++
depending on
being clipped.
There are four types of edge that the clipping function will encounter. All four are
"EEE
falls relative to the side of the view volume against which it is
illustrated by the polygon in Figure 11-5(a), which overlaps the left edge of the ani-
mation window (and thus the left side of the view volume). The arrows show the
direction the algorithm will work around the
edges of the polygon, which is the same
as the order in which the vertices are stored in the polygon’ vertex array. The four
types of edge:
1. Edges that are entirely inside the view volume (such as edge 1 in Figure 11-5)
2. Edges that are entirely outside the view volume (such as edge 3)
3. Edges that are leaving the view volume—that is, edges in which the first ver-
tex (in the order indicated by the arrows) is
inside the view volume and the
second vertex is outside (such as edge 2)
4. Edges that are entering the view volume—that is, edges in which the first ver-
tex is outside the view volume and the second vertex is inside (such as edge 4
in the illustration)
Each type of edge will be replaced by a clipped edge in a different way, as follows.
1. Edges that are entirely inside the view volume will be replaced by identical
edges. In effect, these edges will be copied unchanged to the new polygon.
2. Edges that are entirely outside the view volume will be eliminated. For each such
edge in the old polygon, no new edge will be added to the clipped polygon.
3. Edges that are leaving the view volume will be clipped at the edge of the view
volume. The first vertex of such an edge will be copied unchanged to the
clipped polygon, but the second vertex will be replaced by a new vertex hav-
ing the coordinates of the point at which the edge intersects the side of the
view volume.
4. Edges that are entering the view volume will be replaced by two new edges.
One these will be the old edge clipped at the point where
of
it
intersects the
side of the view volume. The other will be a brand new edge that connects
the first vertex of this edge with the last vertex of the previous type 3 edge—
the leading edge that was clipped at the side of the view volume.
If you start with edgeof the polygon in Figure 11-5(a) and clip each edge men-
1
tally according to the procedures outlined above, you should eventually wind up with
the clipped polygon shown in Figure 11-5(b). The rough skeleton of the procedure
for clipping against an individual edge is
as follows.
66m
Polygon Clipping m
LL
(b) What remains after the clipping procedures have been applied against the
side of the viewport
HE BE BE BE Bm
367
~~ Build Your Own Flight Sim in C++
{
~~
mm
if type 1
edge, perform type 1
replacement
if type 2 edge, perform type 2 replacement
if type 3 edge, perform type 3 replacement
if type 4 edge, perform type 4 replacement
>
The complete clip() function consists of five such loops in the sequence shown sever-
al paragraphs back.
.
Similarly, the formula for finding the y coordinate at which a line crosses the right
edge of the window is
clipped_y = y1 + slope * (xmax - x1);
Clipping against the top and bottom edges works much the same way, except
instead of knowing the x coordinate of the point of intersection we now know the y
coordinate and must calculate the x coordinate using a different version of the same
point-slope formula. We find the y coordinate at which the line crosses the top edge
of the window with the statement:
clipped_y = ymin;
because ymin is the y coordinate of the top edge. We find the y coordinate at which a
line crosses the bottom edge of the window with the statement:
clipped_y = ymax;
because ymax is the y coordinate of the bottom edge.
To find the x coordinate at which the line crosses the top edge of the window, we
use the formula:
clipped_x = x1 + (ymin = y1) / slope;
Similarly, we find the x coordinate at which the line crosses the bottom edge of the
window with the formula:
clipped_x = x1 + (ymaxin - y1) / slope;
Clipping against the front of the view volume is a bit more complicated, since we
must determine both the x and y coordinates of the point at which a line intersects
a plane at a known z coordinate, which we'll call zmin. And we must take into
account the z coordinates of the starting and ending points of the line, which we'll
call zI and 22. To calculate the new coordinates, we use a three-dimensional exten-
sion of the point-slope equation. The formulae for determining the x and y points of
intersection are
clipped_x (x2 = x1) * t + x1;
clipped_y (y2 = y1) * t + y1;
This mysterious factor t, which is similar to the slope factor in the two-dimensional
equations, can be calculated like this:
t = (zmin = 21) / (22 - 21);
The z coordinates will be the same as zmin, but that’s not important, since we won't
need them again in the clipping process. So we'll just throw them away.
And thats all anybody needs to know to write a clipping function. Lets get down
to work.
Build Your Own Flight Sim in C++
mE
EEE EB
The Clipped Polygon Class
The
is
first step in creating a clipped polygon creating a special data structure to hold
the vertices. Why don't we simply create additional data field members within the
existing vertex structure for holding the coordinates of
a clipped vertex? That would
be convenient, since it would allow us to store the array of vertices for the clipped
edges for each polygon within the vertex array already a member of the polygon class.
It wouldn't work, though, because the clipped edges won't necessarily have the same
number of vertices as the unclipped polygons; thus, the arrays may need to be of dif-
ferent sizes. Figure 11-6(a) shows an example. The polygon being clipped has five
edges, yet the clipped version of the polygon will only have four, because two of the
original edges are outside the window and will be replaced by only one new edge con-
necting the clipped edges of the polygon. Similarly, the polygon in Figure 11-6(b) has
three edges, but the clipped version will have four.
Since clipping can both subtract edges from a polygon and add new edges it, we to
must be prepared for the array of vertices to either shrink or grow. Fortunately, no
more than five edges can be added to a convex polygon if we clip against the four sides
of the viewport and the front of the view volume so we don't have to be overly gen-
erous in preparing for additional edges. (Figure 11-7 shows
becomes an eight-sided polygon after clipping.)
a
four-sided polygon that
J7o m
Polygon Clipping m
"ow
(b) This polygon has three edges before clipping, but four afterward
H BE BE BE
= 371
~~" Build Your Own Flight Sim in C++
ee mE HE
ENB
For this
reason, we'll need to create a completely separate array to store the clipped
vertices. As long as we're at it,
it would be convenient to
create a new class to manage
that array, which we'll call ClippedPolygon. The class will need to know how many
vertices are being stored in the array and the color of the polygon. If the hidden sur-
face functions are used, then the six variables to store the coordinates’ minimum and
maximum will be needed. In overview, it
is so similar to the polygon_type class of the
previous chapters (which has been renamed to class Polygon) that it suggests there is
a stronger C++ relationship between the two classes. We will therefore group these
common data elements together into a class, then derive our new class from it. The
declaration for our base class looks like this:
class polyBase {
protected:
int number_of_vertices; // Number of vertices in polygon
int original_color; // Color of polygon
int color; // Drawing color of polygon (after Light
// applied)
Long nlength; // normal Length (for Lighting)
#ifdef HID_SURF_TESTS
int zmax,zmin; // Maximum and minimum z coordinates of
// polygon
int xmax,xmin;
int ymax,ymin;
#endif
public:
polyBase(){;}
int GetVertexCount() const
{ return number_of_vertices; 1}
#ifdef HID_SURF_TESTS
// for outside access to set the
// max and min values:
void SetMax(int x, int y, int 2)
{ xmax=x, ymax = y, zmax = z; 1}
372 =m
Polygon Clipping
"ew.
The polyBase class will be used to form both the Polygon and ClippedPolygon class-
es. The data field members, number_of_vertices, original_color, color and nlength are
declared with protected scope, such that derived classes will have access to them but
the public is denied. The newer fields, original_color and nlength, will be explained
in the next chapter on lighting. We have commented on the inline functions previ-
ously, and in addition, they are very straightforward so need little explanation. A new
addition, however, is the overriden assignment operator function (known as the copy
function) which we will define to copy one instance of polyBase to another.
With the base class declared, the Polygon class can now be reworked as a derived
class of polyBase:
class Polygon: public polyBase {
protected:
friend class Object; // access to vertex pointers
vertex_type **vertex; // List of vertices
Long double distance;
// copy operator is protected since we want to
// avoid possible killing of the vertex array through
// an auto scoped variable
Polygon& Polygon::operator =(const Polygon& polygon);
public:
Polygon():polyBase(),vertex(0){;}
“Polygon();
int backface();
vertex_type * GetVertexPtr(int vertnum)
{ return vertexCvertnuml; J}
mE
EE EEN 373
~~"
:
private:
clip_type vertex[201;
public:
ClippedPolygon():polyBase(){;}
void DrawPoly(unsigned char far *screen);
clip_type * GetClipPtr(int vertnum)
{return &vertexCvertnuml; 1}
struct clip_type {
int x,y,z;
tnt x1,y1,21;
Y;
Just as the ClippedPolygon class is simpler than the Polygon class, so the clip_type
structure is simpler than the vertex_type structure. However, it
still is more compli-
cated than would seem strictly necessary. The x, y, and z fields are used to store the x,
y, and z coordinates of the vertex, but what are the xI, yl, and zI fields for? They're
for storage of the intermediate polygons created by the five stages ofthe clipping func-
tion. In fact, it might seem that we would need five separate sets of coordinate fields
to store these intermediate polygons, but we'll economize by passing the vertices back
and forth between these two sets of fields, timing the process so that we'll end up with
the coordinatesin the x, y, and z fields when the clipping process is complete.
37a m
Polygon Clipping m
"ow
HE BE BE
Em 375
~~" Build Your Own Flight Sim in C++ "EE EEE
overriding assignment operator is
that within the polyBase function, we control what
is transferred (and make use of the compiler directives if the hidden surface tests are
being used).
Next, the variable cp will keep track of the current vertex of the clipped polygon—
that is, the next one to be added as we replace the vertices of the original polygon:
int cp = 0; // Index to current vertex of clipped polygon
A pointer called point to the vertices of the clipped polygon:
pcv is created to
Initially, v2 will point to the first vertex of the polygon. Thus, the first edge to be
clipped will be the edge extending from the last vertex in the vertex array to the first
vertex in the vertex array. We then set the pointer pv1 to point to
the first vertex of the
current edge and the pointer pv2 to point to the second vertex:
vertex_type *pvi=polygon->GetVerticePtr(v1);
vertex_type *pv2=polygon->GetVerticePtr(v2);
Type 1 Edge
Then we examine the current edge to see what type it is. If the z coordinates of both
the first vertex and the second vertex are greater than zmin, the entire edge is inside
the front side of the view volume:
376 m
Polygon Clipping
"ew.
if ((pvl->az >= zmin) && (pv2->az >= zmin)) {
the clipped polygon. (We'll pick up the first vertex when we process the last edge,
since the first vertex of this edge is the second vertex of that edge.) The polygon
counter is incremented so that the next element will be written to:
pcvlcpl.x = pv2->ax;
pcvlcpl.y = pv2->ay;
pcvlcp++l.z = pv2->az;
}
Type 2 Edge
If the z coordinates ofboth vertices are less than zmin, the entire edge lies outside the
front side of the view volume, in which case we don't need to do anything at all:
The code with comments stays in the program to show that we considered the type 2
edge in implementing the zclip routine.
Type 3 Edge
If the z coordinate of the first vertex is greater than zmin and the z coordinate of the
second vertex is less than zmin, then the edge is leaving the view volume:
Once again, the current polygon index into the vertex array is incremented.
HE BE BE
EB 377
a Build Your Own Flight Sim in C++
Type 4 Edge
If the z coordinate of the first vertex is less than zmin and the z coordinate of the sec-
ond vertex is greater than zmin, the edge is entering the view volume:
pcvlcpl.x pv2->ax;
=
pcvlcpl.y pv2->ay;
=
pcxLep++l.z = pv2->az;
>
}
vi=v2; // Advance to next vertex
Thats it! We've clipped the vertex against the front of the view volume. Now, we'll
need to set the value of the number_of_vertices field in the polyBase descriptor to the
number of vertices in our clipped polygon. Since we've been incrementing cp for every
new vertex, we'll simply use that value:
// Put number of vertices in clipped polygon class:
clip_array.SetVertexCount(cp);
}
378 =m
Polygon Clipping m "ow
® Listing 11-1 The ZClip() function
void View::ZClip(Polygon *polygon, int zmin)
{
vertex_type *pvi=polygon->GetVerticePtr(v1);
vertex_type *pv2=polygon->GetVerticePtr(v2);
// Categorize edges by type:
if ((pvl=>az >= zmin) && (pv2->az >= zmin)) {
HE BE BE BEB 379
Bid Your Own Flight Sim in C++
}
vl=v2; // Advance to next vertex
ymax are all private members declared in the View class as ints. These are used for
boundary comparison just as zmin was used within the ZClip() function. the x coor- If
"ns
dinates of the first and second vertices of the edge are both greater than xmin, the
polygon edge is entirely inside the left side of the window, so we pass the vertices
through unchanged.
// Categorize edges by type:
if ((pcvlv1l.x >= xmin) && (pcvLv2l.x >= xmin))
// Edge isn't off left side of viewport
pcvlcpl.x1 = pcvlv2l.x;
pcvLcp++l.y1 = pcviv2l.y;
}
Note that we are now using the x1 and yI fields of the clipped polygon array, so that
we won't obliterate the values in the x and y fields that we set in the ZClip() function.
If both x coordinates are less than xmin, the polygon is entirely off the left side of
the window, so we do nothing (but record the effort within comments):
//else if ((pcvLv1l.x < xmin) && (pcvLv2l.x < xmin)){
// Edge is entirely off left side of viewport,
// so don't do anything
/1Y
But if the first x coordinate is less than xmin and the second is greater, the edge is
entering the left side of the window, so we use the point-slope equation as before to
find the coordinates of the first vertex and pass through the second coordinates of the
edge for the second vertex:
if ((pcvlv1l.x < xmin) && (pcvLv2l.x >= xmin)) {
float m=(float)(pcvLv2l.y=-pcvlv1l.y) /
(float) (pcvlv2l.x-pcviv11.x);
pcvlcepl.x1 = xmin;
pcvicp++l.y1 =
pcvivil.y + m * (xmin - pcviv1l.x);
pcvlecpl.x1 pcviv2l.x;
=
pcvlcp++l.y1 = pcviv2l.y;
}
And we
tie up loose ends as before:
vi=v2;
}
clip_array.SetVertexCount(cp);
pcvlepl.x = pevlv2l.x1;
pcvlcp++l.y = pcviv2l.y1;
}
}
pcvivll.yl + m * (xmax - pcvLv11.x1);
if ((pcvLv1l.x1 > xmax) && (pcvLv2l.x1 <= xmax)) {
vi=v2;
}
clip_array.SetVertexCount(cp);
Clipping against the upper edge is the same as clipping against the left edge, except
that ymin is used instead of xmin and the x coordinate instead of the y is calculated
with the point-slope equation. Similarly, clipping against the lower edge of the win-
dow is a y-coordinate version of clipping against the right edge of the window. Listing
11-2 shows the complete text of XYClip().
pevlepl. x1 = peviv2l.x;
pcvicp++l.y1l = pcviv2l.y;
>
pevlcepl.x = pcviv2l.x1;
a2
Polygon Clipping
CLELE
pcvicp++l.y = pcvLv2l.y1;
}
vi=v2;
}
clip_array.SetVertexCount(cp);
// Clip against upper edge of viewport:
//else if
//
//
lr}
Edge
so
if ((pcvLlv1l.y
((pcvLv1l.y
is entirely off top
don't do
>=
anything
ymin)
<
i
&&
iti —
of viewport,
timp
<
.
ymin)){
" nn
BB
floatm=(float)(pcvLv2l.y-pcvLv1l.y)/(float)temp;
pcvlcpl.x1 =
pcviv1l.x + (ymin = pcvLvil.y) / m;
}
else pcvlcpl.x1 = pcviv1l.x;
pcvicp++l.y1 = ymin;
}
vi=v2;
}
clip_array.SetVertexCount(cp);
// Clip against lower edge of viewport:
// Initialize pointer to last vertex:
vi=cp-1;
cp = 0;
.
// if ((pcvLv1l.y1 > ymax) && (pcvL[v2l.y1 > ymax)){
// Edge is entirely off bottom of viewport,
// so don't do anything
//%}
if ((pcvlv1l.y1 <= ymax) && (pcvLv2l.y1 > ymax))
// Edge is leaving viewport
temp=Cint)(pcvlv2l.x1=-pcvlv1l.x1);
if (temp!=0) {
float m=(float)(pcvLlv2l.y1=-pcvLlv1l.y1)/(float)temp;
pcvlcpl.x =
pcvCv1l.x1 + (ymax = pcvLv1l.y1) / m;
}
else pcvlcpl.x = pcviv1l.x1;
pcvLlcp++l.y = ymax;
}
vi=ve;
}
clip_array.SetVertexCount(cp);
Now that the clipping functions have been introduced, we can show you the
DrawPolygonList() function, which is in Listing 11-3. There is one parameter and that
is the pointer to the buffer where the polygons will be drawn. Recall that the variables
polygon_list and clip_array are private members of the View class.
387
EER
|uid
— " EE
Your Own Flight Sim in C++
The DrawPolygonList() function calls a series of functions for each polygon in the list.
Since the polygons themselves are clipped and stored into the clip_array, the old
Project() and DrawPoly() functions cannot be used. The old drawing functions worked
with vertex_type stuctures and we need to work with clip_type structures. At the end
of the last chapter, I said we would create a draw function for the PolygonList class. I
lied. The old functions worked with vertex_type stuctures and we need to work with
clip_type structures. As you can see from the commented line listing above, however,
the polygon list is mapped to an array of clipped polygons. It is the clip_array that will
be responsible for drawing the polygon to the screen.
The ClippedPolygon::Project() function in Listing 11-4 looks almost identical to
the object_type::Project() implementation in Chapter 8. A caveat of the ZClip() func-
tion is that the z coordinate of the vertex has been correctly determined, so it does not
have to be subtracted from the distance when dividing the x and y coordinates.
We now have all the tools we need to put together a complete viewing package,
which will allow us to roam about at will through a world filled with imaginary objects.
In Chapter 13, we'll do just that, as we introduce the view system. But first, lets shed
some light on light and how we can make our world objects even more realistic.
Advanced Polygon
Shading
Up to this point, we have not considered the effect of light on the objects that we
draw. In the real world, the appearance of a surface depends not only on its color, but
also on the waylight shines on it. A great artist knows how to highlight objects in a
painting so that they appear to be realistically lit. By simulating the effects of light in
our programs, we can make the computer produce this effect automatically, so we don't
need to become master artists.
To add realistic lighting effects to our flight simulator, we need a basic understand-
ing of the physics of light and a way to translate that understanding into practical code.
This code will simulate a model that we derive for handling light. A model is just a set
of rules that describes how we want lighting to work in our software. Lighting models
can be very simple, if we want fast-running code that is easy to write, or very complex,
if we want the most realistic possible effects.
Light is a complicated and sometimes subtle phenomenon with many effects which
are difficult to simulate on a computer. Computer graphics researchers have come up
with so many ways to reproduce the effects of light that a whole series of books could
be written on the subject. Fortunately for us, we can produce compelling lighting
effects and substantially improve the game-playing experience by applying a few of the
simpler techniques. In other words, we will derive a simple but effective lighting
model.
393
Build Your Own Flight Sim in C++
mm BH BE
NB
Color and lllumination
Before we jump into programming our lighting effects, we'll cover some of the physics
light. particular, we will be looking for ways to simplify our lighting model while
of In
sacrificing as little realism as we can. First, we'll examine the issue of color.
394 =m
Advanced Polygon Shading
m "wn
N~———
source is a lot easier to handle on a computer because we only have to calculate a level
of illumination for all the pixels of a polygon. If we were trying to simulate a lightbulb,
we would have to perform illumination calculations for each pixel in each polygon. In
our model, every pixel in a given polygon will have the same brightness. Figure 12-2
illustrates the difference between the lightbulb model and the sun model. The bulb in
Figure 12-2(a) shows rays of lights emanating outward from the bulb and striking a flat
surface at different angles. The surface tends to be darker towards the sides, which are
further away from the bulb and have more of an angle between the ray and the surface.
The sun however, in Figure 12-2(b), lights the surface with the same angle and pro-
duces a consistent lighting across the entire surface.
When we decide that the rays of light from our light source will all be parallel, we
don’t need to consider the location of the light source. We only need to consider the
direction the light is traveling. This reduces the description of the light source to a sin-
gle vector, and will make our lighting code simpler. But have we made our lighting
model too simple?
One thing we have left out of our lighting model is ambient light. When the sun
casts shadows in a real scene, the objects in the shadows are still illuminated to some
degree. This is because shadows are indirectly lit by light reflecting off of objects that
are in the sunlight. To accurately simulate ambient light, we would have to simulate all
Advanced Polygon Shading
"Ew
the reflections that illuminate a particular point. Fortunately, it is just as effective to
assume that ambient light illuminates all shaded surfaces at some fixed intensity. This
intensity can be adjusted to suit our tastes.
In our code, implementing ambient light simply requires that we define a minimum
brightness level for all polygons. A high minimum brightness gives us a lot of ambient
light and a subtle shading effect. A low minimum brightness gives us just a little ambi-
ent light with profound contrast between light and dark surfaces.
HE BE BE BE Bm
397
—
~~ Build Your Own Flight Sim in C++
mH mE
EEE
Keeping It Simple
Now we have a complete lighting model. We will be implementing two kinds of light-
|
.
ing: ambient light and diffuse reflection. The ambient light will take the form of a min-
imum brightness level that will apply to all surfaces. The illumination of a surface due
to diffuse reflection will be determined by the angle between the surface and an inci-
dent vector which tells us the direction in which light is moving from our light source,
the sun. For a review of vectors, see Chapter 2.
With this model in mind, we can proceed to implement our lighting scheme.
int World::LoadSource()
{
Only four numbers are required to define the light source. To read them from the
data file, the following lines must be added somewhere in the loading procedure:
source.x=_pf.getnumber();
source.y=_pf.getnumber();
source.z=_pf.getnumber();
source.ambient=_pf.getnumber();
To refresh our memories, LOADPOLY.CPP contains a static local object, _pf. This
object has a function getnumber() which returns the next integer in the file stream. All
that remains to provide complete light-source information is the calculation of the
structures length field. This is done by following line which is done immediately after
loading the source numbers:
source. length=(long)sqrt((float)source.x*source.x+(float)source.y*source.y+
(float)source.z*source.z);
}
This is a simple distance formula, the square root of the sum of the squares. Our com-
pleted function appears in Listing 12-1.
source.length=(long)sqrt((float)source.x*source.x+(float)source.y*source.y+
(float)source.z*source.z);
return 0;
}
A Graduated Palette
One problem posed by shading our polygons is the number of different colors
required. The VGA hardware doesn’t allow us to
set the brightness of each pixel indi-
vidually, and the number of possible shades is limited. Out of the 262,144 colors in the
color space, we can display only 256 colors at any one time. This means that we need
some way to fit as many of the shades we need as possible into the palette. Rather than
figure out how to do this by hand, we can have the computer do it for us. This section
presents the structures and functions required to manage the palette. In doing so we
will create a Palette class.
The first structure we must define is rgh_type. This structure will contain the red,
blue, and green constituents of one of the 256 entries in the palette.
struct rgb_type {
// Structure for RGBcolor definition
unsigned char red; // Red intensity (0 to 63)
unsigned char green; // Green intensity (0 to 63)
unsigned char blue; // Blue intensity (0 to 63)
};
Next we define a class Palette which contains the information we will need to gen-
erate the VGA palette and use it in color shading. The class contains two lists, one for
unshaded colors and one for shaded colors. The unshaded colors will be used for
things that don't need lighting effects, like the instrument panel in the cockpit and any
text we might want to display on the screen. We will use the shaded colors to render
the world outside the cockpit windows.
In addition to the two lists of colors, the structure includes a field which indicates
the number of shades that are available for each shaded color. After palette entries are
assigned to unshaded colors, the rest of the entries are evenly divided between the
shaded colors. The number_of_shades field lets us know how many shades we could
fit into the palette.
As an example, consider
a
situation in which we have eight unshaded colors and six
shaded colors. The unshaded colors use up eight palette entries leaving 248 for the
unshaded colors. The 248 remaining entries must be divided up evenly between the six
shaded colors, allowing us 41 shades for each shaded color. This uses up all but two of
the palette entries.
All of the functions in the Palette class are public for external access, several of
which are defined inline to return the array counts and pointers to their elements. A
constructor and destructor, plus Load(), Install(), and ColorIndex() functions round
out the class definition:
400 =
Advanced Polygon Shading CI
A
class Palette {
private:
int number_of_unshaded_colors; // Number of colors with no brightness
rgb_type *unshaded_color; // List of unshaded colors
int number_of_shaded_colors; // Number of colors with brightness
rgb_type *shaded_color; // List of shaded colors
int number_of_shades; // Number of brightness levels
public:
// constructor
Palette():unshaded_color(0), shaded_color(0){;}
// destructor
“Palette();
int Load();
// adjust for ambient light:
void Install(float ambient);
// retrieve information:
int GetUnshadedCount()
{ return number_of_unshaded_colors; 1}
rgb_type *GetShadedPtr(int 1)
{ return &shaded_colorCil; }
// compute the index into shaded colors:
int ColorIndex(int color,int lLevel,int ambient);
Y;;
The constructor assigns a zero to both of the rgb_type arrays. The purpose is to guar-
antee deletion of the arrays when the destructor is called. This methodology should
look somewhat familiar:
Palette:: "Palette()
{
if(unshaded_color)
delete [1 unshaded_color;
if(shaded_color)
delete [1 shaded_color;
}
We will read the palette information from the data file. Both lists of colors
must be
specified, but the number of shades can be determined by the program after the data is
loaded. Loading the colors requires an additional function to the LOADPOLY.CPP file,
which is shown in Listing 12-2.
401
Build Your Own Flight Sim in C++
HE BE BE
EBB
continued from previous page
unshaded_colorCcolornuml.red =_pf.getnumber();
unshaded_colorCLcolornuml.green=_pf.getnumber();
unshaded_colorLcolornuml.blue =_pf.getnumber();
}
number_of_shaded_colors=_pf.getnumber();
shaded_color=(rgb_type *) new rgb_typelnumber_of_shaded_colors];
if(!shaded_color)
return -1;
for (colornum=0; colornum<number_of_shaded_colors; colornum++) {
shaded_colorLcolornuml.red =_pf.getnumber();
shaded_colorLcolornuml.green=_pf.getnumber();
shaded_colorLcolornuml.blue =_pf.getnumber();
}
return 0;
}
402 =
Advanced Polygon Shading
"ew
number_of_objects = _pf.getnumber();
obj=new ObjectCnumber_of_objectsl;
ifC !
obj)
return(-=1);
for (int objnum=0; objnum< number_of_objects; objnum++) {
ifC objLobjnuml.Load() )
returnC =1 );
}
return(polycount);
The shading calculations work by calculating the brightness for each shade and then
multiplying the red, green, and blue components of each shaded color by that bright-
ness. This is only done once when the program starts, so it has no impact on the flying
HE BE BE
BEN 403
EE
. "EEE
~
Build Your Own Flight Sim in C++
performance of the program. The brightness values are adjusted so that the lowest
value generated corresponds to the ambient light level and the highest value is one,
meaning 100 percent. The complete function follows in
Listing 12-4.
¥
Listing 12-4 The Palette::Install() function
void Palette::Install(float ambient)
{
rgb_type allcolors[2561;
// Record the number of shades and convert ambient from percentage
number_of_shades=(256-number_of_unshaded_colors)/number_of_shaded_colors;
ambient=ambient/100;
// Copy unshaded colors to the palette array
memcpy (&allcolorsC0]1,unshaded_color,number_of_unshaded_colors*sizeof(rgb_type));
// start filling in the palette at the first available entry:
int index=number_of_unshaded_colors;
for (int shade=0; shade<number_of_shades; shade++) {
float brightness=(shade*(1-ambient))/(number_of_shades-1)+ambient;
for (int color=0; color<number_of_shaded_colors; color++) {
allcolorsCindex].red=(unsigned char)
(shaded_colorLcolorl.red*brightness);
allcolorsCindex].green=(unsigned char)
(shaded_colorLcolorl.green*brightness);
allcolorsCindex++].blue=(unsigned char)
(shaded_colorLcolorl.blue*brightness);
>
}
All that remains to be done to make our graduated palette work is to create a function
which accepts a color number and a brightness level and returns the number of the
palette entry which contains the appropriate shade. When we are shading polygons, we
will know the original color of the polygon (our input file tells us this), and we will know
what brightness level we want based on our lighting calculations. This is the meaning
of the two color fields within the polyBase class. The original_color always contains
the color read in from the file, while the color field contains the adjusted lighting
value. The function Palette::ColorIndex() will tell us which palette entry to use for a
particular original color and a particular brightness. Take a look at Listing 12-5 for the
complete function.
404 ®
Advanced Polygon Shading
"aw
Level=100;
else if (level<ambient) // Level should not be lower than ambient
Level=ambient;
int shade=((level-ambient)*(number_of_shades=1))/(100-ambient);
return color+shade*number_of_shaded_colors;
>
The ColorIndex() function takes three parameters. The first is color, which specifies the
base hue of the palette. The second and third parameters, level and ambient, determine
the shading of that color. ColorIndex() accepts the brightness level as a percentage. A
level of 100 percent indicates that the color should have the RGB values originally
specified in the data file. A lower percentage indicates that the returned color should
have RGB values which are in that ratio to the original values. The function limits the
level to a range from ambient level to 100 percent.
What's Normal?
Now that we have established the light source and the palette, we can put together the
code that determines how polygons are to be shaded. In principle, the calculation of
the illumination level for a polygon is simple. The illumination level of a polygon that
is facing the light source is proportional to the cosine of the angle between the incom-
ing light and a vector which is normal to the plane of the polygon, as shown in Figure
12-3. A normal vector is one which is perpendicular to the plane.
The light source structure we defined earlier provides us with a direction vector for
the light source. What we need is a way to generate a normal vector for our polygon
and a way to find the cosine of the angle between the two vectors. As it turns out, find-
ing the cosine is the easy part. By taking the dot product of two vectors and dividing it
HE BE BE BE B=
405
EEE
= Build Your Own Flight Sim in C++
"EE
by their lengths, we get the cosine without having to use the time-consuming cos()
function. The dot product involves three multiplications and two additions, so it is very
fast. The lengths are another matter. Finding the length of a vector requires the use of
the C run-time library function sqrt(), which is very slow. Fortunately, we can calculate
all the lengths once as part of the program initialization and store this value as part of
our polygon object.
Once we decide on the position of the sun, the light-source incident vector will
never change in our program, so we calculate its length and store it in the light-source
structure as discussed above. On the other hand, the polygons that make up our
objects may be rotating as the program runs, and if so, their normal vectors will
change. But rotating a polygon only changes the direction of the normal vector, not its
length. This means that we can calculate the lengths of the polygon normals once and
use the same values over and over again as the polygons rotate.
is
All that remains to create a normal vector for each polygon. This is done by cre-
ating two vectors corresponding to two edges of the polygon and taking their cross
product. The cross product of two vectors is always perpendicular to both vectors, so
the cross product of two edge vectors of a polygon will be perpendicular to the poly-
gon, as shown in Figure 12-4.
The edge vectors are generated by subtracting vertices from each other as follows
long ax=v0->lx-v1->lx; // Vector for edge from vertex 1 to vertex 0
Long ay=v0->ly-v1=->ly;
long az=v0->lz-v1=->lz;
long bx=v2->Lx-v1->lx; // Vector for edge from vertex 1
to vertex 2
long by=v2->ly-vi1->ly;
long bz=v2->lz-v1->lz;
406 ®m
Advanced Polygon Shading m
"a.
The cross product is a matrix operation on the two vectors which looks like this:
float nx=ay*bz-az*by; // Normal is cross product of the two vectors
float ny=az*bx-ax*bz;
float nz=ax*by-ay*bx;
Now we are ready to write the functions that do the work.
HE BE BE
EB 407
uid Your Own Flight Sim in C++
vertex_type *vO0=pptr->vertex[0];
vertex_type *vi=pptr->vertex[1];
vertex_type *v2=pptr->vertex[2];
Long ax=v0->lx-v1->Llx; // Vector for edge from vertex 1
// to vertex 0
long ay=v0->ly-v1->ly;
long az=v0->lz-v1->lz;
long bx=v2->lx-v1->lx; // Vector for edge from vertex 1
// to vertex 2
Long by=v2->ly-vi->ly;
long bz=v2->lz-v1->lz;
float nx=ay*bz-az*by; // Normal is cross product of the
//
two vectors
float ny=az*bx-ax*bz;
float nz=ax*by-ay*bx;
pptr=>nlength=(long)sqrt(nx*nx+ny*ny+nz*nz);
The normal is generated from the local coordinates of the vertices (Ix, ly, and Iz)
because these are the coordinates read in from the file. When MeasureNormals() is
called, we haven't performed any transformations on the polygons. This is acceptable
unless we intend to scale the polygons. The sole parameter, numshades, is only used to
determine if the normal length needs to be computed—if the polygons color has been
defined as an unshaded color, there is no need to compute the normal length. Since the
function is called once after the object descriptor file is read, you could leave this test
out with no impact on the programs flying speed. On the other hand, we might want
to change a future program to load objects while flying. In this case, the stitch in time
by skipping needless calculations for unshaded polygons could be worthwhile.
The function World::CalcColors() determines the appropriate shade for each poly-
gon in every object. The entire function consists of a loop which calls CalcObject-
Color() for all of the objects in the world.
void World::CalcColors()
{
CalcObjectColor() is another simple looping function. This time the loop calls
CalcPolygonPtr() for each of the polygons within the Object. You may be thinking that
this is a lot of stack overhead to call these functions, and we could optimize these calls
408 ®
Advanced Polygon Shading
"Ew |
CalcPolygonPtr(object->GetPolygonPtr(p));
}
int brightness=(int)(((nx*source.x+ny*source.y+nz*source.z)*(100-ambient)) /
(source. length*pptr->GetNormalLen()));
Finally, ambient lighting is applied, and ColorIndex() is called to find the appropri-
ate palette index. The resulting color value is placed in the color field of the polygon,
where it will be used by DrawPoly() to draw the polygon.
if (brightness<0)
brightness=ambient; // Ambient applies to polygons not facing the
// source
else
brightness+=ambient; // Add ambient for polygons facing the source
pptr->SetColor(palette.ColorIndex(temp,brightness,ambient));
There was a strong inclination to make this function part of the Polygon class rather
than the World class since it mimics the MeasureNormals() function so closely. Several
reasons why we don’t do that are the Polygon and Object module would need to
- ~~ Build Your Own Flight Sim in C++
include the source_type structure. Our object characteristics have been designed to
keep the intricacies of lighting out of the object and polygon classes. Our completed
CalcPolygonColor() can be seen in Listing 12-7.
|
vertex_type *vO=polyptr->GetVerticePtr(0);
vertex_type *vi=polyptr->GetVerticePtr(1);
vertex_type *v2=polyptr->GetVerticePtr(2);
long ax=v0->wx-v1->wx; // Vector for edge from vertex 1
to vertex 0
long ay=v0->wy-v1->wy;
long az=v0->wz-v1->wz;
long bx=v2->wx-v1->wx; // Vector for edge from vertex 1 to vertex 2
Long by=v2->wy-vi1->wy;
long bz=v2->wz-v1->wz;
long nx=ay*bz-az*by; // Normal is cross product of the two vectors
Long ny=az*bx—-ax*bz;
long nz=ax*by-ay*bx;
int brightness=(int) (((nx*source.x+ny*source.y+nz*source.z)*(100-
ambient))/
(source. length*polyptr->GetNormalLen()));
if (brightness<0)
brightness=ambient; // Ambient applies to polygons not facing the
// source
else
brightness+=ambient; //
Add ambient for polygons facing the source
polyptr=>SetColor(palette.ColorIndex(temp,brightness,ambient));
}
}
void World::CalcObjectColor(Object *object)
{
void World::CalcColors()
{
410 =
Advanced Polygon Shading
"Ew
CalcObjectColor(&objLol);
}
}
With these software tools in place, we are ready to complete the demonstration
program
The new main() is almost unchanged from the polygon demo of Chapter 8. All that has
been added are calls to our three initialization functions, InstallSource(), InstallPalette(),
and MeasureNormals(), and a call to CalcColors() in the main loop after the transfor-
mations have been completed, as shown in Listing 12-8.
HE BE BE BE
= 411
.
~~
CoBuild Your Own Flight Sim in C++
"EE EEE
RW Listing 12-8 LIGHTDEM.CPP
#include <dos.h>
#include <conio.h>
#include <iostream.h>
#include <stdlib.h>
#include "world.h"
#include '"screen.h"
cls(screen_buffer);
inittrans(); // Initialize translations
scale(scalefactor); // Create scaling matrix
rotate(xangle,yangle,zangle); // Create rotation matrices
412 ®
Advanced Polygon Shading
"EE
yangle+=yrot;
zangle+=zrot;
// Check for 256 degree wrap around:
if (xangle>255) xangle=0;
if (xangle<0) xangle=255;
if (yangle>255) yangle=0;
if (yangle<0) yangle=255;
if (zangle>255) zangle=0;
if (zangle<0) zangle=255;
translate(0,0,zdistance); // Create translation matrix
// Call the Draw world Loop
world.Draw(curobj,screen_buffer,XORIGIN,YORIGIN);
// Put the viewport out to the video display:
putwindow(0,0,SCREEN_WIDTH,SCREEN_HEIGHT,FP_OFF(screen_buffer),
FP_SEG(screen_buffer));
// Watch for user input:
if (kbhit()) { // If input received....
key=getch(); // Read the key code
switch(key) {
case '\r':
// ENTER: Go to next object
curobj++;
if (curobj>=world.GetObjectCount())
curobj=0;
break;
case '7':
// "7": Speed up x rotation
Xrot++;
break;
case '4':
// "4": Stop x rotation
xrot=0;
break;
case '1':
// "1": Slow down x rotation
-=xrot;
break;
CI
case '8':
continued on next page
413
Build Your Own Flight Sim in C++
case '5':
// "5": Stop y rotation
yrot=0;
break;
case '2':
// "2": Slow down y rotation
-=-yrot;
break;
case '9':
// "9": Speed up z rotation
zrot++;
break;
case '6':
// "6": Stop z rotation
zrot=0;
break;
case '3':
// "3": Slow down z rotation
==2rot;
break;
case '0':
// "0": Shut off all rotation
zrot = xrot = yrot = 0;
zangle = xangle = yangle = 0;
break;
case '+':
// "+": Increase distance
414 =
Advanced Polygon Shading m
"a.
zdistance+=30;
break;
case '-':
// "=": Decrease distance
if (zdistance>530) zdistance-=30;
break;
}
}
}
setgmode (oldmode)
if( screen_buffer) ; // Reset previous video mode & end
delete [1 screen_buffer;
}
Gouraud Shading
When a rendering system like ours uses only polygons, curved surfaces can only be
approximated using a large number of polygons. For example, the file ICOS.WLD
defines an icosahedron, a 20-sided solid that approximates a sphere. When lighting
effects such as those in our previous demonstration program are applied to the icosa-
hedron, its facets, or planes, are easily visible. To see this effect, try running LIGHT-
DEM using ICOS.WLD as the input file.
Gouraud (pronounced “ger-ROW”) shading is a technique developed by H.
Gouraud in the early 19705 to smooth the facets of a surface such as an icosahedron. It
it
doesn't attempt to render the surface accurately, just tries to smooth the transitions
from one polygon to another. Gouraud shading is sometimes called intensity interpo-
lation since it uses a ratio to interpolate a color’ brightness and saturation.
More simply put, Gouraud shading adjusts pixel brightness based on the normals of
the vertices rather than the normals of the polygons. Pixels between the vertices are
illuminated in such a way that the brightness changes gradually from one vertex to
another. The transitions from one polygon to another become less obvious so that if
enough polygons are used in the surface, it will appear to be smooth. Gouraud shading
is a trick, and it’s only partly effective. The silhouette of a faceted surface rendered by
Gouraud shading still shows the facets, and there must be many facets (polygons) to
produce a convincingly smooth surface.
HE BE BE BE Bm
415
Build Your Own Flight Sim in C++
- mE EEE
In order to conserve processing power, the scenes used in our flight simulator con-
tain very few polygons, and no attempt is made to approximate smooth surfaces. For
this reason and because Gouraud shading slows things down, we will not be using it in
the flight simulator. But we will create a demonstration program which uses Gouraud
shading in orderto explore the technique. Since this demonstration program departs
from the progress in building our flight-simulator classes, any file with changes for
Gouraud lighting effects will be renamed. Thus, the World class file, WORLD.CPP, is
renamed to WORLD_G.CPP for the Gouraud demo program. All of these files can be
found in the GOURAUD directory.
Bresenham Revealed
Most of the changes required to get the lighting demonstration program to do Gouraud
shading are in the DrawPoly() function. Unfortunately, in its current condition, Draw-
Poly() is very complicated and therefore difficult to change. A good first step toward
Gouraud shading is to simplify this function.
The DrawPoly() function is based on Bresenham’ line-drawing algorithm presented
in Chapter 6. A polygon is drawn by drawing the lines that make up its edges and fill-
ing in the pixels in between. Basically, DrawPoly() consists of two Bresenham line-
drawing functions which operate simultaneously and little code to fill in the pixels in
between.
Bresenham’s algorithm provides the advantage of drawing a line without the use of
floating-point numbers. In Chapter 6, we weren't all that interested in how it accom-
plishes this. But to simplify polygon drawing, we need to understand the principle
behind the algorithm so we can use the principle directly and leave the line drawing
behind. After all, drawing lines is not what DrawPoly() really needs to do.
The first
step in Bresenham’ algorithm is to determine whether the line will be mov-
ing more rapidly in the horizontal direction or the vertical direction. If a line changes
more rapidly in the horizontal direction for example, we can step from one endpoint to
the other by incrementing the x coordinate each time through the loop and changing
the y coordinate as required. The y coordinate will have to be changed a maximum of
1 pixel
per iteration.
But how do we determine when the y coordinate must change? This is the job of the
mysterious variable error_term. error_term is manipulated using two other values: xdiff,
the difference between the x coordinates of the endpoints, and ydiff, the difference
between the y coordinates of the endpoints. In the example of a line that is more hor-
izontal than vertical, we add ydiff to error_term each time through the loop. When we
notice error_term is equal to or greater than xdiff, we know it’s time to change our y
coordinate and subtract xdiff from error_term. Why does this work?
It works because error_term is actually the numerator, or top part, of a fraction, and
the denominator, or bottom part, of that fraction is xdiff. In fact, error_term and xdiff are
416 m
Advanced Polygon Shading m
LL
part of a mixed fraction in which the integer part is the y coordinate of the pixel we are
drawing. In the code presented in Chapter 6, the y coordinate is
hidden inside of offset,
which contains both the x and y coordinates in the form of a pointer into the screen
memory.
So Bresenham’ algorithm avoids using floating point numbers by using a mixed
fraction instead. We add ydiff to error_term each time through the loop because we
want to add ydiff/xdiff to the mixed fraction. We check to see whether error_term is
to
equal or greater than xdiff because that tells us whether we can move a value of 1
(ediff/xdiff) from the fractional part of the mixed fraction to the integer part. Under-
standing this allows us to apply the same technique to polygon drawing without using
all of Bresenham?’s algorithm.
Bresenham algorithm for lines determines the slope of the line it is drawing so that
the increment applied to the mixed fraction is never greater than 1. In this way, only
the numerator is incremented, and the integer partis only incremented if the fraction
overflows (i.e., the numerator becomes larger than the denominator.) This is important
in line drawing in orderto avoid gaps in the line. Any increment greater than 1 would
produce a discontinuous line.
When we draw a polygon, we always proceed from top to bottom regardless of the
slopes of the edges. The outer loop iterates through the y coordinates starting at the top
of the polygon and ending at the bottom. The inner loop iterates through the x coor-
dinates from left to right. Our mixed fractions will be used to determine the starting
and ending points of the inner loop. The starting and ending points for the x coordi-
nates may change by more than 1 pixel, and that’s okay. The entire row of pixels is
filled in from left to right, so there are no gaps.
to
In order be able to increment the starting and ending points by more than one,
we must increment both the numerators and the integer parts of the mixed fractions.
As before, we must check the fractions for overflow and increment the integers
accordingly.
In the DrawPoly() function below, some additional variables have been added to
handle the mixed fractions. errorincrl and errorincr2 are the increments for the numer-
ators (error terms), and intincrl and intincr2 are the increments for the integer parts. As
before, the integer parts of the starting and ending points are contained within the off-
set] and offset2 variables, and the numerators are errorterml and errorterm2. The vari-
ables calcl and calc2 are introduced to control when the increments and other
edge-related variables are recalculated. Both of these variables act as flags and are both
initialized to one (1). When the top of the loop inspects these flags and finds them to
it
be nonzero, will calculate the number of rows and the mixed fraction variables, then
set the flag to zero (0). When an edge is finished processing (and drawing), the calcl
and calc2 variables are again set to 1 in order to initialize the variables which control
the incremental drawing of the edge. Here's our first stab at a new version of Draw-
Poly():
HE BE BE
EB a17
TT —3 ®H HH EH EH
BB
-
~~" Build Your Overt Fi ht Sim in C++
8g
int ambient)
{
418 =
errorterm1=0; // Initialize error term
if ((ydiffl=yend1-ystart1)<0) ydiffl=—ydiff1; // Get abs value of y length
count1=ydiff1; // Record number of rows
if C(count1) { // Any rows at all?
xdiffl=xend1-xstart1; // Get x length
intincr1=xdiff1/ydiff1; // Get integer part of increment
if (xdiff1<0) {
xunitl=-1; // Calculate x overflow increment
errorincr1=-xdiff1%ydiff1;// Get numerator part of increment
}
else {
xunit1=1; // Calculate x overflow increment
}
errorincr1=xdiff1%ydiff1; // Get numerator part of increment
}
}
Length++;
int L=length; //
Draw a Line of pixels
for (unsigned char far *p=&screen_bufferlstartl;l-—;)
*p++=color;
offset1+=intincr1+320; // Increment offset for x and y
errorterml+=errorincri; // Add increment numerator to error term
if (errorterm1>=ydiff1) { // Has fraction overflowed?
errorterml-=ydiff1; // Yes, subtract 1 from fraction
continued on next page
Wow Bm B® ow
419
2 . ~~ Build Your Own Flight Sim in C++
}
offset1+=xunit1; // Add 1 Cor -1) to integer part
*p++=color;
This is important, because to do Gouraud shading, we will want to change the colors
which are used to draw the pixels. This is easier when there is only one place in the
code where pixels are drawn.
Bresenham on Brightness
The last major change to DrawPoly() adjusts the brightness of each pixel according to
the brightness of the vertices. Gouraud shading is all about making smooth transitions
420 =
Advanced Polygon Shading m
from one brightness level to another, so we must write the code that makes these
transitions. Asit turns out, we have already written this code.
wm
m
Ne
The mixed fraction code used in our new version of DrawPoly() is exactly what we
need to calculate pixel brightnesses. In our new DrawPoly(), the mixed fractions
allowed us to move smoothly from one endpoint x coordinate to another along an
edge. Now we will use the same method to move smoothly from one endpoint bright-
ness to another along an edge and horizontally between two edges.
The first change to DrawPoly() is to add the new variables required to track the
brightness levels along the edges. The variable names are much like those used previ-
ously in DrawPoly() for mixed fractions. Two sets of variables are defined, one for each
of the two edges currently being drawn.
int cdiff1,cdiff2, // Difference between starting and ending
// brightness for edges
cstart1,cstart2, // Starting edge brightnesses
cend1,cend2, // Ending edge brightnesses
cerrorterml,cerrorterm2,// Error terms for brightnesses for edges 1 & 2
cunitl,cunit2, // Unit to advance brightness for edges 1 & 2
cintincrl,cintincr2, // Standard integer brightness increments on
// edges 2 1 &
Next comes the variables required to make the brightness transition horizontally from
one edge to the other. Again, the same naming scheme is used.
int cdiff, // Difference between starting and ending
// brightness for row
cstart, // Starting row brightness
cend, // Ending row brightness
cerrorterm, // Error term for row brightness
cunit, // Unit to advance brightness fpor row
cintincr, // Standard integer color increments for row
cerrorincr; // Error color increments for row
When the mixed fraction values are calculated for determining the x coordinates of the
edges, we must also perform similar calculations for the brightness mixed fractions.
This must be done for both edges, but we will look at just one edge here.
cstartl=vertex[startvert1l->brightness; // Starting brightness
cendi=vertexCendvert1l->brightness; // Ending brightness
cerrorterm1=0; // Initialize error term
cdiffi=cend1-cstart1; // Find span of brightness values
cintincri=cdiff1/ydiff1; // Brightness integer part increment
if Cediff1<0) {
HE BE BE BE
= 421
Build Your Own Flight Sim in C++
This code initializes the mixed fraction that tracks the brightness value along the edge
Now we address the problem of making the transition along the horizontal row. The
is
code
tions:
very similar to the code used along the edges. First we set up the mixed frac-
}
cend=cstart2; // End with edge brightness
Length++;
cerrorterm=0; // Initialize error term
cdiff=cend-cstart; // Determine brightness span
cintincr=cdiff/length; // Brightness integer increment
if (cdiff<0) {
cunit=-1; // Brightness overflow increment
cerrorincr=-cdiff%length; // Brightness numerator increment
}
else {
cunit=1; // Brightness overflow increment
}
cerrorincr=cdiff%length; // Brightness numerator increment
This establishes the brightness value mixed fraction (cstart+cerrorterm/xdiff) and the
increment mixed fraction (cintincr+cerrorincr/xdiff). Next, we use these mixed fractions
to determine pixel brightness while drawing the horizontal line of pixels:
int L=length;
for (unsigned char far *p=&screen_bufferlstartl;l--;)
{
*p++= palette->ColorIndex(color,cstart, ambient);
cstart+=cintincr; // Increment brightness integer part
cerrorterm+=cerrorincr; // Increment brightness numerator
if (cerrorterm>=Length) { // If overflowed
cerrorterm-=lLength; // Add
Subtract from the fractional part
1
}
cstart+=cunit; // one to the integer part
}
Now we must add the code which performs the mixed fraction addition for the edges.
It looks strikingly similiar to the code we just wrote:
Advanced Polygon Shading m
CI
cstartl+=cintincri; // Increment brightness integer part
cerrorterml+=cerrorincri; // Increment brightness numerator
if (cerrortermi>=ydiff1) { // If overflowed
cerrorterml-=ydiff1; // Subtract from the fraction part
1
}
cstartl+=cuniti; // Add one to the integer part
Now, we can put our final DrawPoly() together. Listing 12-9 contains the completed
function using the mixed fraction approach for rows and colors.
// Uninitialized variables:
unsigned int start, // Starting offset of Line between edges
offset1, // Offset of current pixel in edges & 2 1
offset2;
int ydiff1,ydiff2, // Difference between starting x and ending x
xdiff1,xdiff2, // Difference between starting y and ending y
cdiff1,cdiff2, // Difference between starting color and ending
// color
Length, //
Distance from edge to edge 21
errorterml,errorterm2, //
Error terms for edges & 2 1
count1,count2, //
Increment count for edges & 2 1
xunitl,xunit2, //
Unit to advance x offset for edges 1 & 2
intincr1,intincr2, // Standard integer increments for x on
// edges & 2 1
HE BE BE
EB 423
~~
—Build Your Own Flight Sim in C++
// Initialize
—
if (calc1) {
calc1=0;
offset1=320*ystart1+xstart1; // Offset of edge 1
xdiffl1=xend1-xstart1;
intincr1=xdiff1/ydiff1;
if (xdiff1<0) { // Get value of length
xuniti=-1; // Calculate X increment
errorincr1==xdiff1%ydiff1;
¥
424 ®m
Advanced Polygon Shading m m =m
else {
xunit1=1;
errorincr1=xdiff1%ydiff1;
}
cstarti=vertex[startvert1l->brightness;
cend1=vertex[Lendvert1l->brightness;
cerrorterm1=0;
cdiffl=cend1-cstart1;
cintincrl=cdiff1/ydiff1;
if (ediff1<0) {
cunit1=-1;
cerrorincri=-cdiffl1%ydiff1;
}
else {
cunit1=1;
cerrorincri=cdiffl1Zydiff1;
}
}
}
if (calc2) {
calc2=0;
offset2=320*ystart2+xstart2; // Offset of edge 2
errorterm2=0;
if ((ydiff2=yend2-ystart2)<0) ydiff2=-ydiff2; // x &
y lengths of
// edges
count2=ydiff2;
if (count2) {
xdiff2=xend2-xstart2;
intincr2=xdiff2/ydiff2;
if (xdiff2<0) { // Get value of length
xunit2=-1; // Calculate X increment
errorincr2=-xdiff2%ydiff2;
>
else {
xunit2=1;
errorincr2=xdiff2%ydiff2;
}
cstart2=vertex[startvert2]l->brightness;
cend2=vertex[endvert2]l->brightness;
cerrorterm2=0;
cdiff2=cend2-cstart2;
cintincr2=cdiff2/ydiff2;
if (cdiff2<0) {
cunit2=-1;
cerrorincr2=-cdiff2%ydiff2;
}
else {
cunit2=1;
cerrorincr2=cdiff2%Zydiff2;
continued on next page
HE BE EH
Em 425
Build Your Own Flight Sim in C++
}
}
cstart=cstarti;
cend=cstart2;
>
Length++;
cerrorterm=0;
cdiff=cend-cstart;
cintincr=cdiff/length;
if (cdiff<0) {
cunit=-1;
cerrorincr=-cdiff’%length;
}
else {
cunit=1;
cerrorincr=cdiff%length;
}
int L=length;
for (unsigned char far *p=&screen_buffer[startl;l--;) {
*p++=palette->ColorIndex(original_color,cstart,ambient);
cstart+=cintincr;
cerrorterm+=cerrorincr;
if (cerrorterm>=length) {
cerrorterm-=Length;
cstart+=cunit;
¥
}
offset1+=intincr1+320;
errorterml+=errorincri;
if (errorterm1>=ydiff1) {
errortermi-=ydiff1;
offset1+=xunit1;
}
cstartl+=cintincri;
cerrorterml+=cerrorincri;
if (cerrorterml>=ydiff1) {
cerrorterml-=ydiff1;
426 =
Advanced Polygon Shading
"NEN
NN
cstartl+=cuniti;
}
offset2+=intincr2+320;
errorterm2+=errorincr2;
if Cerrorterm2>=ydiff2) {
errorterm2-=ydiff2;
offset2+=xunit2;
}
cstart2+=cintincr2;
cerrorterm2+=cerrorincr2;
if (cerrorterm2>=ydiff2) {
cerrorterm2-=ydiff2;
cstart2+=cunit2;
}
}
Vertex Normals
Having simplified DrawPoly(), we are ready to look at the code required to determine
the initial brightness of the vertices. As with normal polygon shading, brightness is
determined by the angle between the incoming light ray and a normal vector. Previ-
ously, the normal vector we wanted was the normal vector of the polygon we were
drawing. For Gouraud shading, we want vectors which are normal to our vertices.
427
Build Your Own Flight Sim in C++
- EH BE BE
BB
Fortunately, figuring out the normal of a vertex is very easy. All we need to do is add
together the normal vectors for all the polygons that contain that vertex. The result is
a compromise between the polygon normals which is normal to their shared vertex.
Figure 12-5 shows a vertex normal and the polygon normals it is generated from.
As we move an object around, its vertex normals move as well. This seems to indi-
cate that we need to recalculate the vertex normals every time we move the object.
Actually, it's faster to calculate the vertex normals once and move them when the
object is moved. Because the normals indicate a direction and not a position, we only
want to rotate them.
The first code change required to support vertex normals is to add the normal vec-
tor to the vertex_type structure, as found in POLY_G.H. In addition, we will add the
length of the normal vector. The length of a vector doesn’t change when the vector is
rotated, so we only need to calculate the length once. A brightness value is also added
so we don’t need to recalculate it for every polygon we want to draw.
struct vertex_type {
// Structure for individual vertices
long lx,Lly,lz,Lt; // Local coordinates of vertex
long wx,wy,wz,wt; // World coordinates of vertex
long ax,ay,az,at; // World coordinates aligned with view
#ifdef LIGHTING_DEMO
Long sx,sy,st; // Screen coordinates of vertex
#endif
Long nlx,nly,nlz,nlt; // Untransformed normal vector
long nwx,nuwy,nwz,nwt; // Transformed normal vector
long nlength; // Length of normal vector
int brightness; // Brightness for Gouraud shading
428 m
oo
|
Advanced Polygon Shading
"EE
Next, the Object: Transform() function must change to perform the appropriate
transformation on the normal vector. Notice that the translation part of the transfor-
mation has been left out for the normal vector; only rotation is performed. Listing
12-10 contains the Gouraud Transform() function as found in POLY_G.CPP
The normal vectors for the vertices need to be calculated just once. The Object class
MeasureNormals() function did this job when we needed plane normals, so adjustment
to this function can determine the vector normals at the same time. First, it zeros out
the normal vectors for all the vertices. Then it goes through the list of polygons, gen-
erating a normal for each one and adding the normal to each vertex in the polygon list
of vertices. Lastly, it calculates the length of each vertex normal. The completed func-
tion is in Listing 12-11.
r
Listing 12-11 Gouraud MeasureNormals() function
void Object::MeasureNormals(int numshades)
{
HE BE BB BE B=
429
~~ Tome
Build Your Own Flight Sim in C++
vertexCvl.nlx=0;
vertexCvl.nly=0;
vertexCvl.nlz=0;
¥
vertex_type *vO=pptr->vertex[0];
vertex_type *vi=pptr->vertex[11;
vertex_type *v2=pptr->vertex[21;
long ax=v0->lx-v1->Lx; // Vector for edge from vertex 1 to
// vertex 0
Long ay=v0->ly-v1->ly;
Long az=v0->lz-v1->lz;
Long bx=v2->Lx-v1=->lx; // Vector for edge from vertex 1
to
// vertex 2
long by=v2->ly-v1->ly;
long bz=v2->lz-vi->lz;
long nx=ay*bz-az*by; // Normal is cross product of the
// two vectors
Long ny=az*bx—-ax*bz;
long nz=ax*by-ay*bx;
// OBJECT can access POLYGON since its a friend:
for (v=0; v< pptr=>number_of_vertices; v++) {
vertex_type *vptr=&vertex[vl;
vptr=>nlength=(long)sqrt((float)vptr->nlx*vptr->
nlx+(float)vptr=>nly*vptr->nly+
(float)vptr=>nlz*vptr->nlz);
430 =
Advanced Polygon Shading
mm n
polyptr-=>SetColor(palette.ColorIndex(temp,brightness,ambient));
This was fine for simple lighting, but now, ColorIndex() is called from within our
newly improved DrawPoly() function to allow us to construct shaded steps of colors
between two edges. In our new version of CalcObjectColor(), we can store the bright-
ness value for later use in this DrawPoly() function. The effect is not only a delay in the
call to ColorIndex(), but it becomes a call for each pixel, rather than for each polygon!
This extracts a price on the drawing speed for using Gouraud shading. Listing 12-12
shows the entire function.
GOURDEMO Summary
All that’s required now is to compile the GOURDEMO.IDE project. We have copied the
LIGHTDEM.CPP file to the GOURAUD directory and renamed it GOURDEMO.CPP. It
is exactly the same as LIGHTDEM.CPP since all of our changes from simple lighting to
Gouraud shading have taken place within the World and Object class modules. The
drawing of these polygons still happens with a call to World::Draw().
BEB
sa
HE BE BE BE
The effects of Gouraud shading are best seen using the icosahedron in ICOS.WLD.
Even with 20 polygons, the icosahedron doesn't really look like a sphere when ren-
dered with our Gouraud shading program. Despite this, the smoothing effect can
clearly be seen.
The Gouraud demo program, GOURDEMO.EXE, requires a text file argument just
like LIGHTDEM.EXE. Try running GOURDEMO ICOS.WLD to see the Gouraud effect
on an icosahedron.
Texture Mapping
While we have made great strides in improving the look of our polygonal world by
adding lighting, we haven't addressed the issue of detail. Even with dramatic lighting
In the programs so
a
effects, a handful of polygons still produces stark scene without intricacy or texture.
far, our world has consisted of one object. That object has had
either 4 (cube), 5 (pyramid), or 20 (icosahedron) sides. If we want more detail in an
432 =m
Advanced Polygon Shading
"EEN
object, we have to add more polygons. For very fine detail, the number of polygons
required grows enormously.
Texture mapping provides a way of adding surface detail without increasing the
number of polygons. Basically, texture mapping consists of pasting a bitmapped image
to the surface of a polygon. The bitmapped image moves with the polygon, so it
appears to be detail, or texture, on the surface of the polygon. Many commercial game
programs popular today use texture mapping to give the impression of detail without
a lot of polygons. Unfortunately, texture mapping is fairly slow, and the highly opti-
mized code used in commercial games is a closely guarded secret.
We will explore texture mapping by creating yet another demonstration program. It
won't be particularly fast, but it will embody all the important principles of texture
mapping. Later, we'll discuss some strategies for speeding the code up for use in
games.
The Strategy
The Gouraud shading program provides a good framework for doing texture mapping.
The simplification of DrawPoly(), in particular, is an important step in creating the tex-
ture-mapping program. Our new program will start with the simplified DrawPoly(), the
vertex structure as it was before the normal vectors were added, and the normal vector
and brightness calculations removed. We can remove the light source and palette code
since it is not being used. The World class no longer has Palette and source_type as
members. To share our WLD data file format, we alter World::LoadPoly() so that it
reads the light and palette numbers without storing them. This entitles us to use the
data objects from our previous demos. To this we will add the required texture-
mapping code.
The principle of texture mapping is simple: For each pixel of each polygon, figure
out where in the bitmapped image the pixel lies and paint it the appropriate color from
the bitmap. In practice, this is not all that easy to do. What is required is a set of cal-
culations that effectively reverse the perspective calculations that told us where to
draw the polygon on the screen. In addition, we must match up the bitmap with the
polygon regardless of the polygon’ orientation.
We'll have to make use of our Pcx object in order to load the bitmap and allocate
memory. We'll also need a class to control the pixels in the bitmap, just as our palette
controlled the colors in the shading demo. A new class, identifiable as Texture, will be
defined as a child class of Pex and contain fields for storing the vector information for
0, i, and j. A pointer to a Texture object will then be stored in the Polygon class.
The math is a bit complicated, and we won't cover
want to know basically how the calculations work. We
it in complete detail. But we do
will start by considering how to
match up the bitmap with the polygon before any perspective calculations come into
play.
433
Build Your Own Flight Sim in C++
- BEER
Positioning the Bitmap
The bitmap and the polygon can be matched up in an infinite number of ways. We can
rotate the bitmap on the polygon to any orientation, slide the bitmap up and down and
side to side, and we can stretch or shrink the bitmap. All these options are available, and
we must find an easy way to express the options we want in our program. As it turns
out, all these options can be specified with three vectors, as shown in Figure 12-6.
The first vector indicates where the origin of the bitmap (0,0) is positioned in
space. This vector handles the translation option that allows us to slide the bitmap
around on the polygon. We will call this vector o in our code.
The other two vectors indicate the orientation and scale of the x and y axes of the
bitmap. In our case, we will make the x vector point one bitmap pixel width in the pos-
itive x direction and the y vector point one bitmap pixel width in the positive y direc-
tion. These two vectors handle all the rotation and scaling options. We will call these
i
vectors and j, respectively, in our code. To work properly, these vectors must lie in the
same plane as the polygon and must be perpendicular to each other.
434 ®
Advanced Polygon Shading m
LL
Instead, we will generate the three vectors from the polygons themselves. First of all,
we will set the origin of the bitmap (0) equal to the average of all the vertices of the
polygon. Next, we will use a little vector math to generate the required i and j vectors.
In order to do their job correctly, the i and j vectors need to have two properties:
They have to lie in the plane of the polygon, and they have to be perpendicular to each
other. All of the edges of the polygon lie in the plane of the polygon, so any of them
will satisfy the first condition. We could select any of the edges and use it for i, but how
do we generate a perpendicular j vector?
An easy way
to do this is to select another edge not parallel to vector i and do a lit-
tle vector math to make it into a perpendicular vector. Lets assume we've selected a
second edge from which to generate our second vector. Call the vector corresponding
to that edge k. We can express k as the sum of two other vectors, one parallel to i and
one perpendicular to i. The vector parallel to i is called the projection of k on i. The
perpendicular vector, the one we want, is just k minus i. Figure 12-7 shows the vectors
and their relationships.
The formula for the projection of k on i is
p=CC(k*i)/Ci*i))*4
or, in C++:
float scale=((kx*ix+ky*iy+kz*iz)/(ix*ix+iy*iy+iz*iz));
float px=ix*scale;
float py=iy*scale;
float pz=iz*scale;
So the formula for the vector we want (namely j) is
jEk=CCk*i)/Ci*i))*i
HE BE BE
B® 435
Build Your Own Flight Sim in C++
BE BE BE BE
B&B
—-
since we want the perpendicular vector and it’s defined as k minus i. This translates to
the C++ code:
float scale=((kx*ix+ky*iy+kz*iz)/Cix*ix+iy*iy+iz*iz));
float jx=kx-ix*scale;
float jy=ky-iy*scale;
float jz=kz-iz*scale;
Now that we've created two vectors that lie in the plane of the polygon and are per-
pendicular to each other, there is one more issue to consider. How long should these
vectors be? As we will see later, the appropriate length for the vectors is
the width of a
single pixel of the bitmap. It is up to us what this value is. The longer i and j are, the
larger the texture bitmap pixels will appear on the screen. By defining a resolution
value and setting the length of i and j to 1/resolution, we can control the apparent res-
olution of the texture bitmap.
All three functions are public to allow outside access along with the vector fields of
texture_type and the public functions of the Pcx class.
Listing 12-13 contains the complete code for CalcMapVectors(). It takes two para-
meters, a pointer to a Polygon object and an int resolution, which uses an adjusted
scale from 30 to 100. The functions’ comments outline the steps we just discussed.
oy=0 Ne
0z=0;
// retrieve the number of vertices from the polygon:
int vcount = polygon->GetVertexCount();
for (int i=0; i< vcount; i++) {
vertex_type *vptr = polygon->GetVerticePtr(i);
ox+=vptr->wx;
oy+=vptr=>wy;
oz+=vptr->wz;
}
ox/=vcount;
oy/=vcount;
oz/=vcount;
// Generate raw i and j vectors
vertex_type *vpO polygon->GetVerticePtr(0);
vertex_type *vp1 = polygon->GetVerticePtr(1);
vertex_type *vp2 = polygon->GetVerticePtr(2);
ix=vp0->wx=-vp1->wx;
iy=vp0->wy-vpl->wy;
iz=vp0->wz-vpl1->wz;
jx=vp2->ux-vpl1=->wx;
jy=vp2->uy-vpl->wy;
jz=vp2->wz-vpl->wz;
// Make j perpendicular to i using projection formula
float scale=(ix*jx+iy*jy+iz*jz)/
Cix*ix+iy*iy+iz*iz);
jx=jx-scale*ix;
jy=jy-scale*iy;
jz=jz-scale*iz;
// Scale i and j
scale=(float)resolution/10*sqrt(ix*ix+iy*iy+iz*iz);
ix/=scale;
iy/=scale;
iz/=scale;
scale=(float)resolution/10*sqrt(jx*jx+jy*jy+jz*jz);
jx/=scale;
jy/=scale;
jz/=scale;
BB
formula o+u*i+v*j. The values u and v are the x and y coordinates of the point in the
bitmap that corresponds to
the original point in polygon.
Now we can see how the total calculation works. First, we take the screen coordi-
nates of the pixel we want to draw and apply our perspective calculations in reverse to
give us a point on the three-dimensional polygon. Then we figure out the appropriate
u and v values for that point and look up position u,v in the bitmap. That's the color we
use to draw the pixel on the screen.
Deriving the calculations to do this is a matter of cranking the formulas that we
already know through some high school algebra we've long forgotten. We won't cover
every tortuous detail, but we will look at the starting and ending points. First of all, we
have the vector formula above and the perspective formulas:
p=o+tu*i+v¥*j
x=distance*px/(distance-pz)+xorigin
y=distance*py/(distance-pz)+yorigin
In these formulas, p is the three-dimensional point on the polygon corresponding to
our pixel. (px,py,p2) is the same point with its three coordinates separated out. x and y
are the coordinates of the pixel on the screen, and xorigin and yorigin the coordinates of
the center of the screen. distance is a constant used to scale the perspective image on
the screen, as explained in Chapter 7.
The first formula is a vector equation and really represents three equations (one for
the x coordinates, one for the y coordinates, and one for the z coordinates). The three
equations are
px=ox+u*ix+v*jx
py=oytu*iy+v*jy
pz=oz+u*iz+v*jz
438 =
Advanced Polygon Shading
That means we have five equations and five unknown variables (u,v,px,py,pz). As long
as the number of unknown variables is no greater than the number of equations, we
can theoretically find a solution to the system of equations.
The code, which follows in Listing 12-14, shows the solution in the form of C++
code:
s
x==XORIGIN;
y==YORIGIN;
float ao=ox+x*oz/distance;
float bo=oy+y*oz/distance;
float ai=ix+x*iz/distance;
float bi=iy+y*iz/distance;
float aj=jx+x*jz/distance;
float bj=jy+y*jz/distance;
int u=(int)((bj*ao-aj*bo-bj*x+aj*y)/(bi*aj-ai*bj));
int v;
if (fabs(aj)>fabs(bj))
v=(int)((x-ao-u*ai)/aj);
else
v=(int) ((y=-bo-u*bi)/bj);
return MapPoint(u,v,color);
}
The variables ao, bo, ai, bi, aj, and bj don’t mean anything in particular. They are just
values that appear frequently in subsequent formulas, so we calculate them ahead of
time. The calculation that produces u will not result in a division by zero so long as the
i and j vectors are not parallel, and indeed, we constructed the j vector as being per-
pendicularto i. The v calculation can be done in one of two ways, only one of which
can result in a division by zero for a given set of values. The if statement determines
which calculation it.
is safest and uses
HE BE BE BE B®
439
a Build Your Own Flight Sim in C++
"mE
The function MapPoint() simply looks up the color in the bitmap. It is interesting to
note, however, that we don’t need to use a bitmap. So-called procedural texture map-
ping uses any number of special functions to determine the color at a given (u,v) loca-
EER
tion. For example, fractal calculations can be used to create wood grain textures or
simpler functions can be used to create plaids, checks, or other regular patterns. You
could make the MapPoint() function virtual such that a childs different procedure
would be called for MapPoint(). We will use a .PCX file bitmap in our demonstration
program, but you might want to experiment with procedural alternatives.
Below in Listing 12-15 is the code for MapPoint(). The .PCX bitmap contained in
the pexbuf structure is consulted to determine the appropriate color for our pixel.
Before the lookup into the bitmap, the x and y coordinates are modified so that the ori-
gin of the bitmap (0,0)is at the center. Remember that the origin vector, o, places the
bitmap origin at the center of the polygon. Placing the origin of the bitmap at the
bitmap’s center means that the center of the polygon and the center of the bitmap will
correspond.
If there was enough memory available for the bitmap and our texture was successfully
loaded, we loop through all the objects of the world in order
of its polygons.
to
set the texture pointers
440 =
Advanced Polygon Shading
m mm
//Set the objects to the texture
for(int oo=0;00 < world.GetObjectCount(); oo++){
(world.GetObjectPtr(oo))->SetTexture(&texturePcx);
}
To get the colors right, we need to load the .PCX file’s palette into the VGA hardware:
setpalette(pcxbuf.palette); // Set PCX palette
The name of the demo program for texture mapping is TEXTDEMO.EXE. It
requires an argument indicating the name of a WLD data file. The data file doesn’t need
meaningful light-source and palette information, but the place holders must be there.
CUBE.WLD is a good file to use, because the cubes large surfaces show the bitmap
nicely. An additional argument isrequired to indicate the resolution of the bitmap. Typ-
ical values range from 3 to 10. The bitmap is loaded from the file TEXTDEMO.PCX,
but by all means experiment with your own bitmaps.
aa
The View System
We now have nearly all of the program code that we need to build a world
and display it on the computer screen. We can construct objects, move and rotate
objects, remove hidden surfaces, and clip polygons at the edge of the viewport. With
one exception, we have the tools we need to build a view system—a program module
that will produce images of our imaginary world in a viewport on the video display,
showing how it looks from any position or angle.
it
As
stands, we have the capabilities necessary to put on a kind of play inside the
computer, with polygon-fill sets, props, and even actors, while the user sits back and
watches. But that’s not what we set out to do. Our goal was not to give the user a play
to watch, but to make the user the main character in a play. We don’t want the user just
watching the action; we want the user to enter the world inside the computer and
become part of the action.
So far, though, we've watched objects rotating on the video display, we've even
been able to move those objects at will, but we have not been able to alter our point of
view relative to those objects. To do that, we must push our three-dimensional capa-
bilities a step further, giving ourselves freedom of movement and effectively entering
the world inside the computer. In fact, this is the last major step in developing the
graphic capabilities necessary for a flight simulator.
445
. -
Build Your Own Flight Sim in C++
-
EB EERE EB
The view system is the part of a three-dimensional animation program devoted to
placing an image of the three-dimensional world on the video display of the computer.
Much of what we've discussed so far in this book, from transforming vertices in two-
and three-dimensional space to removing hidden surfaces and clipping polygons, can
be used to build a view system. In this chapter we'll discuss how all of these disparate
parts can be put together to form the view system itself.
446 ®
|
The View System
mw wm
Simple. The part of our program that simulates the flight of an airplane will be
allowed to place the viewer at any position it wishes in our 3D universe. It will then
pass the viewer's current coordinates to the view system, so that the view system can
draw an image in the viewport showing how the 3D universe would look from those
coordinates.
Our view system, however, will immediately move the viewer back to coordinates
(0,0,0), the only position from which it knows how to display a view of the universe.
How, then, do we produce the illusion that the viewer has moved to a new position
within the universe? By moving all other objects in the universe so that they will
appear exactly as they would if the viewer were at the coordinates specified by the
flight simulator! (See Figure 13-1.) If Mohammed can’t come to the mountains, the
view system will literally bring the mountains to Mohammed—or to Mike or Melinda
or whomever happens to be playing with the flight simulator at the time.
This would be a pretty difficult trick to pull off in the real universe, where it’s a lot
easier to jump in an airplane and fly to Chicago than it is to bring Chicago to your
home. Of course, the earth is always rotating, so in theory you could levitate a few hun-
dred feet above the ground until Chicago came rotating around. This, in fact, is the
basis for the Coriolis Effect, which, among other things, makes airborne missiles
appear to drift in the direction opposite the earths rotation. But this is an impractical
means of travel.
In our miniature computer universe, however, it is no major problem to move the
entire universe relative to the user. It is simply a matter of creating the appropriate
translation matrix and then multiplying it by the coordinates of every object in the uni-
verse. Voila! The universe is moved! This might sound like a time-consuming task, but
by combining this translation matrix with several other useful transformation matrices,
we can kill several birds with a single series of multiplication stones. This is actually a
very efficient way to use program execution time.
HE BE BE
EB 447
~
—
Build Your Own Flight Sim in C++
— "EE EEE
(b) House moving toward man. From his point of view, both
motions are visually equivalent
448 ®
The View System m
The first three fields, x, y, and 2, contain the x, y, and z coordinates, respectively, of the
viewers position. The second three fields, xangle, yangle, and zangle, contain the
~~
"ow F
viewer’ rotation on the x, y, and z axes. These are all measured relative to the coordi-
nate system and orientation of the world in the computer. When the x, y, and z fields
are set to 0, 0, and 0, the vieweris at the origin of the universe. When the xangle, yan-
gle, and zangle fields are set to 0, the viewer’ local x, y, and z axes are aligned perfectly
with the x, y, and z axes of the universe.
For instance,if the x, y, and z fields are set to 17,-4001, and 6, this means that the
local origin of the object is 17 units from the world origin on the x axis, -4001 units
from the world origin on the y axis, and 6 units from the world origin on the z axis.
Similarly, if the xangle, yangle, and zangle fields are set to 178, 64, and 212, then the
viewer has rotated 178 degrees relative to the world x axis, 64 degrees on the world y
axis, and 212 degrees on the world z axis. (Remember, we're using a 256-degree rota-
tional system here, as described in Chapter 9.)
449
Build Your Own Flight Sim in C++
— HoH BH EB
BB
Figure 13-3 When the viewer rotates, his or her local axes
rotate relative to the z axes of the virtual world
(a) Person standing with local z axis aligned with the world z axis
The most important thing that the viewer's orientation tells the view system is the
direction in which the viewers line of sight is pointing, i.e., what the vieweris
looking
at—or would be looking at the viewer were actually moving around the world inside
if
the computer.
450 =
The View System
En |
(b) Person rotated forward so that local z axis now is out of alignment
with the world z axis
We're going to use the system of transformation matrices that we developed back in
Chapter 7 to transform the coordinates of all the objects in the universe so that they
appear as they would from the coordinate position of the viewer. Before we can use the
transformation matrix functions, we must call the inittrans() function to initialize the
master transformation matrix:
H BE BE
EB 451
-
NB
// Initialize transformation matrices:
inittrans();
We need to perform two different transformations on all the objects in the universe. We
must translate them in the x, y, and z dimensions and we must rotate them on the x, y,
and z axes. First we'll set up the matrix for the translation. Before that, however, we
must decide where we want to
locate all of those objects.
Relative Motion
This decision is pretty simple to make, but it requires that we take another look at the
problem. Suppose the flight simulator has determined that the viewer, snug in the
cockpit of his or her aircraft, is
at coordinates 1271, -78, and 301 in the virtual uni-
verse. Our view system has no way of moving the viewer to those coordinates, so
instead it places the viewer at coordinates (0,0,0). Now it must move everything else in
the universe so that it appears as it would if the viewer were at coordinates
(1271,-78,301). In effect, it must move the point in the universe that is at coordinates
(1271,-78,301) until it is at coordinates (0,0,0), effectively aligning the viewer’ virtual
position with the viewer’ real position.
How do we do this? We subtract the users virtual coordinates from the user’ real
coordinates and translate every object in the universe by the resulting difference in each
coordinate. This is simple since the user’ real coordinates will always be (0,0,0): thus,
the difference in each coordinate will always be the negative of the virtual coordinate.
In the example in the previous paragraph, for instance, we can make the world appear
as it would if the viewer were at (1271,-78,301) by translating every object in the uni-
verse by x,y,z translation factors of (-1271,78,-301). Why does this work? Let's look at
a real-world example and then a flight-simulator example.
Have you ever had the experience of sitting in a stationary car (perhaps in a parking
lot), looking out at the car next to you, and realizing that your car was rolling back-
ward—only to discover, when you stabbed at the brake, that you weren't moving back-
ward at all but had seen the car next to you moving forward? This is an easy mistake to
make, as long as the only frame of reference in view is your own car and the other car.
Motion of your car backward relative to the other car looks and feels exactly like motion
of the other car forward relative to yours. (See Figure 13-4.) (Acceleration of one car rel-
ative to another, which can be detected by other means, is a different kettle of fish.)
In the same way, we can simulate forward motion of an airplane in a flight simula-
tor by producing backward motion of the universe. Suppose we are in an airplane at
coordinates (0,0,0) within our imaginary universe and we fly forward 100 units along
the z axis. As far as the out-the-window 3D view is concerned, this is equivalent to
translating every object in the universe backward the same distance along the z axis,
using a translation factor of -100. (See Figure 13-5.) The same is true of motion along
the other two axes.
452 =
The View System CI
I)
000
The position and orientation of the viewer are passed to the AlignView() function in
a view_type structure called view. Thus, the virtual coordinates of the viewer are con-
tained in the fields view.x, view.y, and view.z. To translate the universe into the proper
position, we simply set up a translation matrix using the negative values of each of
these coordinates, like this:
translate(-view.x,~view.y,-view.z);
And that’ all that needs to be done to set up a translation of all objects in the universe
to make them appear as they would from coordinates view.x, view.y, and view.z. 1
promised that it would be easy, right?
- - -
Aligning Rotation
Rotation is performed in pretty much the same manner. Once we've translated every
object in the universe so that it appears as it would from the viewer’ virtual position,
we must rotate those objects around the viewer’ position until they appear from an ori-
entation of (0,0,0) on the x, y, and z axes as they would appear from the viewer’ virtual
orientation—where the flight simulator believes the viewer to be. If you guessed that
we do this by rotating every object in the universe by the negatives of the degrees to
which the viewer is supposed to be rotated, go to the head of the class. We set up the
rotation matrix like this:
rotate(-view.xangle,-view.yangle,-view.zangle);
At this point, there’ nothing left to do except actually multiply the coordinates of every
object in the universe by the master transformation matrix that we have set up with the
last two function calls. We can do this with a for loop:
Object *optr = world->GetObjectPtr(0);
for (int i=0; i<world->GetObjectCount(); i++, optr++) {
454 ®
The View System
mmm
// Rotate all objects in universe around origin:
rotate(-view.xangle,-view.yangle,-view.zangle);
// perform the transformation on every object
Now
// in the universe:
Object *optr = world->GetObjectPtr (0);
for (int i=0; i<world->GetObjectCount(); i++, optr++) {
HE BE BE BE
= 455
oo.
~~ Build Your Own Flight Sim in C++
a EE BB
Because we have placed the results in the aligned coordinate fields, we still have the
world coordinates available should we need them later. (We won't need them in the
current version of the view system.) And, because the local coordinate fields have
been untouched by later transformations, they are still available so that we can start this
whole process again the next time the view system is called.
private:
int xorigin,yorigin;
int xmin,ymin,xmax,ymax;
int distance,ground,sky;
World world;
unsigned char *screen_buffer;
ClippedPolygon clip_array;
PolygonList polygon_Llist;
int projection_overlap(Polygon poly1, Polygon poly2);
The View System m mm
ee]
int intersects(int x1_1,int y1_1,int x2_1,1int y2_1,
int x1_2,int y1_2,int x2_2,int y2_2);
void AlignView(World *world,view_type view);
void DrawHorizon(int xangle,int yangle,int zangle,
unsigned char *screen);
void Update(Object *object);
void DrawPolygonList(unsigned char far *screen);
void XYClip(ClippedPolygon *clip);
void ZClip(Polygon *polygon,ClippedPolygon *clip, int zmin=1);
public:
void SetView(int xo,int yo,int xmn,int ymn,int xmx,
int ymx,int dist,int grnd,int sk,
unsigned char *screen_buf);
void SetWorld(const char *worldfilename);
void Display(view_type curview,int horizon_flag);
};
You might recognize most of these declarations as variables and functions discussed in
earlier chapters. In particular, xmin, ymin, xmax, ymax, clip_array, and polygon_list were
all used in the chapter on clipping where we delved into the inner workings of ZClip(),
XYClip(), and DrawPolygonList(). There is no constructor for the View class and only
three of the functions are public, none of which have been introduced yet. Let’ look at
them one at a time.
xorigin=xo0; //
coordinate of screen origin
X
yorigin=yo; //
coordinate of screen origin
Y
xmin=xmn; X
//
coordinate of upper left
//
corner of window
Xmax=xmx; X
//
coordinate of Lower right
//
corner of window
ymin=ymn; //
coordinate of upper Left
Y
//
corner of window
ymax=ymx; //
coordinate of Lower right
Y
//
corner of window
distance=dist; //
Distance of viewer from display
ground=grnd; //
Ground color
sky=sk; //
Sky color
screen_buffer=screen_buf; // Buffer address for screen
screen_width=(xmax-xmin)/2;
screen_height=(ymax-ymin)/2;
}
This function passes the value of the parametersto variables local to the View class so
that they can be referenced later by other functions in the class. In fact, we've already
seen some of these variables—screen_buffer, xorigin, yorigin—referenced in some of
the functions that are now part of this class. In a moment, we'll see the others in
action.
458 =
The View System
"===
RE ‘Listing 13-4 The SetWorld() function
void View::SetWorld(const char *worldfilename)
{
int polycount= world.LoadPoly( worldfilename );
polygon_Llist.Create(polycount);
// Initialize transformations:
inittrans();
// Create scaling matrix:
object->Scale();
// Create rotation matrix:
object->Rotate();
// Create translation matrix:
object->Translate();
// Transform OBJECT with master transformation matrix:
object=>Transform();
continued on next page
HE BE BE
EN 459
ih
b Build Your Own Flight Sim in C++
"EE EERE
continued from previous page
460 =
The View System m = =m
The first three parameters represent the rotation of the viewer relative to the world
axes. The pointer to unsigned char screen points to either the screen or the screen
buffer.
461
Bud Your Own Flight Sim in C++
"= =
"EEE
Figure 13-6 Which axis the viewer is rotating on determines how the
horizon appears to rotate
(@) Rotation on the viewer's z axis causes the horizon to rotate on its z axis
(b) Rotation on the viewer's x axis causes the horizon to move up and down
The View System
"Ew
We start by declaring some useful variables. These will be used as the and left right
coordinates of the horizon line as well as temporary storage during calculations:
long rx1,rx2,temp_rx1,temp_rx2;
long ryl1,ry2,temp_ry1,temp_ry2;
long rz1,rz2,temp_rz1,temp_rz2;
We'll represent the sky and ground as polygons, which can be stored in an array of four
vertices that we'll call vert:
vertex_type vert[41;
And we can set the aligned z field of the horizon polygons to a standard value. We'll
use the viewer distance from the display, initialized by SetView():
vertL0J.az=distance;
vertC1].az=distance;
vertL2].az=distance;
vert[3].az=distance;
We also need a clipped polygon, for the call to DrawPolygon():
ClippedPolygon hclip;
And we'll need a Polygon class object, to
point at these vertices. Uh,oh. The Polygon
class was never designed with access to the vertices. The Object class does initial the
assignment to
the vertices in Object::load(), but it was declared
into the vertices
as a friend
without
to the
having to
Polygon class. What we need is a way to stuff values
HE BE BE BE BE
463
oo.
~~ Build Your Own Flight Sim in C++
mA =
"NEE
go back and tear up the Polygon class (and rename the class again). The solution is
rather easy in C++. We break from our current function to write a class definition
above the function to alleviate the encapsulation difficulty. The localPolygon class is
defined along with its constructor:
class localPolygon: public Polygon
{
public:
LlocalPolygon(int num, vertex_type *vertlist);
// the base class (poygon_type) destroys allocated vertex
// ptr array
“localPolygon(){;}
void SetColor(int col)
{ color = col; }
};
{
localPolygon::localPolygon(int num, vertex_type *vertlist)
// Allocate memory for polygon and vertices
vertex = new vertex_type *[numl;
number_of_vertices = 4;
vertex[0J=&vertlist[0];
vertex[1J1=&vertlist[1];
vertex[2]1=8&vertlist[2];
vertex[3J1=&vertlist[3];
}
464 m
The View System m mm
When this conversion occurs, we must note it, since it means that we need to swap
the colors of ground and sky to make it appear as though we are seeing the horizon
Because a
upside down. The integer variable flip will be incremented to flag this event.
side
7-axis rotation in the 64- to 127-degree range can put the horizon right up again
it down if no x-axis flip has occurred), we must also flip z-axis rotations
(or put upside
and increment the flip variable when we do so. Thus a value of 0 in the flip variable
we're right side
means we're right side up, 1 means we're upside down, and means
2
odd, then, we can determine whether
up again. By checking to see if flip is even or
ground and sky colors need to be swapped:
int flip=0;
xangle&=255;
if ((xangle>64) && (xangle<193)) {
xangle=(xangle+128) 255;
&
flip++;
¥
zangle&=255;
if (C(zangle>64) && (zangle<193)) {
flip++;
}
if (ldx) dx++;
This means that we can never have a perfectly vertical horizon, but this is
largely
unnoticeable in practice and saves us
Now we obtain the slope:
a
lot of work in drawing the horizon.
466 =
The View System
CLE |
We'll use the first two vertices of the vert structure to hold the upper line of the
and the width of the
ground polygon, which will be a line with the slope of the horizon
function):
viewport (i.e., from xmin to xmax, as established by the SetView()
vert[OJ.ax=xmin;
vert[Ol.ay=slope*(xmin-rx1)+ry1;
vert[1Jl.ax=xmax;
vert[1].ay=slope*(xmax-rx1)+ry1;
If we had to user’ x rotation coordinate or z rotation coordinate earlier, this
flip the
This
ground polygon will actually be a sky polygon and should be colored accordingly.
can be determined by checking to if
see the
is
flip variable even (i.e., has a least signif-
icant binary digit of 0). If not, color the polygon with the sky color:
if (flip&1) hpoly.SetColor(sky);
Otherwise, color it with the ground color:
else hpoly.SetColor(ground);
be sure
Now we set the other two vertices to the extreme coordinates so that we can
they fit properly in the viewport and there will be no problems drawing
the proper line
slope:
vertL2].ax=32767;
vert[2].ay=32767;
vert[31.ax=-32767;
vert[3].ay=32767;
Then there’ nothing left to do but clip the polygon to the viewport:
ZClip(&hpoly,&hclip);
XYCLlip(&hclip);
And—if we haven't clipped it out of existence—draw it:
if (hclip.GetVertexCount())
hclip.brawPoly(screen);
Thats all there were flipped over on either the x
is to drawing the ground (or sky, if we
or z coordinates). The same process draws the sky (or ground) polygon:
EE EEN 467
s Build Your Own Flight Sim in C++
vert[2].ay=-32767;
vert[3].ax=-32767;
vert[3l.ay=-32767;
#1 EH NB BE
EF
// Clip sky polygon:
ZClip(&hpoly,&hclip);
XYCLlip(&hclip);
The horizon is drawn. When the end of the function is reached, hclip—the
localPolygon
object—will go out of scope and its destructor called, thereby freeing
up the memory
used by the temporary pointer array. The complete DrawHorizon() function
appears in
Listing 13-6.
long rx1,rx2,temp_rx1,temp_rx2;
Long ryl,ry2,temp_ry1,temp_ry2;
long rz1,rz2,temp_rz1,temp_rz2;
vertex_type vert[4];
vertlOl.az=distance;
vertL1].az=distance;
vertl2].az=distance;
vert[3]l.az=distance;
ClippedPolygon hclip;
localPolygon hpoly(4, vert);
// rotation angle to remove
Map backward wrap-around:
int flip=0;
xangle&=255;
if ((xangle>64) && (xangle<193)) {
xangle=(xangle+128) &
255;
flip++;
}
zangle&=255;
if ((zangle>64) && (zangle<193)) {
}
flip++;
468 m
The View System m
mm
// Create initial horizon line:
rx1=-100; ry1=0; rzl=distance;
rx2=100; ry2=0; rz2=distance;
// Rotate around viewer's X
axis:
- rz1*SIN(xangle))
temp_ryl1=(ry1*Cc0S(xangle) >> SHIFT;
temp_ry2=(ry2*C0S(xangle) - rz2*SIN(xangle)) >> SHIFT;
temp_rz1=(ry1*SIN(xangle) + rz1*C0S(xangle)) >> SHIFT;
temp_rz2=(ry2*SIN(xangle) + rz2*C0S(xangle)) >> SHIFT;
ryl=temp_ry1;
ry2=temp_ry2;
rz1=temp_rz1;
rz2=temp_rz2;
// Rotate around viewer's ~N
axis:
temp_rx1=(rx1*C0S(zangle) 1 ry1*SINCzangle)) >> SHIFT;
temp_ry1=(rx1*SINC(zangle) + ry1*C0S(zangle)) >> SHIFT;
temp_rx2=(rx2*C0S(zangle) 1 ry2*SINC(zangle)) >> SHIFT;
temp_ry2=(rx2*SIN(zangle) + ry2*C0S(zangle)) >> SHIFT;
rx1=temp_rx1;
ryl=temp_ry1;
rx2=temp_rx2;
ry2=temp_ry2;
// Adjust for perspective
float z=(float)rz1;
if (z2<10.0) z=10.0;
// Divide world x,y coordinates by z coordinates
// to obtain perspective:
rx1=(float)distance*((float)rx1/z)+xorigin;
ryl=(float)distance*((float)ryl/z)+yorigin;
rx2=(float)distance*((float)rx2/z)+xorigin;
ry2=(float)distance*((float)ry2/z)+yorigin;
// Create sky and ground polygons,
// then clip to screen window
// Obtain delta x and delta y:
469
\ B uild Your Own Flight Sim in C++
470 m
The View System
"Ew.
ZClip(&hpoly,&hclip);
XYCLip(&hclip);
Before we can draw anything in the screen buffer, we have to clear out whatever's left
from the last view. So we call the clrwin() function to clear out the viewport:
clrwin(xmin,ymin,xmax-xmin+1,ymax-ymin+1,screen_buffer);
The parameters used here to specify the dimensions and address of the viewport were
established by the SetView() function.
If the value of the horizon_flag parameter passed to this function is nonzero, the
calling application wants a horizon in the view. So we check the flag and, if
requested,
draw the horizon:
if (horizon_flag) DrawHorizon(curview.xangle,curview.yangle, curview.zangle,
screen_buffer);
The
calling application may have made changes to the positions and rotations of the
objects in the world database. If so, we need to update the vertices of those objects.
Thats the job of the Update() function, which we described earlier in this chapter. We
can call it in a loop, one by one,to see if the objects in the database need updating:
for (int i=0; i<world.GetObjectCount(); i++) {
Update(world.GetObjectPtr(i));
}
To make the view drawable, the positions of all objects must be adjusted until they
appear as they would from the viewer's position. That's the purpose of the AlignView()
function, also described earlier in this chapter:
AlignView(&world,curview);
Before drawing, a polygon list must be constructed:
polygon_Llist.MakePolygonList(world);
HE BE BE
Em 471
~~ —
—
Build Your Own Flight Sim in C++
All the preparation is done. The view is created in the screen buffer by drawing the
polygon list:
DrawPolygonList(screen_buffer);
The complete text of the Display() function appears in Listing 13-7.
472 =m
The View System m mm
473
bud Your Own Flight Sim in C++
float z2=vi1->az;
float z3=v2->az;
// Calculate dot product:
float c=(x3*((z1*y2)=-(y1*z22)))+
C(y3*((x1*%22)-(z21*x2)))+
(23*((y1*x2)=-(x1*y2)));
return(c<0);
The entire View module is complete at this point. To keep things organized, all View
class functions are in the file VIEW.CPP, with headers in VIEW.H. Matrix manipulation
functions remain in the file POLY2.CPP where they've been all along.
A few details remain before we can create a flight simulator around this view system.
The first, which we'll discuss in the next chapter, involves giving the view system
something to view. In other words, we need to construct some scenery.
474 =m
oo
Fractal Mountains
and Other Types
of Scenery
An empty universe, even one with a horizon delineating blue sky from green
ground, is a boring universe. Wandering around—or flying—through such a universe,
youd never know where you were. Some of the early flight simulators were like this.
All of the action was in the sky, where jet planes or World War I biplanes battled it out
for supremacy
ofthe air, and there wasn't much on the ground. Except for airplanes,
the world was empty.
Flight-simulator programmers can't get away with that any longer. Even the simplest
flight simulator needs scenery, and some of the most elaborate flight simulators sell
disks that add increasingly detailed scenery to the initial database. The scenery, in
fact, is often one of the most entertaining parts of the flight simulator. What armchair
pilot isn't tempted to dip a wing toward the ground to get a look at the city far below
or even to dive under the Golden Gate Bridge?
So far, though, we've populated our world with very few objects—a cube here, a
pyramid there. How do we build up something that looks like the scenery in the real
world?
477
. — Build Your Own Flight Sim in C++
478 ©
Fractal Mountains and Other Types of Scenery
CLL
The second section is the object section. The first number in every object section
represents the number of objects in that file. Most of the files on the accompanying
disk contain a single object only (and thus have the number 1 for the object count),
a
but
a
file of objects for flight simulator would start its object section with a value rep-
resenting the total number of objects in the file. If the world that you've designed con-
tains 118 objects, then the object count should be the number 118.
This number is followed by a series of two-part object descriptors, as many as are
indicated by the first number. In our hypothetical world file, there would be 118
object descriptors.
The first seven numbers in an object descriptor have not been explained. The
first three represent the position of the object within the world, in x,y, and z coor-
dinates. The local origin of the object will be moved to this position before the view
is drawn. The next three numbers are the rotation of the object on its local x,y, and
z axes. And the seventh number is a scale factor for the object—that is, the object
will be magnified by this factor before it is drawn. Although the version of the
Object class that will be used in the flight simulator has fields for separate x,y, and
z scale factors, and the scale() function in the POLY2.CPP module will scale sepa-
rately in each dimension, the current version of the WLD file format uses a single
scale factor for all three dimensions. (If you'd like to change the program to handle
separate scale factors, you'll need to change the LoadPoly() function in the
LOADPCPP module. Here's a hint: You'll need only to remove comment marks
from two lines and add them to one other line, since the code is already in place
for the revised format but was not used in order to remain compatible with the cur-
rent world database.)
These seven numbers are followed by a single number representing the number of
vertices in the object. This is followed by a series of vertex descriptors equal to the
number just given. So if the first number in the object descriptor is it 5, should be fol-
lowed by five vertex descriptors. Each vertex descriptor consists of three numbers: the
x,y, and z coordinates, respectively, of the vertex.
The second part of the object descriptor is a list of polygons. The list begins
with a number representing the number of polygons in the object, followed by that
number of polygon descriptors. Each polygon descriptor begins with the number of
vertices in that polygon, followed by pointers to those vertices in the vertex list for
that object—i.e., numbers representing the positions of the vertices in the vertex
list. These are followed by a number representing the palette position of the color
for that polygon. And that’s it. All object descriptors in the file follow this general
format.
A sample file consisting of an object descriptor for a house is shown in Listing 14-1.
Although quite simple, in the right context it could pass for quite a respectable house,
especially if clustered along a polygon-fill avenue side-by-side with a row of similar
houses. Figure 14-1 shows how the object is constructed.
HE BE BE
EB 479
—u
~~ Cee
Build
0,-20,10,
50, *
1,
8,
a
Your Own Flight Sim in C++
* Number of
0,0,0,
* Number
0,0,31,
0,31;0,
unshaded colors
* Black
of shaded colors
*
*
Blue
Green
"
0,63,63, * Cyan
63,0,0, * Red
63,0,63, * Magenta
31,3150, * Brown
31,31,31, * GRAY
0,063; * Bright blue
0,63,0, * Bright green
63,63,0, * Yellow
63,63,63 * White
**%%*
Object definition file **%
480 =
Fractal Mountains and Other Types of Scenery
"ow
4, 0,1,5,4, 1,
4, 5,6,7,4, 2,
4, 6,2,3,7, 3,
4, 2,1,0,3, 4,
4, 2,6,5,1, 5,
4, 4,7,3,0, 6,
4, 8,11,12,9, 7,
3, 8,9,10, 8,
3, 9,12,10, 9,
3, 12,11,10, 10,
3, 11,8,10, 11,
1 * Yes, we want backface removal.
Actually, this object is more detailed than most buildings in a flight simulator need
to be. In fact, whole cities in the databases of {light simulators such as Falcon 3.0 have
been built by placing variously sized cubes together along a grid resembling a layout of
avenues. (A certain well-known flight simulator often gets by with just the avenues and
no cubes at all.)
481
~~
ee
Build Your Own Flight Sim in C++
-
an ASCII text file, which could be merged with other ASCII data to create a .WLD file.
This idea of writing computer programs to create specific types of objects is a pow-
erful one. I'm going to touch on some of the theory in this chapter and skip the
details, but you should consider this concept and perhaps write some programs yourof
own that use this idea.
Right-Handed or Left-Handed?
One reminder: As we've mentioned in earlier chapters, polygons should be specified in
the object descriptor in such a way that the vertices appear to move in a counter-
clockwise direction around the visible face of the polygon. Why is this? I won't go into
the mathematical reasons—I won't even pretend that I fully understand them—but
has to do with the fact that we are using what is called a right-handed coordinate sys-
it
tem. If we were using a left-handed coordinate system, we would need to arrange the
vertices in a clockwise manner around the polygon face; otherwise, the Backface()
function, as well as some of the hidden surface ordering tests shown in Chapter 10,
would cease to work properly.
How can you tell if a coordinate system is right-handed or left-handed? The stan-
dard method is to imagine that you are grasping the z axis of the system with your fin-
gers curling from the positive z axis to the positive y axis. If you can do this with your
right hand, then the system is right-handed. If you can't, then it’s left-handed. (If your
thumb is x, your pointer finger y, and your middle finger z, point all three fingers in
positive directions—whichever hand can do it, that’s your coordinate system).
If you have trouble grasping this distinction, so to speak, then heres an alternate
method of distinguishing between coordinate systems: Any system in which the y axis
is positive going up, the x axis is positive going right, and the z axis is negative going
into the screen, or in which the axes can be rotated into this configuration, is right-
handed. If it can’t be rotated into this configuration, then it’s left-handed. The system
used in this book has the y axis negative going up and the z axis positive into the
screen. But if you rotate the axes 180 degrees around the x axis, it magically becomes
a right-handed system. And that’s why the vertices are specified in a counterclockwise
direction.
Fractal Realism
Surely the most difficult objects to design by hand are those that resemble features of
the real world. Unlike buildings, which are essentially cubical, or streets and parking
482 ®
Fractal Mountains and Other Types of Scenery m m =m
Self-Similarity
Most trees have an essentially fractal shape because a branch of the tree resembles the
tree as a whole. A magnified picture of a branch might well be mistaken for a picture of
HE EH BE HE B®
483
§a
~~" Build Your Own Flight
8
Ba
Sim in C++
"EB EE EB
similarity; made
it is out of branches and twigs that
resemble the whole tree
a tree. And who can tell a magnified picture of a twig from a picture of a branch? In
each case, only the size—the level of magnification—is different. The shape remains
the same. (See Figure 14-3.)
do_nothing(meaningless_value++);
484 ®
Fractal Mountains and Other Types of Scenery m m m
In this example, the recursive function does nothing more than call itself and pass itself
an integer parameter of ever-increasing value. Obviously, this function is worse than
useless. If compiled and executed, it would lock up the computer by calling itself
eternally (or until someone hit the reset switch).
A slightly more useful recursive example would be the following:
void print_numbers(int
{
start,int finish)
if (start>finish) return;
else {
This is a bit more cumbersome than the last recursive function, but it also does a lot
more. What does it do? Well, here’s the main() function that calls this function:
void main()
{
print_numbers(1,10);
}
The function prints out a sequence of numbers, starting with the first parameter and
ending with the second. In the example call, the print_numbers() function would
print out this sequence:
WKN
Ub
XN
OO
—
0
While not terribly useful, this example begins to show what recursion can do. In
effect, the recursive function has created a for loop where no such loop in fact exists.
The print_numbers() function continues calling itself until the value of start has been
incremented past the value of finish.
Of course, the for loop could have done the same thing more efficiently and a great
deal more clearly, but recursion does have its uses. Let’s look at the one that will be of
most use to us.
HE BE EE
EB 485
EE BE BE BE BE EB
Build Your Own Flight Sim in C++
Recursive Graphics
A picture can contain smaller copies of itself. Everybody's familiar with the image that
is created when one mirror faces another mirror. Each mirror contains images of itself
reflected in the other mirror and these images in turn contain smaller and smaller
images of both mirrors. This is a kind of recursive image, an image that calls itself, in
effect. Like our earlier recursive function that called itself infinitely, the mirror images
theoretically go on without end, repeating themselves in smaller and smaller images
until they recede into the mists. These images are also a kind of fractal, because they
are self-similar at each diminishing level of reflection.
Fractals, in fact, are a kind of recursive shape. It's almost as if they were created by
a function that calls itself again and again to create smaller and smaller, yet essentially
identical, images. Such living fractal objects as trees may indeed have a kind of recur-
sive function in their genes that calls itself to produce branches, twigs, and ever smaller
branchlike shapes.
Recursion therefore is an ideal technique for drawing fractal shapes. To prove this,
let’s create a recursively organized program for drawing a natural fractal: a mountain
range.
486 ®
Fractal Mountains and Other Types of Scenery m
mm
HE BE BE BE B=
487
~~
_ Build -
Your Own Flight Sim in C++
~ mE
EE EEE
~
This function, which we've called lineseg() (because essentially it just draws segments
of lines), does almost all of the work of drawing the mountain range. But how? It cer-
tainly doesn’t look capable of doing all that. To see the secret, lets look at the entire
program, line by line.
488 ®m
Fractal Mountains and Other Types of Scenery
"EN
// Set video to 320x200x256:
setgmode (0x13);
All of this activity should be familiar to you by now. The randomize() statement ini-
tializes the random number generator. Then we set up the graphics by pointing the
pointer variable at video RAM, clearing the screen, saving the old video mode, and set-
ting the current video mode to 320x200x256 colors. We then set up the loop to con-
tinue until the key
is
pressed. Each time through the loop the screen is cleared
and we call the recursive function. This loop continues until key is set equal to
27, the
ASCII value for
the key:
int key = 0;
while( key !'= 27)
‘
// Clear graphic screen:
cls(screen);
// Call recursive Line segmenting function:
Lineseg(0,319,100,100,depth, range);
// Hold picture on screen until key pressed:
while(! kbhit());
key = getch();
}
The first two parameters to the lineseg() function are the horizontal positions on the
display at which the mountain range begins and ends. The third and fourth parameters
are the vertical positions of the starting and ending points of the mountain range. (This
particular call sets up a mountain range from the left side of the display to the right,
across roughly the vertical center of the display.) The fifth and sixth parameters estab-
lish the recursive depth and vertical range, as defined above.
Once the loop
is
over, the screen is reset to the starting video mode:
// Reset previous video mode:
setgmode (oldmode) ;
}
489
~~ Build Your Own Flight Sim in C++
- EEE EEE
Then it checks to
see if the depth parameter has reached 0 yet. If it has, it draws a line
segment between the positions defined by the first four parameters, using the line-
draw() function from our BRESNHAM.CPP file:
int midvpos=(vpos1+vpos2)/2+random(range)-range/2;
// Call recursively with random perturbation:
lLineseg(hpos1,(hpos1+hpos2)/2,vpos1,midvpos,depth-1,
range/2);
Lineseg((hpos1+hpos2)/2,hpos2,midvpos,vpos2,depth-1,
range/2);
>
These two new line segments represent the halves of the old line segment, the one
defined by the first two parameters. The function has now split that line in two and
called itself twice to draw the halves. In doing so, however, it has perturbed the posi-
tion of the midpoint of the line segment up or down randomly, within the area defined
by the parameter range. And when it calls itself recursively, it subtracts 1 from the value
of depth, counting down toward 0, the value that will trigger the drawing of the line
segments. And it has cut the value of range in half, so that the next line segment will
have its midpoint perturbed somewhat less than the previous one.
Do you see how this works? The lineseg() function calls itself twice, but each of
those calls to lineseg() in turn calls lineseg() twice again. See Figure 14-5 for an illus-
tration of this recursion. In our example with a depth of 5, by the time the final recur-
it
sive level depth of 5 has been reached, will have called itself 32 times Each time the
.
lineseg() function is called, the line segment has had one of its endpoints randomly
perturbed up or down. We've added an additional feature to interpret the key input to
demonstrate the effect of the depth variable. Pushing a number from 1 to 9 on the key-
board will assign that value to depth, so the results can be seen on the screen.
The visual result is
a jagged series of line segments seemingly rising and falling in a
random, yet oddly smooth, pattern—the mountain range that you see when you run
the program.
490 =
Fractal Mountains and Other Types of Scenery m
mm
——
(¢) Third level: four line segments
Noor? Vu
I=
(d) Fourth level: eight line segments
HE BE BE
EB 491
~~" Build Your Own Flight Sim in C++
- EEE EEN
The Mountain Program
The complete listing of the MOUNTAIN.CPP program appears in Listing 14-2.
492 ®
Fractal Mountains and Other Types of Scenery
"Ew
while( key !'= 27)
{
int midvpos=(vpos1+vpos2)/2+random(range)-range/2;
// Call recursively with random perturbation:
lineseg(hpos1,(hpos1+hpos2)/2,vpos1,midvpos,depth-1,
range/2);
lineseg((hpos1+hpos2)/2,hpos2,midvpos,vpos2,depth-1,
range/2);
HE BE BE BE B®
493
"Build Your Own Flight Sim in C++
Three-Dimensional Fractals
Can the same thing be done in three dimensions? Is it possible to create a three-
dimensional fractal object in the same way that we just created the two-dimensional
outline of a mountain range?
Sure it is. And the principle by which we would create such a three-dimensional
fractal object is exactly the same as we used to create the two-dimensional mountain
range: We start with a simple object and break it apart recursively into jagged but self-
similar pieces. To draw the mountain range, we started with a line and broke it apart
into line segments. To generate a three-dimensional mountain, we'll start with a pyra-
mid (which, as noted earlier, can be thought of as an extremely simplified mountain)
made of four triangles, then break each of those triangles recursively into smaller and
smaller triangles. The more triangles we break the pyramid into, the more realistically
fragmented the surface of the mountain will look. However, we don't want to break it
into too many triangles, because we’ll need to manipulate those triangles in real time in
our flight simulator.
It won't be necessary to write a program from scratch to demonstrate fractal moun-
tain building. We'll borrow the LIGHTDEM program from Chapter 12 and rewrite the
LIGHTDEM.CPP module to generate a fractal mountain when the user presses the
key. Much of the remaining program will stay exactly the same, since it’s
already designed to display light-sourced, polygon-based objects. The major difference
is that, instead of loading the object data from the disk, the program will generate that
data itself. Therefore, our polygon class will have to be touched to allow for the
dynamic construction of its vertices. We'll initially load object data from the MOLE-
HILL. WLD file to give the program the pyramid that it’s going to make the mountain
out of. The loading module will create an extra blank object for which to store the frac-
tal object version. The World module will likewise be changed to prevent us from pro-
cessing the blank object. And we'll make use of the PolygonList class to cull visible
polygons and perform a z_sort(). We'll call our mountain-building program
FRACDEMO.
A 3D Mountain Generator
Figure 14-6 illustrates the algorithm that we'll be using. In Figure 14-6(a), we see a
pyramid. In Figure 14-6(b) we see the same pyramid with each triangular side broken
into four smaller triangles. An additional vertex has been added in the middle of each
of
of the three edges each of the triangular sides and three new edges have been added
to connect these vertices. It’s not enough simply to split each triangle into smaller tri-
angles, though. The vertices must be perturbed from their original positions to give the
triangles the jagged, irregular look of rock cliffs, as in Figure 14-6(c).
494 ®
Fractal Mountains and Other Types of Scenery m
"ow
Most of this work will be done by a function that we'll call fractalize(). We'll place
this function in the main module of our program, which we'll call FRACDEMO.CPP.
The prototype for the fractalize() function looks like this:
HE BE BE
BEB 495
—-
~~ Build Your Own Flight Sim in C++
"EE
EEE
itself recursively it will add 1 to this number. This lets the function determine when it
has reached the maximum level of recursion, which we'll represent with the constant
MAXLEVEL, defined in the header file FRACDEMO.H. Although we can theoretically
set this constant to just about any positive nonzero value, a maximum recursion level
of 3 probably represents the best compromise between a large and unmanageable
number of polygons at the one extreme and an unrealistically simplistic mountain at
the other.
We'll need to create an array of structures for this function’ internal use. As in any
recursive function, these structures will be created on the stack and generated anew for
every recursive call. Here are the declarations:
vertex_type newvertl[é6l];
The newvert array of type vertex_type will allow us to create a temporary array of
vertices to pass to the next level of recursion.
Our first order of business in this function will be to perturb the vertices of the poly-
gon that has been passed to the function as a parameter. This is a relatively simple mat-
ter of adding a random value toeach of the coordinates of those vertices. However, this
simple matter will turn out to have some unexpected complexities. To make the coding
easier, we'll start out by creating some vertex pointers and long integer variables that
will contain the initial coordinates of the vertices:
496 =
Fractal Mountains and Other Types of Scenery m
mm
object, these common vertices will be torn apart. A common vertex held by three dif-
ferent polygons may be given a different set of random coordinates for each polygon.
How do we guarantee that common vertices end up the same for each polygon that
shares them? There’ a simple trick we can use. It involves the srand() function.
Pseudo-Random Numbers
In Borland C/C++, the srand() function reseeds the random-number generator. (Since
it is an ANSI function, other compilers should have this function.) You may be aware
that the numbers generated by the random-number functions that come with most C
compilers aren really random. They are pseudo-random. Pseudo-random number
generators start with a number called the seed, usually a long integer, and perform
mathematical operations on it to produce a sequence of numbers that give the appear-
ance of randomness (and which, for most purposes, are close enough to being random
that the difference is insignificant). However, if you start the random number genera-
tor again with the same seed, it will produce the same sequence of random numbers.
The srand() function passes a seed to the random-number generator and starts a
new sequence of random numbers based on that seed. Thus, if we seed the random-
number generator with the same number each time we want to randomly perturb a
vertex held in common by several polygons, it will give us back the same random
numbers on each occasion, producing the same perturbed coordinates for the same
vertices. But how do we guarantee that we'll pass the same seed to the random number
generator each time we perturb the same vertex? We'll need to use some characteristic
of that vertex to produce the seed. And the x,y, and z coordinates of the vertex are the
perfect candidates.
Here's the line of code we'll use to produce a random-number seed based on the
coordinates of the first vertex of the polygon passed to fractalize():
HE BE HE EE B=
497
yd
Build Your Own Flight Sim in C++
BB li
A
sl
VARIATION set to 10, this will give us a random number from 0 to 9. We then sub-
is
498 ®
Fractal Mountains and Other Types of Scenery
store these vertices, along with the three existing vertices, in the fractal object. If we
RAR
need to split the triangle further, we'll store them in the newvert|] array.
if (rlevel<MAXLEVEL)
vptr = newvert;
else
vptr = fracObject->GetVertexPtr(vertnum);
If rlevel hasn't reached the maximum level of recursion allowed by the MAXLEVEL
constant, we point the vertex_type pointer, vptr, to the newvert array. If we have
reached that maximum level, vptr is set equal to a vertex pointer from the fractal
object. The two undeclared variables, fracObject and vertnum, are both global scope.
fracObject isset to point to the blank world-fractal object and vertnum keeps track of
our place in this fractal object's vertex array. Later, we'll increment vertnum by 6 to indi-
cate that we've added six new vertices to the array. For now, however, we'll continue
placing vertices into the object array starting at vertnum.
Vertices are stored by working our way clockwise around the triangle from the first
vertex. The first vertex of the old triangle is simply copied verbatim to the first vertex
structure in the vptr array:
vptrLO01.Llx=x0;
vptrCLOl. ly=y0;
vptrC0l.lz=20;
then create a second vertex for the new triangles at a coordinate position
We'll
halfway between the first and second vertices of the old triangle:
vptrL11.Lx=x0+(x1-x0)/2;
vptrL1l. ly=y0+(y1-y0)/2;
vptrL11.lz=20+(21-20)/2;
We'll repeat this process for the remaining four vertices of the new triangles:
vptrL21.lx=x1;
vptrL21.ly=y1;
vptrC2l.lz=21;
vptrL3]1. Lx=x1+(x2-x1)/2;
vptrL3]1.ly=y1+(y2-y1)/2;
vptrL3l.lz=21+(22-21)/2;
vptrL4d. lx=x2;
vptrL4l. ly=y2;
vptrL4l.lz=22;
vptrL51. Lx=x2+(x0-x2)/2;
vptrL5]1.ly=y2+(y0-y2)/2;
vptrL5]1.lz=22+(20-22)/2;
H BE BE
EH 499
LL Build Your Own Flight Sim in C++
"EE EE EE
triangles and pass them recursively, one by one, back to the fractalize() function. So
first we'll check the recursion level:
if (rlevel<MAXLEVEL) {
If we are going to recurse another level, we need to define a temporary storage space
for a temporary triangle.
Polygon newpoly;
The newpoly variable of type Polygon will allow us to temporarily store a polygon
which will contain an array of pointers to the vertices in the newvert array. It is a
pointer to this Polygon object that will actually be passed in the recursive call.
Next we'll need to provide some memory for an array of vertex pointers that will be
pointed to by the vertex field in the newpoly variable. The Polygon class has been
changed and provides a function for dynamic construction of the vertex array.
int Polygon: :MakeVertexArray(int vnum)
{
if( vertex)
delete [J] vertex;
vertex = new vertex_type *[Lvnuml;
number_of_vertices = vnum;
return (vertex?0:-1);
}
With that function added to the Polygon class, the newpoly can now allocate some
memory:
if( newpoly.MakeVertexArray(3) )
return;
Now we can create a brand new triangle simply by pointing the vertex pointers in
this array at three of the vertices in the newvert array, as shown in Figure 14-7:
newpoly.SetVertexPtr(0,&newvert[01);
newpoly.SetVertexPtr(1,&newvert[11);
newpoly.SetVertexPtr(2,&newvertl[51);
The SetVertexPtr() function is an additional inline function in the Polygon class. It
assigns the Polygon::vertex array element indexed by the first parameter to the value of
the second parameter:
inline void SetVertexPtr(int vnum, vertex_type *vptr) { vertex[Lvnuml = vptr; }
Here comes the recursive call, as we pass this new triangle back to the fractalize()
function:
fractalize(&newpoly,rlevel+1);
There are three more recursive calls yet to come, as we create three more triangles
and pass them back to fractalize():
Fractal Mountains and Other Types of Scenery m
mm
Triangle Verfices
0
Always assigned in
1
015
clockwise direction 2 1,23
3 53,4
4 513
newpoly.SetVertexPtr(0,&newvert[11);
newpoly.SetVertexPtr(1,&newvertl[21);
newpoly.SetVertexPtr(2,&newvert[31);
fractalize((Polygon *)&newpoly,rlevel+1);
newpoly.SetVertexPtr(0,&newvert[51);
newpoly.SetVertexPtr(1,&newvert[31);
newpoly.SetVertexPtr(2,&newvertL41);
fractalize((Polygon *)&newpoly,rlevel+1);
newpoly.SetVertexPtr(0,&newvert[51);
newpoly.SetVertexPtr(1,&newvertl11);
newpoly.SetVertexPtr(2,&newvert[31);
fractalize((Polygon *)&newpoly,rlevel+1);
If we haven't reached the maximum recursive level yet, that’s all we need to do. But
if we have, theres more work ahead:
}
else {
return;
polyptr->SetVertexPtr(0,&vptrL01);
polyptr->SetVertexPtr(1,&vptrL11);
polyptr->SetVertexPtr(2,&vptrL51);
polyptr->SetOriginalColor(polyColor);
polyColor is a global variable containing the color of the polygon from the object
being fractally converted. SetOriginalColor() is an inline function of polyBase defined
as:
After storing the first polygon information, we do the same for the other three tri-
angles:
polyptr = fracObject->GetPolygonPtr(polynum+1);
if( polyptr->MakeVertexArray(3) )
return;
polyptr->SetVertexPtr(0,vptr+1);
polyptr->SetVertexPtr(1,vptr+2);
polyptr->SetVertexPtr(2,vptr+3);
polyptr->SetOriginalColor(polyColor);
polyptr = fracObject->GetPolygonPtr(polynum+2);
if( polyptr->MakeVertexArray(3) )
return;
polyptr->SetVertexPtr(0,vptr+5);
polyptr->SetVertexPtr(1,vptr+3);
polyptr->SetVertexPtr(2,vptr+4);
polyptr->SetOriginalColor(polyColor);
polyptr = fracObject->GetPolygonPtr(polynum+3);
if( polyptr->MakeVertexArray(3) )
return;
polyptr->SetVertexPtr(0,vptr+5);
polyptr->SetVertexPtr(1,vptr+1);
polyptr->SetVertexPtr(2,vptr+3);
polyptr->SetOriginalColor(polyColor);
The vertnum and polynum global variables have been initialized to O at the start of
the fractal conversion in FRACDEMO.CPP, so we'll need to increment them to reflect
the new position in their respective arrays. For the same reason, the number_of_poly-
gons and number_of_vertices fields for fracObject are updated:
502 ®
Fractal Mountains and Other Types of Scenery m
mm
vertnum+=6;
polynum+=4;
fracObject->SetNumVertices(vertnum);
fracObject->SetNumPolygons(polynum);
And that, except for a couple of closing brackets, finishes the fractalize() function.
r=RandomVari()/rlevel;
x2+=r;
r=RandomVari()/rlevel;
y2+=r;
r=RandomVari()/rlevel;
z2+=r;
504 m
Fractal Mountains and Other Types of Scenery
EN
fractalize((Polygon *)&newpoly,rlevel+1);
newpoly.SetVertexPtr(0,&newvert[51);
newpoly.SetVertexPtr(1,&newvertl[3]);
newpoly.SetVertexPtr(2,&newvertl41);
fractalize((Polygon *)&newpoly,rlevel+1);
newpoly.SetVertexPtr(0,&newvert[51);
newpoly.SetVertexPtr(1,&newvertC11);
newpoly.SetVertexPtr(2,&newvertl[31);
fractalize((Polygon *)&newpoly,rlevel+1);
// newPoly is destroyed
else {
return;
polyptr->SetVertexPtr(0,vptr);
polyptr->SetVertexPtr(1,vptr+1);
polyptr=>SetVertexPtr(2,vptr+5);
polyptr->SetOriginalColor(polyColor);
polyptr = fracObject->GetPolygonPtr(polynum+1);
if( polyptr->MakeVertexArray(3) )
return;
polyptr=>SetVertexPtr(0,vptr+1);
polyptr=>SetVertexPtr(1,vptr+2);
polyptr->SetVertexPtr(2,vptr+3);
polyptr->SetOriginalColor(polyColor);
polyptr = fracObject->GetPolygonPtr(polynum+2);
if( polyptr—->MakeVertexArray(3) )
return;
polyptr=>SetVertexPtr(0,vptr+5);
polyptr->SetVertexPtr(1,vptr+3);
polyptr->SetVertexPtr(2,vptr+4);
polyptr->SetOriginalColor(polyColor);
polyptr = fracObject->GetPolygonPtr(polynum+3);
if( polyptr->MakeVertexArray(3) )
return;
polyptr->SetVertexPtr(0,vptr+5);
polyptr->SetVertexPtr(1,vptr+1);
polyptr->SetVertexPtr(2,vptr+3);
polyptr->SetOriginalColor(polyColor);
vertnum+=6;
polynum+=4;
fracObject->SetNumVertices(vertnum);
fracObject->SetNumPolygons(polynum);
.ee Hh
Build Your Own Flight Sim in C++
There’ just one flaw with our fractalize() function. Because of the way we reseed the
random-number generator with the coordinates of the vertices before perturbing those
BEEN
vertices, fractalize() will always produce the same mountain from the same pyramid.
And, since we're using the data in MOLEHILL.WLD as the starter pyramid for the frac-
tal mountain, we'll always start with the same pyramid. Which means we can only gen-
erate one mountain—over and over again.
A fractal mountain generator that can only generate one mountain is a pretty bor-
ing piece of software. We need to introduce some more randomness into the process
of mountain building, but we must do so in such a way that the common vertices
aren't torn apart, which is why we reseed the random-number generator in the first
place.
The solution that we'll use is to write a function called InitRand(). This function will
take the pyramid loaded from the MOLEHILL.WLD file and make random changes to
all five of its vertices. This way, we'll never start with the same pyramid twice. Here’ the
prototype for InitRand():
void InitRand()
As you can see, InitRand() takes no parameters and returns no values. If you
guessed that InitRand() is a pretty simple function, you're right. It’s essentially a small
for loop that iterates through the five vertices in the pyramid and perturbs them ran-
domly, using the same method that we used to perturb vertices in fractalize(), minus
the call to srandRand():
World Changes
As mentioned previously, a few changes were made to our World class in order to cre-
ate our polygon on the fly. The first of these was made to the World::LoadPoly() func-
tion in WORLD_FR.CPP. The number of objects is incremented so that an extra blank
object will be created.
Fractal Mountains and Other Types of Scenery m mm
number_of_objects = _pf.getnumber() + 1;
After allocating the Object array, the object initializing loop is adjusted so that the pro-
gram won't try and read in data for an extra object.
int Lastobj = number_of_objects -1;
for (int objnum=0; objnum< Llastobj; objnum++) {
objClastobjl.Cloak();
Thus, the Object class gains functions to initialize the polygon and vertex arrays,
and to control the visibility of the object. Since this is the third field used for Boolean
logic (flag), all three characteristics are combined into a multiple bit field. The follow-
ing constants are defined for testing:
const int CONVEX 1;
const int UPDATE 2;
const int VISIBLE = 4;
The previous inline functions are changed and two new visibility functions are added
to the class in POLY_FR.H:
int isConvex()
{ return ((flags & CONVEX) !'= 0); }
void Complete)
{ flags &= (UPDATE); J}
int NeedsUpdate()
{ return ((flags & UPDATE) !'= 0); 1}
void Uncloak()
{ flags |= VISIBLE; }
void Cloak()
{ flags &= ("VISIBLE); 1}
int isVisible()
{ return ((flags & VISIBLE) != 0); 1}
The changes ripple throughout the object functions, with the Object::Load() function
being adjusted to the new style, in LOADP_FR.CPP:
flags = (UPDATE|VISIBLE);
if( (_pf.getnumber())!= 0)
flags |= CONVEX;
The Object::Init() function in POLY_FR.CPP was created to allow dynamic alloca-
tion of the two Object arrays, polygon and vertex. The first time this is called, both
pointers are equal to 0 (as set in the constructor), but on subsequent calls, the arrays
are deleted so that no memory leaks occur.
507
~~
Build Your Own Flight Sim in C++
polygon_Llist.GetPolygonPtr(i);
ew
if (objptr=>isConvex()) {
if(!'pptr->backface()) {
CalcPolygonColor(pptr);
pptr=>DrawPoly(screen);
else
pptr=>DrawPoly(screen);
}
The complete World::Draw() function can be seen in Listing 14-4. A compiler directive, DEPTH-
SORT, allows you to use the drawing method from the light demo program without using the poly-
if(objptr=>isVisible()) {
CalcPolygonColor(pptr);
pptr=>DrawPoly(screen);
else
pptr=>DrawPoly(screen);
continued on next page
~~ Build Your Own Flight Sim in C++ "EEE EN
Now, we can set up our fractal object. First, we get a pointer to the current object
and multiply its polygon count by the magic number.
Object * objptr =world.GetObjectPtr(curobj);
int polyNeed = objptr->GetPolygonCount()* Llpow(4, MAXLEVEL);
polyColor = pptr=>GetOriginalColor();
fractalize(pptr,1);
After the loop is completed, set the flags of the objects to hide the original object
and show the fractal object.
objptr->Cloak();
fracObject->Uncloak();
That covers the essential workings of FRACDEMO. There’ a copy of the program in
the FRACTAL directory that came on the disk with this book. You can run it by typing
FRACDEMO at the DOS prompt. Initially, you'll see a pyramid spinning in space. (It’s
spinning rapidly because the extra polygons created by the fractalization process are
about to slow it down somewhat.) You can control the spinning of the mountain in the
same way that you controlled the spinning objects in the earlier OPTDEMO and
LIGHTDEM programs, using the numeric keypad. To fractalize the pyramid, press
the key.
Presto! The pyramid becomes a mountain! You'll notice that if
you rotate the moun-
tain so that you can see it from underneath, it
appears to vanish. That's because we
haven't included a bottom to the mountain. You can see right up inside of
it,
where the
backfaces of the polygons are. Since we removed these polygons with backface
removal, they're invisible. The mountain will reappear when its topside rotates into
view again. To exit the program, hit (ESC).
tal clouds to an otherwise blue sky would also up the realism ante of your simulator.
How do you create fractal shorelines and fractal clouds? Thats for you to figure out.
Once
you start thinking in fractal terms, you'll find that fractal algorithms are not ter-
ribly difficult to generate. And if you become really ambitious, you can turn your
hand to real-time fractals, which are generated on the fly, while your simulator is run-
ning. These will greatly decrease the amount of memory required to store fractalized
polygons, since only a few parameters will be necessary to describe the fractal object,
and will eliminate a lot of otherwise time-consuming processing of polygons and ver-
tices. Obviously, real-time fractals require highly optimized fractal generating code, but
many of the optimization tricks that we've described in this book
will help you speed
up your code.
Before you know it, you'll be looking around for fractals every time you leave your
house—and finding ways to put those fractals into your computer games.
Sound
Programming
Thus far, you've learned some maw, many useful algorithms, and many
graphics techniques. We will soon have a running 3D flight simulator, but there is an
ingredient missing—it doesn’t make any noise yet. This chapter will begin with some of
the history and theory behind PC sound, and then we'll show how to program exciting,
realistic sound for flight simulators and other programs.
Some History
When IBM introduced the PC, it came with a built-in, low-quality speaker. This was
connected to an inexpensive amplifier, making PC sound incapable of high fidelity or
reasonable output volume. From a programming standpoint, it was CPU-intensive
(tied up the processor), which is the reason why it was mostly restricted to error
beeps. As result, games with good sound effects were restricted to other computers.
a
In 1987, Adlib introduced the first important sound card to the PC world. The card
had a Yamaha OPL-2 FM synthesizer chip on it, which could generate nine voices
simultaneously. Nine voices meant nine sounds. They could harmonize or be disso-
nant. To a musician, that meant enough sounds to produce a chord, and to a
~~ Build Your Own Flight Sim in C++
—
programmer, it provided enough to make sound effects which rivaled the popular
game machines.
In the following years, Creative Labs developed the first Sound Blaster, which was
released in 1989. They put a Yamaha OPL-2 chip on it, for Adlib compatibility, and
added a Digital Signal Processor (DSP) and a Digital-to-Analog Converter (DAC) for
digitized sound effects. The combination proved very effective. Since 1990, Creative
Labs has dominated the sound-board market, which has expanded to include nearly all
PC computers sold for the home today.
Back in the days when EGA graphics were state-of-the-art, the FM synthesizer chip
was considered adequate for producing music and sound effects. But in modern con-
sensus, the FM chip is better suited for music duty, with sound effects handled by the
DSP. The DSP can play any prerecorded digitized sound—such as voices, explosions, or
of
the roar a jet engine.
Sound Theory
Before jumping right into how you program the Sound Blaster, let’s cover some useful
background information which will help you to better understand sound program-
ming. First, we'll discuss basic sound theory, touching briefly on the mathematics of
waves, and finally in sound programming, we'll introduce FM synthesis and digital
sampling.
Sound is created when physical objects vibrate. Examples of sound-producing
objects are vocal chords, guitars, blocks of wood, and loudspeakers. As the object's sur-
face vibrates, it compresses and expands the air molecules around it proportionally.
This wave propagates to your ear, where it induces microscopic hairs to vibrate, which
in turn fire off signals to your brain. When the rate of vibration (frequency) is between
approximately 20 and 20,000 cycles per second it is perceived by the human ear and
brain as sound.
The unit of measurement for cycles per second is the Hertz, which is abbreviated Hz.
Figure 15-1 shows the parts of a wave. The speed of the wave is a function of the
does not travel through a vacuum—so, Darth
matter through which it travels. Sound
Vader never heard the explosion of the Death Star. But, in dry air at sea level at 20
degrees centigrade, sound travels at almost 770 miles per hour. Given this, the wave-
length is calculated as the speed divided by the frequency. This ratio of speed to fre-
quency implies that lower frequencies have longer wavelengths; as the frequency
increases, the wavelength becomes shorter. How does all this affect human perception?
Higher frequencies are perceived to be higher in pitch. Higher amplitudes are perceived
to be louder.
Sounds in the real world are complex and composed of many waves. In mathemat-
ical terms, this is represented as the sum of a finite number of sine waves, functions
which generate a curve endlessly cycling between -1 and 1. Back in Chapter 9 when
Sound Programming
CL
the FIX.CPP file was created, this same sine characteristic was exhibited in our fixed-
point table for the sine function. If graphed, the table would show 256 (the NUM-
BER_OF_DEGREES) points between 512 and -512, fixed point values for 1 and -1
(after the SHIFT).
As shown in Figures 15-2(a) through (d), real-world sounds look much more com-
plicated than sine waves. Part A shows a plot of y = 2sin(x); part B shows y = sin(2x) / 2;
part C shows y = sin(4x) / 2; and part D shows the sum of all three waves, y = 2sin(x)
+ sin(2x) / 2 + sin(4x) / 2. Don't be frightened by the math—its not important to
understand how the graphs are made, but it is significant that we can add waves
together. This additive property is a concept exploited in both FM synthesis and digi-
tized sound.
As in mathematics, we've drawn the sine wave from 0 to 2P radians. From Chapter
2, we might remember that radians and degrees are measurements of a circle and may
be used interchangeably. So, in essence, the sine graph spans from 0 to 360 degrees. As
mentioned above and depicted in Figure 15-2(d), sine waves are additive. So if two or
more sine-wave sounds are playing at the same time, you can simply add the y value
for each x together, and plot one composite. This is simple enough, but to use that
knowledge, the component sine waves must be extracted from a composite. And that
isn't easy—either conceptually or computationally.
Any plot of a wave, such as Figure 15-2, uses the y axis to represent amplitude. But
what is the x axis? Its time. So, such a plot can be said to be a time-domain plot. In
HE BE BE EE B®
517
~~" Build Your Own Flight Sim in C++
HE EE BE
BEB
(©
y=sine). (d) y=2sin(x) +
ans int]
%
contrast, a frequency-domain plot shows the amplitude of each (sine wave’) frequency,
rather than the amplitude at each point in time. The Fourier Transform converts time-
domain data into frequency-domain data. An explanation of this method is beyond the
scope of this chapter. The important point to realize is that a relationship exists
between the time-domain and frequency-domain. Many useful algorithms for pro-
cessing sound rely on this relationship.
Sound Programming m
"a.
Figures 15-3(a) and (b) show a composite of three sine waves in both the time- and
frequency-domains. Figures 15-3(c) and (d) show a square wave, also in both domains.
Notice in 15-3(d), how a square wave is comprised of an infinite number of sine
waves. In real life, there is no such thing as a true square wave. All amplifiers, cables,
and speakers will cut off everything above a certain frequency—each acts as a band-
pass filter.
Build Your Own Flight Sim in C++
~
FM Synthesis
FM synthesis is the most common method used to produce computer music. This is
because it is well suited to generate music—requiring inexpensive hardware, little
memory, and few CPU cycles.
First, define a few terms. Synthesis is the combination of elements into some-
let’s
thing new. Sound synthesis, then, is algorithmically making sounds. This is funda-
mentally different from digitizing existing sound, as we'll discuss in the next section.
FM stands for Frequency Modulation. To understand that term, we need to look at
modulation synthesis in general. In modulation synthesis, a modulator is used to alter
a carrier in some way. Both are usually sine waves, although the output is not sinu-
soidal (shaped like a sine wave). The word modulation itself denotes that one wave is
systematically changing the other. One notable use of this technique is in the radio
industry, where you tune in your favorite station on the FM or AM dial. Amplitude
Modulation (AM) is less complicated than FM, so let’s look atit first.
In AM, the amplitude of the modulator is used to alter the amplitude of the carrier.
(See Figure 15-4.) The problem with AM is that it doesn't really sound very good.
Except for old organs, AM can’t reproduce musical instruments very well. FM sounds
can be much more interesting and exciting, which is why we use FM for computer
music synthesis.
In FM, the amplitude of the modulator is used to alter the frequency of the carrier.
As the value of the modulator increases and decreases, the frequency of the carrier
increases and decreases. And as the frequency increases, the wavelength gets smaller.
It’s surprising how this simple method can produce an infinite variety of harmonically
is
rich sounds. (See Figure 15-5.) Notice how the amplitude of the output not affected
by the amplitude of the modulator.
Our task will be to generate a waveform, but that in itself won't do. We don't want
to just switch the wave on when a note is hit, and then switch it off when its done. This
won't sound like music because real instruments don’t go from silence to full volume
instantly—they swell and fade.
To mimic the surge and decline of a sound, an envelope can be applied which will
constrain the volume in a way that’s more pleasing. On the Yamaha OPL-2 chip, the
envelope has four parameters: attack, decay, sustain, and release. These are the rate at
which the sound attains its loudest level (attack), the rate at which it falls to a level
which it holds for most of its duration (decay), this level itself (sustain), and the rate
the level falls to silence when the sound is done (release).
There are actually two kinds of sounds in OPL-2 FM synthesis. Continuing sounds
will hold the sustain level until they're manually cut off. Diminishing sounds wind
down to silence on their own. As you can see in Figure 15-6, the sustain level has a dif-
ferent meaning for each sound type.
FM synthesis can be used to generate satisfactory music. Unfortunately, most soft-
ware fails to achieve this because FM programming is so tricky.
520 m
Sound Programming
"Ew
HE BE BE BE B®
521
Build Your Own Flight Sim in C++ "EEE EN
Digitized Sound
As discussed earlier, sound consists of sine waves. These waves are contiguous—that is,
they vary continuously over time. Even a section of a wave spanning a short period of
time contains an infinite number of values of infinite precision. We once again meet up
with the problem of precision and quality versus computer time and space (see Chap-
ter 9 for a discussion on the problem). Computers require that such analog sounds be
to
sampled, or digitized, in order be stored and then reproduced. A digital sound
number and each
is dis-
sample can only be
crete, meaning that there is a finite of samples
one of a finite number of values. Figure 15-7 shows a contiguous (analog), and a dis-
crete (digitized) wave.
Build Your Own Flight Sim in C++
"EN EE EE
If 8 bits are used to represent each sample, we can represent amplitudes from -128
to +127. This can cause some problems. All values larger than +127/-128 will be
clipped to +127/-128. This sounds so awful that it is to be avoided at all costs. Note: If
you're mixing sounds together, you must keep the sum beneath this limit as well!
If a limit on dynamic range causes
a fairly serious problem, then the fact that we're
discretely sampling is even worse. As we saw above, discretely sampled sounds are not
contiguous in the time domain. We have a sample for T=12 and T=13, but not for
T=12.7. Obviously, in playing back the digitized sound, the air doesn't return to its sta-
tic pressure level in between each sample. What happens?
It turns out the DAC will hold each voltage level until it’s given another. If we send
each sample to the DAC at the correct time, Figure 15-8 shows the output.
You may notice that the raw wave in Figure 15-8(a) looks very “square.” This is due
to the additional high frequencies which were added to the wave inadvertently during
conversion to analog. It’s an artifact caused by converting a discretely sampled wave to
a contiguous waveform. In Figure 15-8(b), this wave is filtered in the analog domain.
If the wave comprises only those frequencies below half the sampling rate, then it is a
perfect reconstruction of the original wave.
Let’ state this more formally. To capture a waveform containing frequencies up to E
you need to sample
lower
it
than
at a rate of at least 2F This is called the Nyquist rate. Sampling at
the Nyquist frequency will cause aliasing, which is when high-
any rate
frequency sounds appear as one or more lower frequencies. The cause of the problem
is that you must have two samples per wave. Anything less, and you capture an alias of
524 m
Sound Programming
"=.
is
the wave at a lower frequency. (See Figure 15-9). The original wave dashed. The two
dark points are the samples, and the solid line is the aliased wave.
The stair-step effect in a line as seen in Chapter 6 and propellers which appear to
slowly turn backwards are other examples of aliasing. The frequency (resolution) of the
is
screen is too low to display the line, and the frame rate of the human eye too low to
see a 15,000-RPM propeller.
Hai
526 ®
Sound Programming
"Ew
As we write the code we'll use in the flight simulator, we'll presentthe details of pro-
gramming the Yamaha OPL-2 FM synthesizer chip at the register level. The OPL-2 con-
tains 18 operators which combine to make up the nine 2-operator (modulator and
carrier) voices. Each operator contains data describing its sound attributes: volume,
attack, sustain, etc. The term operator refers to registers which manipulate a unique
channel of sound. In other documentation, operators are sometimes called operator
cells (Creative Labs) or slots (Yamaha), but here we will stick with the simple name,
control the
operator. We'll tackle the code from bottom to top: first, we'll write code to
FM chip, and later we'll write an interface module, which will manage the sound
details for the rest of the program. This interface has a similar purpose as the Event-
Manager class of Chapter 5.
The code in this chapter uses lots of macro identifiers to take the place of hardware
specifications. If you decide to experiment, these identifiers can be tailored to your own
specific sound card. The original SoundBlaster cards have one OPL-2 FM synthesis chip
at port addresses 288h and 289h. The original Sound Blaster Pro card (CT-1330) had
two OPL-2 FM chips to achieve stereo capability. The newer Sound Blaster Pro (CT-
1600) contained an OPL-3 FM chip synthesis chip. This chip is compatible with the
OPL-2 chip and added another 18 operators. Each of the stereo speakers were given
their own port addresses—the left channel FM chip at 280h and 281h, and the right at
282h, 283h as seen in Table 15-1. In the case of stereo, 288h and 28%h will access both
chips simultaneously, thus maintaining programming compatibility with the other
cards. The 16-bit cards were expanded to 20 voice, 4-operator FM synthesizers, and are
still backwards compatible. The 32-bit cards contain the same capability in addition to
a newer synthesizer chip (EMU8000) for a different route to musical generation. The
common denominator for compatibility is the single OPL-2 FM chip, so it is our
default.
There will be no SoundCard, Voice, or FM-synth class, although all the characteris-
tics and elements to create such classes are here. Basically, this is C code which we will
compile as CPP files so that the function names will be properly mangled for C++ link-
ing. This allows us to replace these functions in the Appendix for an example of digi-
tized sound and FM music without having to completely gut a C++ class. As an
alternative, we could write a header file to declare these prototypes as extern “C” func-
tions, then name all our files as .C files. This is a matter of preference. For the more
527
Corsa
~~ Build Your 0Own Flight Sim in C++
— ~~ # WH WH EH
EH
ambitious programmer, we'll suggest some ideas for organizing classes as we describe
each function. All of the code in this chapter can be found in the FSIM directory on the
distribution disk.
The words REGSEL, SHORT, DATA, and LONG are all macro identifiers from #define
statements placed at the top of the file. REGSEL and DATA are both port addresses for
the sound card, respectively 0x388 and 0x389. The other two, SHORT and LONG are
magic numbers fed to the WasteTime function. This is a function for delaying the CPU
from further processing of the program by making it do specific code designed to
waste time. The outp() function takes two parameters, the first is
the port address, and
the second is the value to be written. This is a non-ANSI function but is surprisingly
the same and available in most compilers.
Unfortunately, the FM chip is slow. After writing the register number to the first port
to select the register, we must wait about 2.3 microseconds. After writing to the second
register to output a value, you must wait about 23 microseconds. To a modern proces-
sor, this is a long time! These delays are required because the chip is internally very
slow; even the sluggish ISA bus on the PC is capable of feeding it data faster than the
chip can processit.
To time the delays after writing to the FM ports, a delay function is used. The
WasteTime() function will loop in code which reads the status port of the FM chip. We
do not do anything with the status information; we're reading the
port because it
takes time:
static
{
void WasteTime(word numreads)
while (numreads--)
{
528 m
Sound Programming m
mm
inp(STATUS) ; //reading the status port causes
} //a timed delay
}
Why not just add two numbers in a similar loop? Why port reads? There are two
really good reasons: compiler independence and hardware independence. First, opti-
mizing compilers are free to rewrite your code, in order to make it execute faster. If you
simply add numbers together in a big loop, the compiler will often eliminate the loop.
There goes your delay. As for the hardware problem, the end-user processor speed is
never really known. But, a little trick can be exploited—the AT-bus runs at 8 MHz.
Therefore each read or write using the bus will take at least 125 nanoseconds (Ns).
Determining the number of reads to cause a desired delay isn't nearly so simple. We
resorted to the empirical method—trying and measuring. For 3.3 msecs, the loop
needs to be executed 7 times, hence SHORT is
to supply 23 msecs, the loop is read 36 times.
defined as 7. For the LONG definition
Both of these functions, Write and Waste Time, are static, and therefore only avail-
able to the SBFM.CPP module. In a class, these functions would most certainly be
made private to the class.
The FM chip has two timers on it, which were intended by Yamaha to be hooked up to
IRQ lines in the computer. Neither Adlib, nor Creative Labs, actually connected them
to IRQ lines. But the terminology stems from this.
it
First, we reset one of these timers and start ticking. Then we read the status port.
If any bits are set right now, then there’s definitely no FM chip present.
int res;
Write (IRQ_TIMER_CNTRL, RESET_TIMER); //reset timer
Write(IRQ_TIMER_CNTRL, IRQ_ENABLE); //enable timer IRQ
continued on next page
res = inp(STATUS);
Fr :
— #1
}
return(false); //if not "timed out" fail
return(true); >
Note: Time-critical code like this is normally executed with the CPU% interrupt flag
clear. Clearing the interrupt flag has the effect of disabling interrupts. This is done so
that some hardware interrupt does not happen and steal some CPU time, thereby
invalidating the test. To implement such control of the interrupt flag, we could wrap
the code with one line assembler directive using the Borland C++ ASM keyword and
the 8086 cli and sti instructions, or the enable and disable C functions. However, in
practice, such interrupt problems are very rare. And also, in this particular case, we
want the timer to expire. A hardware interruption which delays our immediate polling
of the FM status does not invalidate our test, since that’s what we want
The completed code of the shfm_PresenceTest() function is in Listing 15-1.
to
happen.
Sound Programming m
NN
|
}
return (false); //if they are, then fail
}
return(false); //if not "timed out" fail
One of the things that might strike you as you look at the code is that there are a
slew of capitalized identifiers used in this function. These are all defined at the top of
the module. For your perusal, here are the defined identifiers from the SBFM.CPP
module:
#define WAVESELECT_TEST 0x01 //1 per chip
#define TIMER1 0x02 //1 per chip (80 @sec resolution)
#define TIMER2 0x03 //1 per chip (320 asec resolution)
#define IRQ_TIMER_CNTRL 0x04 //1 per chip
#define CSM_SEL 0x08 //1 per chip
#define SND_CHAR 0x20 //18 per chip (one per operator)
#define LEVEL_OUTPUT 0x40 //18 per chip (one per operator)
#define ATTACK_DECAY 0x60 //18 per chip (one per operator)
#define SUSTAIN_RELEASE 0x80 //18 per chip (one per operator)
#define FNUML Oxa0 //9 per chip (one per operator pair)
continued on next page
~~ - Build Your Own Flight Sim in C++
Such a function stops all sound and sets the registers to some reasonable defaults. First,
to stop the sound, all nine voices must be turned off. So, we go through a loop:
for (int channel=0;channel<NUMVOICES;channel++) {
The organization of registers on the FM chip is easiest understood by using the fol-
lowing table as the base of operations:
static ofst_op1[] = {0x00, 0x01, 0x02, 0x08, 0x09, Ox0a, 0x10, 0x11, 0x12};
This table provides an offset register number for each of the nine voices, also referred
to as channels. Each voice is composed of two operators, which can be thought of as
the modulator and carrier. By adding three to each of the base values, the second
operator array can be generated:
static ofst_op2L] = {0x03, 0x04, 0x05, OxOb, OxOc, Ox0d, 0x13, Ox14, 0x15};
These two arrays work in tandem such that an index into both arrays yields that
voice’ two operators. For example, the two base values for voice six are 0x0a and 0x0d
5532 ®
Sound Programming
"Ew
Table 15-2 M FM registers
Register Address (in hex) Acts On Attribute
20-35 Operator Multiplier
40-55 Operator Attenuation
60-75 Operator Attack/Decay
80-95 Operator Sustain/Release
AO-A8 Channel Frequency (lower 8 bits)
BO-B8 Channel Frequency (high 2 bits);
Block number (octave);
Key on
CO0-C8 Channel Stereo; Operator connection
EO-F5 Operator Wave Select
since they are in the sixth element positions. The identification of the operator base
value then makes it possible to alter its values for attack, decay, sustain, release, vol-
ume, vibrato, and other attributes. This is done by adding the attribute’s register
address to the channel operator's base value. Table 15-2 shows a partial list of the per-
tinent attribute registers for the FM chip. Notice that some registers modify the opera-
tor and some change the channel (therefore both operators are affected). As an
example, suppose we want to address the sustain/release attribute of channel 2’ carrier
operator. Figure the base address as the channel - 1 + 3, then add the register number
from the table for sustain/release, 80h, and arrive at the answer of 84h.
So, now, within the loop, several of the voice’s characteristics are set to a startup
value:
Write(LEVEL_OUTPUT + ofst_op1Lchannell, Oxff);
Write(LEVEL_OUTPUT + ofst_op2Lchannell, Oxff);
Write(ATTACK_DECAY + ofst_op1Lchannell, Oxff);
Write(ATTACK_DECAY + ofst_op2Lchannell, Oxff);
Write(SUSTAIN_RELEASE + ofst_op1lchannell, 0x0f);
Write(SUSTAIN_RELEASE + ofst_op2Lchannell, 0x0f);
The volume for each voice is set, then the attack, decay, and release rates to super-fast,
while sustain is set to 0. Any note still in progress will reach its end in no time at all
(well, not quite literally).
The number which makes up the frequency spans two registers. More exactly, it
spans one-and-one-half registers, the lower 8 bits into one register and the upper 4 bits
in another. This frequency is used for both operators of the voice. Thus there is one set
of frequency registers per voice. The other 4 bits of the high frequency register are used
to turn the note on and to set an octave multiplier (also called a block). To make the
carrier’s frequency differ from the modulators frequency, use different multipliers. The
resulting frequency is therefore a formula involving the block value, the frequency
533
om
~~" Build Your Own Flight Sim in C++
— Ea
(12-bit value) and the multiplier. The following sets the block to 0 along with the
upper 4 bits of the frequency, as well as turns the note off:
EERE BE
The loop repeats until all nine voices are finished. We're not done with sbfm_Reset(),
however. Some odds and ends need to be fixed before we return from our function. In
particular, the timerisreset and both rhythm and speech modes are turned off.
//this will set chip to 9 channel mode
Write(WAVESELECT_TEST, WAVESEL); //enable wave select
Write(IRQ_TIMER_CNTRL, RESET_TIMER);
Write(CSM_SEL, 0x00); //clear Composite Speech Mode
Write (DRUMCONTROL, 0x00); //clear drum mode
as key release. These terms come from MIDI, where they are events, but they
m
NU
NECR —
probably stem from the model of the piano keyboard. Before we can look at the pro-
cedure to key the channel, we need to look at the structure which will store all the
sound characteristics.
Channel attributes are not readable from the FM chip. Thus, the programmer must
keep track of the values written to the various voices. The sbfm_INST structure defines
fields to hold the FM-instrument settings:
typedef struct {
byte opl1soundchar;
byte op2soundchar;
byte opllevel_output;
byte op2level_output;
byte oplattack_decay;
byte op2attack_decay;
byte oplsustain_release;
byte op2sustain_release;
byte oplwave_select;
byte op2wave_select;
byte op1feedback_con;
} sbfm_INST;
Note there are two fields for every sound characteristic, each prefixed with one of two
operators. There is only one feedback_con field since that information applies to how
the two operators are connected.
To turn on the sound in a channel, we call the sbfm_KeyOn() function with three
parameters: the channel number, a pointer to an instrument structure, and the fre-
quency at which the sound should intone:
void sbfm_KeyOn(int channel, sbfm_INST *inst, word fnum)
{
First, the frequency is saved for later when the key is released. The block_fnumh array
is declared as static at the top of the SBFM.CPP module. Both operators are then atten-
uated and the channel is turned off so that any snaps, crackles, or sputters will not
occur as we write the registers with our instrument data.
block_fnumhLchannell = (byte) ((fnum >> 8) | KEYON);
Write(LEVEL_OUTPUT + ofst_opl1Lchannell, Oxff); //attenuate this
Write(LEVEL_OUTPUT ofst_op2Cchannell, Oxff);
+ //channel's volume
Write(KEYON_BLOCK_FNUMH + channel, 0x00); //key release
Next, data from inst is transferred to the relative registers for the first operator.
Write(SND_CHAR + ofst_opl1Lchannell, inst->op1soundchar);
Write(LEVEL_OUTPUT + ofst_op1Lchannell, inst->opl1level_output);
Write(ATTACK_DECAY + ofst_op1Lchannell, inst->oplattack_decay);
Write(SUSTAIN_RELEASE +ofst_op1Lchannell, inst->oplsustain_release);
Write(WAVE_SELECT + ofst_opl1Lchannell, inst->oplwave_select);
The same transfer then happens for the channels second operator.
> Build Your Own Flight Sim in C++
ee "- BE BE
EEN
Write (SND_CHAR + ofst_op2Lchannell, inst->op2soundchar);
Write(LEVEL_OUTPUT + ofst_op2Lchannell, inst->op2level_output);
Write(ATTACK_DECAY + ofst_op2Lchannell, inst->op2attack_decay);
Write(SUSTAIN_RELEASE + ofst_op2Lchannell, inst->op2sustain_release);
Write(WAVE_SELECT + ofst_op2Lchannell, inst->op2wave_select);
The inividual operators are now set, so we write the channel fields.
Write(FEEDBACK_CON + channel, inst->op1feedback_con);
Write(FNUML + channel, (byte) fnum);
The lower 8 bits are written via a cast to the user type byte, which truncates the fnum
argument. Finally, the last step is to key on the channel. Note that this is done last so
noise is not heard; if we had keyed on the sound before making adjustments to the
operator registers, unwanted noises might take place.
Write(KEYON_BLOCK_FNUMH + channel, block_fnumhCchannell );
}
The previous value stored in block_fnumh is written out to key on the sound. The
entire function is shown in Listing 15-3.
536 m
Write(WAVE_SELECT + ofst_op2Lchannell,
Sound Programming
inst->op2wave_select);
CLL SN
//misc. characteristics
Write(FEEDBACK_CON + channel, inst->op1feedback_con);
Write(FNUML + channel, (byte) fnum);
//key it
on
Write(KEYON_BLOCK_FNUMH + channel, (byte)block_fnumhCchannell);
Shutting down a channel is pretty trivial. We just need to reset a bit in the FM chip.
void sbfm_KeyRelease(int channel)
{
Write(KEYON_BLOCK_FNUMH + channel, block_fnumhCchannell &
KEYREL);
}
This function alters the pitch of a sound which is already playing. Again, it’s pretty
simple.
void sbfm_KeyBend(int channel, word fnum)
{
Flying Sounds
Now that we've got some decent low-level code up and running, we want a higher-
level module which will be called by the rest of the application. In the appendix, this
same module acts as an interface to DiamondWare’s Sound ToolKit.
This module will be called SOUND.CPP. We'll discuss each of its functions in turn,
beginning with sound _Init().
void sound_Init(void)
{
if (sbfm_PresenceTest() == true) {
sbfm_Reset();
}
initted =
true;
}
The initted variable is declared as user type boolean at the top of the file. Let’s examine
this trivial-looking function, because there are interesting points worth mentioning.
It begins with a call to our shfm_PresenceTest(), from SBFM.CPP. Notice how we
can tell what module the function is found in? The technique of using prefix mnemon-
ics to identify files is especially useful in a program written by more than one pro-
grammer (and containing more than one module!). This same effect occurs when class
functions are grouped in the same module since the objects type identifies the module.
E BE BE BE BN
537
—Build "EE EEE
Lr Your Own Flight Sim in C++
void sound_KillL()
{
initted = false;
sbfm_Reset();
}
This function resets initted. This is important, as we'll soon see. But first, here’s the
structure to group together the various sound conditions which can exist in our simu-
lator. It is self-evident from the code comments:
typedef struct {
word rpm; //rpm of engine (on or off)
word tpos; //throttle position
word airspeed;
boolean engine; //true if engine running
boolean airborne; //true if the plane has taken off
boolean stall; //true if stall condition
boolean brake; //true if brake on
boolean crash; //true if plane is crashing
boolean sound; //true if sound is on
} sound_CNTRL;
The variable airspeed is the air speed. This structure then becomes our logical handle
for the sound effects.
First let's design an additional sound of a switch being toggled. This is not part of
our sound_CNTRL structure. We'll call PlaySwitch() from within the module whenever
a state change indicates that a switch was thrown by the player (e.g., the brakes).
Such a sound effect adds a nice touch.
static
{
void PlaySwitch(void)
sbfm_KeyOn(U1, &sfx[SWITCH]l.inst, O0x1d81);
sbfm_KeyRelease(U1);
}
This function was declared with the keyword STATIC. That tells the compiler that only
functions within SOUND.CPP will call it. The name PlaySwitch will not be visible to
any module other than SOUND.CPP If it can, the compiler will optimize the call to sta-
tic functions by making them relative (near) calls. In a Sound Control class, such
functions would be protected or private to achieve the same scope limitations.
Having defined the sound_CNTRL struct, we can use it immediately as a parameter
to the sound_Update() function:
void sound_Update(sound_CNTRL *cntrl)
{
When writing this module, we didn’t know what assumptions we could make
about the code which is calling us. For instance, would sound_Update() be called from
some sort of timer interrupt handler? If so, then it’s likely that it could be called before
sound_Init(). Using the boolean, initted, prevents that problem if this module is reused
in something other than our flight-simulator project.
There are four functions called from within the sound_Update() function. Each of
them are declared static and take one parameter, a pointer to a sound_CNTRL struc-
ture. There are no coding abnormalities in these functions, the FM registers are manip-
ulated to load instruments and key on the channel. The numbers used for data as
frequency settings are from trial and error, tweak and adjustment. Feel free to mess
with these values. The four functions are
1. UpdateCrash() If the flight-simulator code tells us that the player has crashed,
we'll use five FM channels to cause a crashing sound effect. OK, OK, so it
sounds like a colossal fizzle. Everyone knows what it means!
if (cntrl->engine != engstate) {
//if the engine state has
//changed
PlaySwitch(); //figure out if it has turned on
//or off and turn its sound on or
//off accordingly
if (engstate == false) {
//Turn part of the engine noise on
fnum (dword)sfx[LENG31.minf;
=
fnum |= block[LsfxLENG3]1.blockl;
sbfm_KeyOn(E3, &sfx[ENG3l1.inst, (word)fnum);
}
else {
//Turn part of the engine noise off
sbfm_KeyRelease(E3);
}
engstate = cntrl->engine;
}
if (lentrl=>rpm &8&
!lastrpm) {
//No sound is produced for this condition
}
else if (cntrl->rpm &&
!lastrpm) {
Lastrpm = cntrl=>rpm;
//Turn other part of the engine noise on
fnum = (dword)sfx[LENG11.minf;
fnum |= block[Lsfx[CENG1l.blockl;
sbfm_KeyOn(E1, &sfx[ENG1l.inst, (word)fnum);
fnum = (dword)sfxLENG21.minf;
fnum |= blockCsfxLENG2]1.blockl];
sbfm_KeyOn(E2, &sfx[LENG2l.inst, (word) fnum);
}
else if (cntrl->rpm &&
lastrpm) {
//Update the engine noise for a new rpm
Sound Programming m m wm
if (engstate == true) {
541
~~ Sena
Build Your Own Flight Sim in C++
if (brake != cntrl->brake) {
brake = cntrl->brake;
PlaySwitch();
}
Sounding Off
It isn't too terribly complicated to program the FM chip, but then again,
it isn't too
rewarding either. If you spend some effort on it as we did, the sound effects produced
will be mediocre. Digitized sound effects provide maximum impact. On the other
hand, many successful games of the past used FM effects; it’s not completely worthless
either.
The functions suggest classes for both managing the FM-chip registers and control-
ling the FM for the game as the logical sound-interface class. There are identifiable
objects which naturally stand out for possible exploration as distinct classes: operators,
channels (or voices), and instruments. We pointed out where static functions would be
private or protected class members. The static variable members
update function could be made private class fields.
at the top of each
This has been the last piece of the puzzle in our quest
the next chapter, we will fit all the pieces together.
to
build a flight simulator. In
542 =
The Flight
Simulator
The cleverest algorithms, the most optimized code, the best-looking graphics
. are all useless unless they can be melded into a program. You now have an impres-
sive repertoire of effects at your disposal. You know how to create 256-color VGA
graphics on a PC-compatible microcomputer. You can decode PCX-format graphics
files (and compress the data in them for later use). You can animate bitmaps. You've
learned how to create images of three-dimensional objects using wireframe and poly-
gon-fill techniques and how to manipulate those images to show how those objects
would appear from any angle or distance. You can build a world and fill it with scenery.
You can even perform rudimentary sound programming.
In this chapter, you're going to learn how to use these techniques in an application
program. And not just any application, but the type of application to which such tech-
niques are most frequently applied: a flight simulator. We’re about to create a working
flight simulator. Both the executable code and the source files for this flight simulator are
available on the disk that came with this book, under the overall name FSIM.EXE. The
first version of the program, called FOFEXE (short for Flights of Fantasy), has been
overhauled to incorporate the classes we created from the previous chapters. The full
instructions for operating this program—that is, for flying the flight simulator—and for
recompiling it under the Borland C++ compiler are in Appendix A of this book. But the
story of how it works is in this chapter.
545
lf ~~
_ Build = oie
Your Own Flight Sim in C++
Any application can interface to the view system that we have created in this book and
WB
draw a view of a
three-dimensional world. You need only follow these simple rules:
VIEW.CPP
POLY2.CPP
SCREENC.CPP
SCREEN.ASM
BRESNHAM.ASM
DRAWPOL2C.CPP
FIX.CPP
LOADPOLY.CPP
WORLD.CPP
POLYLIST.CPP
PALETTE.CPP
® You can also link in the PCX.CPP file, if you want to use the Pcx class to load a
PCX format graphic file, and the INPUT.CPP file, ifyou want touse the input
functions we used in this book. To use our sound effects, link in SOUND.CPP
and SBFM.CPP.
m Create a world database using the format that we've described in this book.
m Write the application. It will need to initialize certain of the modules that con-
tribute to the view system, and then it will need to make a call to the view sys-
tem on each loop through the program's main event loop. Here is a skeleton
program that initializes these modules:
// The following files need to be included:
#include <iostream.h>
#include <dos.h>
#include <bios.h>
#include <conio.h>
#include <stdlib.h>
#include <math.h>
#include "poly2.h"
#include "view.h"
#include "screen.h"
#include "pcx.h" // Optional
// The following constants are useful:
const XORIGIN=160; // Virtual x screen origin
const YORIGIN=90; // Virtual y screen origin
const WIND_X=16; // Window upper Llefthand x
const WIND_Y=15; // Window upper Llefthand y
The Flight Simulator
EE
const WIND_X2=303; // Window lower righthand x
const WIND_Y2=153; // Window Lower righthand y
const FOCAL_DISTANCE=400; // Viewer distance from screen
const GROUND=105; // Ground color
const SKY=11; // Sky color
setgmode (0x13);
HEH BE BH BE N=
547
- ~
pe
~
~~ -
"Build Your Own Flight Sim in C++
winview.Display(curview,1);
curview.copz++;
}
Movement along the other axes, or combinations of axes, is done the same way. Simi-
larly, successively incrementing (or decrementing) any of the orientation fields—axangle,
yangle, or zangle—would give the impression that the user is rotating in place. A slow
rotation around the y axis, that is
by steadily increasing the value of yangle from 0 to 255,
will create rotation such as found when a movie camera pans the horizon. The move-
ment of the view’ orientation in the world can be used to create a cimematic fly over.
The second way to change the image between frames is
to change the description of
an object in the world database. Usually, you'll want to
change it in one (or both) of
The Flight Simulator
"EN
two ways: making an object seem to move or making an object seem to rotate. To do
the former, change the x, y, and/or z fields in the objects descriptor. To do the latter,
change the xangle, yangle, or zangle fields in the object's descriptor. Thus, to make an
object seem to rotate on its x axis, you would use a loop suchas this:
while (1) (
winview.Display(curview,1);
Object *objptr = (winview.GetWorld()).GetObjectPtr(OBJNUM)
objptr->xangle++;
}
This code assumes that the View class has been provided an access function to the
world object and the Object class has made its xangle field accessible for external tam-
pering. Animation information can be encapsulated into the Object class, such that the
class stores movement data internally along with its position in the world. A semaphore
in the class can be used to signal automation on or off. Each loop the objectis checked
for updating its position in the world, and a public method can be provided such as
Animate(x,y,z,xangle, yangle, zangle, speed). This supplies a means for external manip-
ulation of the type of motion.
By changing the view position or the world and calling winview.Display() each
time to draw a new image of the world in the viewport, the screen will constantly be
updated. The user will get the sense that the viewport really is looking into a real
world—and that he or she is able to move through that world.
The final step is to connect the user to the world through some sort of input. This
can be done via keyboard, joystick, mouse, or any type of input device. The changes in
the view are then determined by the users actions.
Now lets look at a real application, constructed precisely in this manner, that uses
the view system: the Build Your Own Flight Sim in C++, version 2.0.
Flight Simulators
A is
flight simulator a program that simulates the experience of flight. Most flight sim-
ulators are based on the performance of flying machines, such as airplanes, jets, heli-
copters, and spacecraft, though at least one flight simulator of a few years ago and
today simulated the flight of a dragon. The spaceflight simulators, such as Wing Com-
mander and Tie Fighter, are an immensely popular subgenre of flight simulation.
Although there are probably as many ways to build a flight simulator as there are to
build a flying machine, most flight simulators look pretty much alike (though they vary
widely in quality). We won't vary from the look of the standard flight simulator in this
book, though this doesn’t mean that the reader of this book need feel constrained to
use the view system only in this manner. In fact, the view system presented in this
book can be used to construct a nearly infinite variety of programs—not just flight sim-
ulators and not even just games.
—
~~"
eel
Build Your Own Flight Sim in C++
LL — mE
EE EEE
The standard flight simulator presents the armchair pilot with the view from the
cockpit of an airplane. (The cockpit view of Falcon 3.0 is shown in Figure 16-1.) The
control panel of the airplane is visible in the lower portion of the video display, while
the majority of the screen is taken up with the out-the-window view. It is the out-the-
window view that is produced by the view system. The control panel is then laid over
the bottom of the viewport to produce the rest of the display.
The most common control device for a flight simulator is the joystick, because it
resembles the actual controls of an airplane. However, the keyboard and mouse are also
supported by most flight-simulator programs.
choosing which of these forces should be dominant at any given moment, we can make
the airplane do what we want it to do. When lift is dominant over gravity, for instance,
the airplane will go up; but when gravity is dominant over lift, the airplane goes down.
Similarly, when thrust is dominant over drag, the airplane goes forward, but when drag
is dominant over thrust, the airplane comes to a halt. (Only in a powerful headwind
would the airplane actually go backward.) Balancing these forces, however, is no easy
task. Thrust, for instance, is intimately associated with lift. Without thrust, there is no
lift—which is why you can’t make the plane stand still (relative to the air around it)
without it falling out of the sky.
At this point, I should apologize to students of aerodynamics for the gross simplifi-
cations in this description. Anyone interested in knowing the full details on the subject
should seek out a good text on aerodynamics. Alas, such texts are not easy to find and
may require the resources of a good college bookstore or library.
Thrust
Let's look at how all of this works in practice. The first force that must come into play
when flying an airplane is thrust. Thrust is generally created in one of two ways:
The Flight Simulator m
mm
through the principle of reaction mass or through pressure differential. Let's look at
these one at a time, starting with reaction mass.
Even if all you studied in college were nineteenth-century poets who used non-
standard rhyme schemes, you probably have a vague recollection of Isaac Newton's
First Law of Motion, usually restated as: “For every action, there is an equal and oppo-
site reaction.” This is why, when you fire a gun, you are propelled backward with the
same force that propelled the bullet forward. (The reason you don’t move as fast or as
far as the bullet is that you're a lot larger than the bullet.) This tells us that we can start
something moving in one direction by starting something else moving in the opposite
direction. The something else that we move is called the reaction mass, because we're
moving it in order to get a reaction.
In theory, for instance, we could propel a boat backward by placing a pile of stones
on the deck and hurling them off the bow one by one. The stones would be the reac-
tion mass. In practice this doesn’t work very well because the stones are just too much
smaller than the boat and the human arm isn’t capable of applying all that much force
to them. But it probably moves the boat a little. (See Figure 16-3.)
The most obvious application of reaction mass is in the rocket engine. Oxygen (or
some similarly volatile substance) is heated to high temperatures inside a tank, causing
it to expand explosively and escape through a hole located at one end of the tank.
These atoms of oxygen (or whatever) serve as a reaction mass. Every time a hot atom
shoots out of the hole, the tank itself (and anything that happens to be attached to it)
is propelled in the opposite direction. Although an atom is substantially smaller than
HE BE BE EB = 553
ee
Build Your Own Flight Sim in C++
"EE EEN
—u”
the average rocket engine, enough of them shoot out of the engine at the same time, to
create a powerful thrust. (See Figure 16-4.)
Jet planes use this same effect to achieve thrust. In fact, jet engines use oxygen for
reaction mass just as a rocket engine does. The only real difference between a jet and a
rocket is that jet engines heat oxygen from the air around them, while rocket engines
carry their own oxygen supplies with them. (This allows rockets to boldly go where jet
planes can’t—into the airless void of space.)
Propeller-driven airplanes, which is what we are going to simulate in FSIM.EXE, rely
more on pressure differential. The propellers are shaped in such a way that, as they
rotate, air movesat a speed on one side of the propeller different from that on the other
side. This creates more air pressure on one side of the propeller and this air pressure lit-
erally pushes the propeller, and the airplane attached to it, in the direction of the low
pressure. Thus, the pressure differential produces thrust, as we see in Figure 16-5.
Drag is the countervailing force to thrust. Air doesn't necessarily like having things
moving through it at a high speed, so it resists this motion. (Okay, this is blatant
anthropomorphization of a substance that has nothing whatsoever resembling human
motivation—but this is between friends, right?) This air resistance fights the motion of
an aircraft, slowing it down. This drag becomes more and more significant as the craft
moves faster and faster. It must be taken into account both in the physical design of the
craft, which should minimize friction with the air, and in calculations of how much air-
speed (speed relative to the air) will be produced by a given amount of thrust.
554 ©
The Flight Simulator m
"a.
Figure 16-5 A
propeller thrusts an
airplane forward
because the pressure
to the front of the
propeller is lower than
the pressure behind
Thrust can make an aircraft fly all by itself, if there's enough of it. Aim a rocket at the
sky,turn on the engine full blast, and it will shoot upward purely from the force of the
reaction mass popping out of its rear end. Once it gets moving fast enough it can keep
going on momentum alone, in the pure ballistic flight that we associate with bullets
and ICBMs. This is how rockets and missiles fly. But airplanes use thrust mostly just to
get moving. Flight is handled by a second force: lift.
Lift
Here's an experiment you can perform in the comfort of your own automobile, prefer-
ably after the rush hour and, more wisely, from the passenger’ seat. Get on the freeway,
drive at the speed limit, and hold your hand out the window—palm down. Now tilt
the leading edge of your palm slightly into the wind. Suddenly your hand will be pro-
pelled upward; you'll have to fight to keep it down. Congratulations! You've just expe-
rienced lift!
There are a couple of reasons why objects such as your hand tend to move upward
(or, under some conditions, downward) when air is rushing past them at a high speed.
The most important one for our purposes is called the Bernoulli principle or airfoil
effect. Like the thrust produced by a propeller,
it results from differences in air pres-
sure. (In fact, propellers are utilizing the Bernoulli principle, too.) When air pressure
a / d “Build
/ Your Own Flight Sim in C++
pe
Figure 16-6 The pressure under the wing is higher than the
pressure over it, which pushes the airplane into the air
gets higher on the underside of an object than on the upper side, the air literally
pushes the object up.
Let's look at how this works in the case of an airplane. We've turned on the engine
and the propellers are spinning, creating reaction mass that thrusts us forward. Air
rushes past us both above and below. That air creates pressure, a force pressing directly
against the wings and body of the plane. However, because it is passing both above and
below the body of the plane, that force pushes both upward and downward at the same
time. It would seem to cancel itself out. How then do we produce flight?
The shape of the wing is such that the air flows somewhat differently over the top
than over the bottom, leaving a pocket of low pressure directly above the wing. Thus,
the air pressure is greater beneath the wing than above it. (See Figure 16-6.) The
faster the air moves past the wing, the greater the pressure differential. Once the air is
rushing with sufficient speed over the wings to produce enough upward force—that is,
lift—to overcome the gravity that ispulling the airplane down, it will actually begin to
rise off the ground. We will have achieved flight!
Because lift results from the shape of the airplane and its wings, itis affected by the
angle at which the airplane is oriented on its local x axis, known technically as the
angle of attack. Tilting the front end of the plane upward, so that more of the underside
of the body and wings is exposed to the onrushing air, increases lift and therefore alti-
tude. Unfortunately, it
also slows the airplane down, which decreases lift. Tilting back
too much, therefore, can cause the airplane to stall and start falling out of the sky. (See
Figure 16-7.)
The Flight Simulator
Ew
Figure 16-7 The angle of attack affects
the degree of lift
(b) A large angle of attack produces (c) Too large an angle of attack causes the
more lift plane to stall
Controlled Flight
Aerodynamic flight would be little more than an interesting curiosity were it not pos-
sible to control the way in which an airplane flies. We've figured out how to thrust an
airplane forward and make rise into the air, but now we need to control the amount
it
of thrust that we produce and the amount of lift that results from that thrust. And, of
HE BE BE BE B®
557
" EE EEE
ol Build Your Own Flight
8 Sim in C++
course, we also need some way to control the direction in which we are flying, so that
we can travel to the destination of our choice.
is
Controlling thrust easy. Ultimately, thrust is produced by a fuel such as gasoline.
This fuel is either burned to heat oxygen (in the case of a jet) or funneled into an
engine to turn a propeller (in the case of a prop plane). Therefore the amount of thrust
is roughly proportional to the amount of fuel flowing to the engine, which can be easily
controlled with a throttle. Opening the throttle wide lets the maximum amount of fuel
flow, producing the maximum amount of thrust. Pulling back on the throttle decreases
the amount of fuel and therefore the amount of thrust.
This, in turn, can affect lift, though in a more complicated way than you might
think. Since lift is created by the speed at which the plane is traveling through the air,
it would make sense that pulling back on the throttle to reduce thrust would also
reduce lift. This is true to some extent, but it isn’t quite that simple. When the airplane
slows down and loses lift, it starts to fall under the pull of gravity. But as it falls, it
speeds up—which increases lift. Thus, getting an airplane to go back down after it
the sky is not a trivial matter and requires some experience. Probably the single most
is in
difficult thing that a pilot has to do is to land a plane. Taking off and maneuvering
while in the air are trivial by comparison.
Control Surfaces
To simplify these matters and to give us the added ability to steer, airplanes are given
control surfaces. These are external features of the plane that can be manipulated via
controls placed in the cockpit, effectively allowing the pilot to change the shape of the
plane dynamically while flying in order to slow it down, speed it up, make it rise and
it
fall, or steer left or right.
The three major airplane control surfaces are the ailerons, the elevators, and the rud-
der. They are controlled from the cockpit by two controls: the stick (or joystick) and the
pedals. When moved to the left or right, the stick controls the ailerons. When moved
forward and back, the stick controls the elevators. The pedals control the rudder.
What do these control surfaces control? To explain that, we'll need some new ter-
minology. Since an airplane moves in three dimensions, it is capable of rotating on all
three of its local axes—the x, y, and z axes. These rotations have names. Rotation on the
airplane’ local x axis is called pitch, because it makes the craft pitch up and down from
the viewpoint of the pilot in the cockpit. Rotation on the airplanes local z axis is
called roll, because it makes the craft roll sideways from the viewpoint of the pilot. And
rotation on the airplane’ local y axis is called yaw, for no reason that I can figure out.
(See Figure 16-8.)
The three main control surfaces control the airplane’ rotation on these three axes: the
ailerons control roll (z rotation); the elevators control pitch (x rotation); and the rudder
controls yaw (y rotation). Thus, moving the stick left or right causes the airplane to roll
left or right. This in turn causes the airplane to turn in the direction of the roll, making
The Flight Simulator m
"ow
(b) Yaw-rotation on the plane's local (¢) Roll-rotation on the plane's local z axis
y axis
the stick into a kind of steering wheel. (Some planes actually use a wheel for this pur-
pose.) Pulling the stick back (i.e., towards you) causes the airplane to pitch up (and
climb or stall, depending on how far it pitches). Pushing
it forward causes the airplane
to pitch down (and lose altitude). Operating the pedals causes the airplane to yaw.
Thats it for our description of how an airplane flies. It was an extremely simplified
description, but so is the flight simulator that we are going to base on it.
In the rest of
this chapter, we'll show you how the flight simulator itself is constructed.
~~ Build Your Own Flight Sim in C++
private:
void CalcPowerDyn();
void CalcFlightDyn();
float CalcTurnRate();
void CalcROC();
void ApplyRots();
protected:
void DoWalk();
public:
inline boolean AnyButton()
{ return (button? || button2) ; }
562 =
The Flight Simulator
CLE
{
AC.button1 = false; // reset both button flags
AC.button2 = false;
if (usingStick) // is the joystick in use?
CalcStickControls( AC ); // yes, grab the control state
else if (usingMouse)
CalcMouseControls( AC );
else
CalcKeyControls(AC); // no, get the keyboard controls
CalcStndControls( AC ); // go get the standard controls
CheckSndViewControls( AC );
AC.aileron_pos = stickX; // update the state vector
AC.elevator_pos = stickY; // with values calculated in
AC.rudder_pos = rudderPos; // the other functions
}
AC.ReducelIndices( J; // remap the deflections
The FSIM simulator supports joystick, keyboard, and mouse input. When the pro-
gram is first run, we'll ask the user which type of control he or she wishes to use and
set the flags usingStick and usingMouse appropriately. (We'll see the code for this later in
the chapter.) If the usingStick flag is nonzero, then the user wants joystick control. If the
usingMouse flag is nonzero, then the user wants mouse control. If both the flags are
zero, the user wants keyboard control. Of course, only some of the controls will vary
according to this flag; others will always be performed through the keyboard. We'll call
these the standard controls.
The GetControls() function starts out by setting the aircraft button states to false. If
the joystick or mouse buttons (or for keyboard—the or keys) are pressed
they will be set in the appropriate function. The function checks to see if the user has
requested joystick, mouse, or keyboard control. If the first, it calls the CalcStickCon-
trols() function, passing it the AirCraft object reference (which is in the parameter AC),
to perform the necessary calculations for the joystick. Likewise, if mouse control is
desired, the function calls CalcMouseControls() with AC passed as the parameter. If the
third, it calls CalcKeyControls() to perform the necessary calculations for the key-
board. Then it calls CalcStndControls() to handle those controls that are always done
through the keyboard.
Then it uses the information returned from these functions to update the state-vec-
tor positions of three control surfaces: the aileron, the elevator, and the rudder. Since
state_vect is a public base class for AirCraft such access is permitted. Each of these sur-
faces has a center position, but can be deflected to either side of these positions.
Therefore, we'll measure these positions on a scale of -15 to 15. Since the numbers
returned from the earlier functions will be positive, we'll call the AirCraft function
Reducelndices() to adjust them to the proper scale. The text of this function appears in
Listing 16-2.
"Build Your me
Own Flight Sim in C++
elevator_pos /= 7;
if (elevator_pos > 15)
elevator_pos = i
else if (elevator_pos < =15)
elevator_pos =15;
rudder_pos /= 7;
if (rudder_pos > 15)
rudder_pos = 15;
else if (rudder_pos < =-15)
rudder_pos = =-15;
Thats the basic process involved in receiving input. However, we need to look at
some of these functions in more detail. Let’ start with the CalcStickControls() function,
the text of which appears in
Listing 16-3.
This function is called from GetControls(). It calculates the state of the flight con-
trols that are mapped to the joystick when it is in use. This function is only called when
the joystick is in use. The comments placed in this function pretty much tell the tale.
The goal of the function is to get the position and button status from the joystick
(which will be used by GetControls() to set the positions of the rudder, ailerons, and
elevator); remap the position of the joystick to a -128 to 127 range (where 0 means that
the joystick is perfectly centered); and set the joystick and button fields in the state vec-
tor to the appropriate values.
The FControlManager class has an altered EventManager class as its base. If you
look at the original EventManager class in Chapter 5, we defined the fields xcent,
xmax, etc. as private to the class. This was changed to protected in order to allow
derived classes to access these values.
The actual reading of the stick and buttons is performed by the function Read-
StickPosit(). This function reads the joystick port and calculates the stick axes position.
The text of this function appears in Listing 16-4. It is similar to the joystick reading
function that we created back in Chapter 5. (In fact, you might want to go back and
reread that chapter if you're feeling a little shaky on the basics of reading a joystick.)
asm {
mov ax, axisNum // load the axisnum
continued on next page
uid Your Own Flight Sim in C++
readloop1:
asm {
in al, dx // get the byte from the game port
test al, bl // test it against the axis mask
Loopne readloop1 // if it's still set loop
XOr ax, ax // clear ax in case this was a time_out
jcxz done // did we time-out? if yes exit, ax == 0
}
elapsedTime = timer1->timer0ff();
bit16Time = ( word JelapsedTime;
asm {
sti
// now read the joystick port repeatedly until the other 3 axis
// timers run down. This is MANDATORY.
mov dx, gamePort
Xor cx, cx
}
mov bl, activeAxisMask // mask for all active axes
readloop2:
asm {
in al, dx // same basic operation as in
test al, bl // readloop1
Loopne readloop2
mov bx, bit16Time; // get elapsed time into bx
mov cl, &
// style x,y values, i.e. 0-255
shr bx, cl
mov ax, bx // final result in AX
}
xor ah, ah // don't need the high byte
done:
return(_AL);
}
This function uses a Borland C++ convention that we haven't used in any of the ear-
lier functions in
this book: the asm directive. This allows assembly language instruc-
The Flight Simulator
tions to be embedded directly into C++ code. The statement or block of statements
within curly brackets that follows the asm directive should be written in 80x86 assem-
mm
.
bly language rather than in C++. This allows us to selectively optimize parts of our
code in assembly language or to access low-level functions without introducing a com-
plete assembler module. C++ variables may be accessed from the asm code directly, by
name. The compiler will substitute the proper addressing mode for accessing the
memory location where the value of that variable is stored. Also note that normal C++
style comments using two forward slashes can be used within an asm block instead of
the assembly language semicolon.
As you'll recall from Chapter 5, the trick to reading the gameport is to output a
value—any value—through port 0201H. This tells the gameport that we want to read
the joystick. We then read the value at this same port, checking the bit in the byte that
we receive that corresponds
to the joystick axis (horizontal or vertical) that we wish to
read, waiting for it to become 0. The longer it takes to return to 0, the further the stick
has been pushed to the right. (To determine specific values for specific positions on the
axis we must calibrate the joystick during program initialization.)
The code in the ReadStickPosit() function does all this and more. The main differ-
ence between this and our readstick function is that ReadStickPosit() also sets a timer
(about which we'll have more to say in a moment). This timer is used instead of the CX
register count to provide accuracy for the resulting measurements of stick movement
on the axes. It returns the stick timing as a number from 0 to 255.
Mouse input is imitative of the CalcStickControls() with the x and y positions
being supplied by the relpos() function from Chapter 5. Control of the plane with the
mouse is difficult—there should be some kind of stabilizer for mouse users by which
the flight controls automatically center themselves. This will be left up to future flying
enthusiasts.
The last method of controlling the plane is through the keyboard's arrow keys. If the
user has requested keyboard input instead of joystick/mouse input, GetControls() calls
the function CalcKeyControls(), the text of which is in Listing 16-5. It checks the
state of all the flight control keys, and adjusts the values which would normally be
mapped to a joystick. GetControls() only calls this function if both joystick and mouse
are not being used.
HE BE BE HE B®
567
a Build Your Own Flight Sim in C++
boolean state array is updated on the press of any relevant key. Note that the array
WR
elements stay true until read by GetControls() or one of its
subsidary functions. The
code is in Listing 16-6.
byte code;
if (KeyWaiting()) {
// make sure something in port
code = inportb(kbd_data); // grab the scan code
keypressed =
true; // returned by KeyPressed()
if( code < 128 )
_keydownLcodel = true;
}
0LldO9Handler (); // chain the old interrupt
*nextBiosChar = biosBufOfs; // make sure the bios keyboard
}
*lastBiosChar = biosBufOfs; // buffer stays empty
When a keyboard interrupt is executed, the CPU port that we have labeled with the
constant kbd_data contains the scan code of the key that has been pressed. (For a dis-
cussion of scan codes, see Chapter 5.) The value returned from reading the key-
board port directly is 1 byte where the first bit (0x80) signifies that the key has been
pressed (0) or released (1). The lower 7 bits make up the scan code for the key. Thus,
if the scancode for the key is 15 (0x0f), a code of 15 returned from reading the
keyboard port means the key is pressed down, while a code of 143 (0x8f) sig-
nifies that the key has been released. The NewO9Handler() checks if the code is
less than 128; this is equivalent to qualifying only those codes without the high bit
set. It then calls the regular BIOS keyboard handler function, which will not detect
the key press because we've already read the scan code out of the port. Note that this
function also sets a flag called keypressed so that other functions will know that a
keyboard event has taken place. For details on which keys control which functions of
the airplane, see Appendix A.
The CalcKeyControls() function checks to see if any keys were pressed that relate to
joystick functions. so, If
it
sets the same variables that are set by the CalcStickControls()
function, to make it look as though the joystick has been moved. The constants
LEFT_ARROW, RIGHT_ARROW, etc. used as indices into the _keydown array have all
been declared in EVNTMGR2.H and set equal to the appropriate scan code for that key.
~~ Build Your Own Flight Sim in C++
Finally, in GetControls(), after polling the appropriate input device, we call the
CalcStndControls() function, which checks for keys that don't relate to joystick move-
ments. The text of this function appears in
Listing 16-7.
if (_keydown[KEY_GT1) (
rudderPos += rudder_sens;
if (rudderPos > 127)
rudderPos = 127;
}
_keydown[LKEY_GT] =
false;
else if (_keydown[KEY_LTI1) {
rudderPos -= rudder_sens;
if (rudderPos < -128)
rudderPos = -128;
_keydown[KEY_LT] = false;
}
else
rudderPos = 0;
if ((_keydown[PAD_PLUS]) && (AC.throttle_pos < 15)) {
AC.throttle_pos++;
_keydown[PAD_PLUS] = false;
}
else if ((_keydown[PAD_MINUS]) && (AC.throttle_pos > 0)) (
AC.throttle_pos--;
_keydown[LPAD_MINUS] = false;
}
if (_keydown[KEY_I1) {
if (AC.ignition_on)
AC.ignition_on = false;
else
AC.ignition_on =
true;
_keydown[KEY_I] = false;
}
if (_keydown[KEY_BJl1) {
if (AC.brake)
AC.brake = false;
else
AC.brake = true;
_keydown[KEY_B] = false;
}
}
}
The controls monitored by this function operate the rudder (KEY_GT < and KEY_LT
>), the throttle (PAD_PLUS and PAD_MINUS), the ignition (KEY_I), and the brake
(KEY_B). The function checks the appropriate keys, and then sets the flags in the
570 =m
The Flight Simulator
"Ew
AirCraft object that monitors the appropriate controls. It
resets the _keydown array ele-
ment to false, so that subsequent visits to the function do not cause false triggers.
That gets us through the input functions that we'll need during one loop of the
flight simulator animation. Now, we can reveal the constructor code to the FControl-
Manager:
FControlManager::FControlManager():EventManager()
{
usingMouse
usingStick = false;
for(int ii = 0; ii < 128; _keydown[ii++] = false) ;
stick_sens 15;
= // arbitrary value for control sensitivity
rudder_sens = 15;
keypressed = 0;
activeAxisMask = joydetect();
}
The private fields of FControlManager are initated, as well as the global _keydown
array and activeAxisMask variable. The joydetect() function has been added to the
assembly I/O file. This new file with the joydetect() function has been renamed to
MIO2.ASM and its header file to MIO2.H. We'll see the destructor for the FControl-
Manager class later, after we have covered the int 9 installation process.
Now we need to call the AirCraft::RunFModel() function to see what our flight
model makes of these changes to
the control surfaces and other state vector parameters.
The text of the RunFModel() function appears in
Listing 16-8.
571
Build Your Own Flight Sim in C++
if (opMode == WALK)
DoWalk(); // traverse the world
else {
CalcPowerDyn(); // calculate the power dynamics
}
CalcFlightdyn(); // calculate the flight dynamics
efAOF = Degs(efAOF);
// rotate the point in y
* sin(Rads(yaw))) + (tmpX
newX = (tmpZ * cos(Rads(yaw)));
newzZ = (tmpZ * cos(Rads(yaw))) - (tmpX * sin(Rads(yaw)));
tmpX = newX;
tmpZ = newZ;
572 m
The Flight Simulator
"Ew
collectX += newX;
if ((collectX > 1) || (collectX < -1)) {
x_pos == collectX;
}
collect = 0;
collectY += newY;
if ((collectY > 1) || (collectY < -1)) {
y_pos == collectY;
collecty = 0;
}
collectZ += newZ;
if ((collectZ > 1) || (collectZ < -1)) {
z_pos += collectZ;
collectz = 0;
}
This is a great deal more complex than the GetControls() function. It begins by call-
ing a timer object to determine how much time has passed since the
last frame of ani-
mation and records the result in the loopTime long integer variable. Then it starts the
timer again. The timer functions are in the HTIMER.CPP module. In this file, the
HTimer class is defined. The HTimer objects allow events to be timed to within a res-
olution of one microsecond. We don’t need quite that fine a resolution here, so the first
thing that RunFModel() does with the loop Time variable is to divide it by 1,000 to pro-
duce a millisecond resolution. The value of loopTime will be used again and again in the
functions that follow, in order to determine how much time has passed since the pre-
vious frame. This information, in turn, will be used to determine how much the world
of the simulator has changed since the last frame. If we did not perform these calcula-
tions, the simulator would run at a different speed on different machines. The aircraft
would shoot across the skies on fast 486s but crawl on slow 286s. This way, the ani-
mation becomes smoother—i.e., is sliced into more and more frames—without becom-
ing faster. The AddFrameTime() function, which is called after the loop Time variable
has been set, is used to record the frame rate, so that it can be printed out later for
debugging purposes.
Then we update the state vector (the base class to the RunFModel() function) to
reflect any changes that have occurred since the previous frame of the animation.
Next, a series of calculations determine what the new physical position of the aircraft
will be, based on the state vector values. Finally, if the plane has reached flight altitude
for the first time, a flag is set to indicate that the craft is now airborne. This allows cer-
tain controls to operate that could not operate if the craft were sitting on the ground.
(For instance,it is impossible to bank the plane while it is on the ground.)
HE BE BE
= B=
573
~~" Build Your Own Flight Sim in C++
- "EE EEE E
When debugging the FSIM program, if is defined at the top of the
__ TIMEROFF__
AIRCRAFT.CPP module, then this function sets the loop Time variable to LOOP ms, else
it uses the timerl object to time the running of the flight model. The loopTime variable
is set on entry to this module with the number of elapsed ms since the last call, and
then used throughout the module for calculations of rate of change parameters. Defin-
ing __ TIMEROFF__ effectively sets the performance of the system to match a 486. This
lets you step through the flight model and get a nice, smooth change in the variables
you're watching. Otherwise the timer continues to run while you're staring at the
debugger screen.
The RunFModel() function calls five functions, four of which are AirCraft member
functions that update state-vector and global fields: CalcPowerDyn(), CalcFlightDyn(),
InertialDamp(), CalcROC(), and ApplyRots(). These functions must be called in the
order just listed, since there is a dependency on the fields being adjusted. This is
noted in the comments throughout AIRCRAFT.CPP. To understand how the flight
model works, we'll have to examine these five functions one at a time, starting with the
CalcPowerDyn() function which appears in Listing 16-9. This function adjusts the
engine RPM for the current iteration of the flight model. It also toggles the engine_on in
response to changes in the ignition_on parameter.
?
Listing 16-9 The CalcPowerDyn() function
void AirCraft::CalcPowerdyn( )
{
This function first checks to see if the GetControls() function received a command
requesting the engine be turned off. If so, it resets the appropriate flags in the state vec-
574 =m
The Flight Simulator
"Ew
tor structure. If the throttle and ignition settings have been changed since the last
loop, it
resets the engine appropriately, changing its RPMs or shutting
it
on or off. (Neg-
ative RPMs are checked for and eliminated if found, since they aren't possible.)
Next comes the CalcFlightDyn() function, in Listing 16-10. This function calculates
the flight dynamics for the current pass through the flight model. It does not attempt to
model actual aerodynamic parameters. Rather, it
is constructed of equations developed
to produce a reasonable range of values for parameters like lift, speed, horizontal
acceleration, vertical acceleration, etc.
E BE BE BE Wm
575
HE BE BE BE
EB
Lia
~~ Build Your Own Flight Sim in C++
ae
continued from previous page
climbRate = v_speed/loopTime; // save the value in feet/min.
climbRate *= 60000L;
if (stall) €
if (pitch > 30)
stall = false;
else
pitch++;
}
}
This is where the flight modeling begins in earnest. At the beginning of the function,
floating-point variables are created for the speed, acceleration, lift, and angle of attack
of the aircraft.
Initially, the speed is calculated based on the current engine RPMs. This is modified
according to the airplane’ pitch, which will determine the angle at which it is moving
through the air and thus the amount of drag from air resistance. As we saw a moment
ago, the variable loopTime contains the number of milliseconds that have passed since
the last frame of animation was drawn and the last flight model calculations per-
formed. The acceleration of the craft per millisecond is calculated next and multiplied
by
this value, giving the distance that we have traveled since the last frame, which is
stored in hAccel.
If the airbrake is on and the craft is
in the air, it
is slowed down by a unit—i.e., the
hSpeed field of our AirCraft class is decremented. It
is
not allowed to become negative,
however, since airplanes don't fly backwards. If the brake isnt on, the acceleration
value is added to the hSpeed field.
The lift speed is then calculated. The amount of downward gravitational accelera-
tion since the last frame is also calculated and added to the amount of lift since the last
mm
frame. (The gravitational constant, contained in GRAV_C, is negative, so this is effec-
TE
RRR
IR 576 =
The Flight Simulator
tively a subtraction.) The sum is the amount of change in the aircrafts vertical position
"ew
since the last frame—i.e., the vertical speed, which is contained in vSpeed. This is
used to calculate the airplane’ climb rate.
Next, the angle of attack is calculated and finally, we check to see if the aircrafts
angle of flight is such that it is in danger of stalling, in which case the appropriate flags
are set.
In order to get the proper rotation rates on our aircraft, we need to simulate the
effect of inertia (the tendency of an object in motion to remain in motion that we men-
tioned earlier) on the rotation of the craft. This is done in the function InertialDamp()
(Listing 16-11). The inertia simulation is fairly crude but gives the interested reader a
basis for writing his or her own code to approximate the actual rotation of an airplane.
You can see its effects now in the momentum when the aircraft is rolled.
if (deltaVect.dYaw) {
deltaVect.dYaw —= deltaVect.dYaw / 10;
if ( ((deltaVect.dYaw > 0) && (deltaVect.dYaw < .01 J) ||
((deltaVect.dYaw < 0) && (deltaVect.dYaw > -.01 )) )
deltaVect.dYaw = 0;
}
if (deltaVect.dRoll) {
deltaVect.dRoll -= deltaVect.dRoll / 10;
if ( ((deltaVect.dRoll > 0) && (deltaVect.dRoll < .01 J) |]
((deltaVect.dRoll < 0) && (deltaVect.dRoll > -.01 )) )
deltaVect.dRoll = 0;
}
}
The InertialDamp() function is not a member of the AirCraft object class and uses a
global variable, deltaVect, to store its values from frame to frame.
The final flight-model function is CaleROC() (Listing 16-12). This function in turn
calls the CalcTurnRate() function (Listing 16-13). The CalcROC() function finds the
current rate of change for aircraft motion in the three axes, based on control-surface
deflection, airspeed, and elapsed time.
577
-@ HE EH EB BB
BB
"Build Your Own Flight Sim in C++
if (aileron_pos != 0) {
* aileron_pos) / 10000);
torque = ((h_speed
if (deltaVect.dRoll != (torque * LoopTime))
deltaVect.dRoll += torque * 6; // *8
¥
}
if ( elevator_pos != 0) {
torque = 0;
if (deltaVect.dPitch != (torque * LloopTime))
deltaVect.dPitch += torque * 1.5; /1* 4
}
if (h_speed) {
torque = 0.0;
if (rudder_pos !'= 0)
* rudder_pos) / 10000);
-((h_speed
torque =
torque CalcTurnRate( );
+=
if (deltaVect.dYaw != (torque * LoopTime))
deltaVect.dYaw += torque * 1.5; // *8
ROC stands for Rate Of Change. These two functions determine the rate at which
the orientation of the craft is changing. The CalcTurnRate() function is called from
CalcROC() to calculate the current turn rate based on roll.
Finally, to cause the craft to rotate, we call the ApplyRots() function (Listing 16-14).
The Flight Simulator
mmm
This function applies the current angular rates of change to the current aircraft
rotations, and checks for special case conditions such as pitch exceeding +/-90 degrees.
HE BE BE BE B®
579
-
"Build Your Own Flight Sim in C++
roll axes is actually measured in a range of -180 to +180. This is done only as a matter
of preference. Even odder, the pitch axis is measured from -90 to +90. When it passes
outside of this range, it
is immediately mapped back into this range and one of the
other axes is
adjusted to compensate for the remapping. For instance, if the plane
pitches back until it
is pointing straight up, it
will have a pitch value of +90. But if it
then goes further and begins rolling over onto its back, the pitch value will be reset to
+89 and the yaw value rotated 180 degrees to indicate that the airplane is now point-
ing in the opposite direction. The ApplyRots() function performs this remapping. It
watches for values on the roll, yaw, and pitch axes that fall out of the ranges appropri-
ate to each and performs the appropriate wraparounds and other adjustments.
All that remains in the flight modeling at this point is
to determine where the air-
plane is located
to one foot in the
within the
virtual
world
world,
space. Assuming
the RunFModel()
that one world coordinate is equal
function calculates the new position
of the craft. The technique used here is based on the same three-dimensional rotation
equations that we studied earlier. Basically, the point at which the plane was located at
the time of the last frame of animation is assumed to be the world origin and the air-
plane is rotated around this position by its yaw, pitch, and roll values, then moved
along the z axis by the distance that it
has traveled since the last frame. Finally, it is
translated back to its original position plus the change that has just been added to it.
Thats for the flight model. We now need a frame program from which to call both
it
the flight model and the view system. Thats the role of the FSMAIN.CPP module. The
main() function from this module, which is also the main() function of our flight sim-
ulator, appears in
Listing 16-15.
f
Listing 16-15 The main() function
void main( int argc, char* argv[1)
{
window(1,1,80,25); // conio.h: set a text window
ctrsert); // conio.h: clear the screen
textcolor(7); // conio.h: set the text color
oldVmode = *(byte *)MK_FP(0x40,0x49); // store the text mode
opMode = FLIGHT; // assume normal operating mode
ParseCLP( argc, argv ); // parse command Line args
if (opMode == HELP) { // if this is a help run
DisplayHelp(); // then display the command
exit(0); // List and exit
>
if (opMode == VERSION) {
DisplayVersion();
exit(0);
}
StartUp();
steward.GetControls(theUserPlane); // dinput.cpp: run one control pass
// to initialize the AirCraft fields
The Flight Simulator
"ew
// *** main flight Loop ***
while(!steward.Exit()) (
steward.GetControls(theUserPlane);
theUserPlane.RunFModel ();
SoundCheck(theUserPlane);
GroundApproach();
if (!UpdateView( theUserPlane )) // make the next frame
Terminate("View switch file or memory
error" ,"UpdateView()");
if (opMode !'= DEBUG) // if not debugging...
blitscreen( bkground.Image() ); // display the new frame
else // else if debugging...
}
VectorDump(); // do the screen dump
checkpt = 4; // update progress flag
Terminate("Normal program termination", "main()");
}
The first part of this function determines whether we've typed any command-line
options. This enables us to put the program in a special debugging mode. In this mode,
instead of displaying a cockpit view, the program will produce a changing readout of the
values of variables. We used this mode to help in debugging it. However, we've left it in
so that you can see it in operation. For full instructions on how to use the debugging
mode, see the flight-simulator instructions in Appendix A. Alternatively, there are com-
mand-line options that will print out all of the available command-line options (the
HELP option), will print out which version of the program this is (the VERSION
option), and will draw frames without the airplane present (the WALK option).
The GetControls() function is called twice in main(). The first time it is
used to ini-
talize any fields in the AirCraft object. The FControlManager declared globally at the
top of the FMAIN.CPP module is called steward, while the AirCraft object is
declared
globally as theUserPlane.
Both the HELP and the VERSION options cause the program to terminate early. If
neither of these is chosen, the StartUp() function (Listing 16-16) is called which ini-
talizes the major comoponents of the program: its internal systems for input, video and
sound, as well as the object components which make up the flight model simulator.
checkpt = 1;
clrscrQ); // conio.h: clear the text screen
if (CopMode == FLIGHT) || (opMode == WALK)) { // if not debugging or
// walking...
setgmode( 0x13 );
ClrPalette( 0, 256 );
if (opMode == FLIGHT)
if ( !'DoTitleScreen() )
Terminate( "error loading title image",
"DoTitleScreen()");
}
if ( 'InitView( &bkground, opMode J)
Terminate( "Graphics/View system init failed", "main" );
checkpt = 2;
if ( 'theUserPlane.InitAircraft( opMode ))
Terminate( "Aircraft initialization failed”, "main()" );
checkpt = 3;
}
Then StartUp() calls steward. InitControls() to (surprise!) initialize the aircraft con-
trols. The InitControls() function must be called before putting the video card into
582 ®
The Flight Simulator
"Ean
graphics mode since it
uses console text output ask the user tothey want to use the
mouse, calibration of the joystick, etc. The InitControls() function is located in the
if
INPUT.CPP module, and appears in Listing 16-18.
return 0;
}
This function creates the HTimer for the input module. (This is the same name,
timerl, as the timer inthe aircraft module. They are distinct objects, however, since
both are declared as static and therefore local to their respective modules.) It then
checks the activeAxisMask to see if there’ a joystick attached to the users machine.
This was set in the FControlManager’s constructor in
the call to joydetect(). If theres a
joystick, it calls UseStick() (Listing 16-19) to prompt the user to
see if she or he wants
to use it. UseMouse() and UseStick() both call YesNo() (also Listing 16-19) since they
are so similar. If the joystick is being used, it
calls Calibrate() (Listing 16-20) to walk
the user through a calibration routine. (See Listing 15-21.) Our simpler method for cal-
ibrating the joystick from Chapter 5 could not be used since the HTimer object was
needed for measurement, but feel free to refer to that chapter for an explanation of joy-
stick calibration.
— Build Your Own Flight Sim in C++
boolean
{
}
return YesNo(
"=> A
———
FControlManager::UseStick()
YesNo() functions
boolean FControlManager::UseMouse()
a do you wish
EERE
{
return YesNo(
"=> A mouse has been detected... do you wish to use it? (Yy/Nn)");
¥
return false;
}
clrscril);
cout << "=———— Calibrating Joystick ————————" << endl;
cout << "\nMove joystick to upper Left corner, then press a button..." <<
endl;
while( !
EitherButton()) {
xmin = ReadStickPosit(JOY_X);
ymin = ReadStickPosit(JOY_Y);
¥
delay (50);
while( EitherButton() );
584 ®
The Flight Simulator
"Ew
cout << "\nMove joystick to lower right corner, then press a button.."
endl;
<<
while( EitherButton())
! {
xmax =
ReadStickPosit(JOY_X);
ymax ReadStickPosit(JOY_Y);
}
delay(50);
while( EitherButton() );
cout << "\nCenter the joystick, then press a button..." << endl;
while( EitherButton()) {
!
tempX = ReadStickPosit(JOY_X);
tempY = ReadStickPosit(JOY_Y);
}
tempX = tempX * (25500 / (xmax —=
xmin));
xcent = tempX/100;
tempY = tempY * (25500 / (ymax = ymin));
ycent = tempY/100;
cout << "\n...calibration complete." << endl;
The InitControls() function then puts the new keyboard handler in place, after which
it returns to the StartUp() function. The keyboard handler
is
safely installed by using the
setvect() function after storing the original interrupt handler as returned from the
getvect() function. We restore this older interrupt vector when our program terminates.
This is performed automatically in the FControlManager destructor (Listing 16-21).
return result );
The StartUp() function then performs initialization on the view system and the
flight model. To do this, it
calls two functions, appropriately named InitView() and
InitAircraft(). These two can be seen in Listings 16-23 and 16-24, respectively.
degree_mul /= 360;
current_view
= theBGround;
degree_mul = NUMBER_OF_DEGREES;
= 5;
)
opMode = mode;
if (SetUpACDisplay())
result = true;
}
}
return(result);
This function is called to initialize the view system. It's found in VIEWCNTL.CPP. The
pointer to a Pex object is passed from StartUp(), and is the same object used to load the
title screen at the beginning of the program. Since we never deal with more than one
PCX at a time we create a single Pcx object in main() and pass it to other parts of the
program as required.
The Flight Simulator
"ew
Listing 16-24 The InitAircraft() function
boolean AirCraft::InitAircraft( int mode )
{
boolean result = true;
LoopTime = 0;
if (C timer1 = new HTimer()) == NULL )
result = false;
else {
Now all of the initialization is out of the way, also call the GetControls() function
onceto initialize the AirCrafts state vector. Once that’s done, we enter the main flight-
simulator loop. This loop begins with calls to steward.GetControls() and theUser-
Plane. RunFModel() functions.
Now there are a couple of details we have to take care of. First, we need to check to
see if there have been any changes in those things in the program that generate sound,
so that we can change our sound output accordingly. This is the job of the Sound-
Check() function, which appears in Listing 16-25. This does little else but tell the
sound_CNTRL structure member that there is not a crash in progress. It then calls
ExtractSoundData() which transfers fields from the state vector to the sound_CNTRL
structure (see Listing 16-26).
=
static
{
Listing
void
16-26 The ExtractSoundData() function
ExtractSoundData(sound_CNTRL& cntrl, AirCraft& ac)
continued on next page
H BE 5B
Em 587
—— ——— mE
EE EEE
te
Build Your Own Flight Sim in C++
if(ac.sound_chng == true) {
sound_on = sound_on?false:true;
ac.sound_chng = false;
if( sound_on)
sound_Init();
else
sound_Kill();
}
if( sound_on) {
cntrl.rpm (word)ac.rpm;
cntrl.tpos nn
(word)ac.throttle_pos;
cntrl.airspeed (word)ac.h_speed;
cntrl.engine uw
ac.engine_on;
cntrl.airborne un
ac.airborne;
cntrl.stall ac.stall;
cntrl.brake ac.brake;
mun
sound_Update(&cntrl);
}
ShowCrash();
theUserPlane.ResetACState();
SoundCheck(theUserPlane);
delay (200);
while( !steward.AnyPress() ); // dinput.cpp
}
else
theUserPlane.LandAC(); // aircraft.cpp
}
}
}
The Flight Simulator
m mm
Now it’s time to translate all of this aircraft information into view information, so that
we can draw it on the display. This is a little complicated. So, naturally, an entire function
is devoted to it. This function is called to update the offscreen image buffer with the cur-
rent aircraft instrument display and view overlay. It also checks for changes in sound state
and toggles sound on or off in
response. See the Update View() function in Listing 16-28.
#%*
Listing 16-28 The UpdateView() function
boolean UpdateView( const AirCraft& AC )
{
boolean result = false;
ViewShift( AC );
// stuff the struct we'll send to the view system
MapAngles();
curview.copx = AC.x_pos;
curview.copy = AC.y_pos;
curview.copz = AC.z_pos;
if (ViewCheck( AC.view_state )) {
result = true;
winview.Display( curview, 1);
if (opMode !'= WALK) {
return(result);
This function first calls the ViewShift() function (see Listing 16-29) to check which
direction we are currently looking toward out of the cockpit and adjusts the view
variables for front, back, right, or left views. Then it calls MapAngles() (see Listing
16-30) to convert the rotation system of the AirCraft to the 256-degree system used by
the view system. The fields of a view_type variable are set to the appropriate position
values and the view system is called to draw the out-the-window view. The ctransput()
function (appearing in the SCREEN.ASM module and in Listing 16-31) is called to
decompress the PCX image of the instrument panel over the view, then Updatelnstru-
ments() (see Listing 16-32) is called to draw the current instrument settings on the
instrument panel. Then the main loop copies the entire contents of the screen buffer to
the video display.
acPitch = AC.pitch;
acYaw = AC.yaw;
acRoll = AC.roll;
acYaw += view_ofs;
switch (current_view) {
case 1:
{
int temp=acRoll;
acRoll=acPitch;
acPitch=-temp;
break;
}
case 2:
acPitch=-(acPitch);
acRoll=-(CacRoll);
break;
case 3:
{
int temp=acRoll;
acRoll=-=(CacPitch);
acPitch=temp;
break;
3
if C(acRoll >= 0)
acRoll -= 180;
else if (acRoll < 0)
acRoll += 180;
if (acYaw >= 0)
acYaw -= 180;
else if (acYaw < 0)
acYaw += 180;
if CacPitch > 0)
acPitch = (180 - acPitch);
else if (acPitch < 0)
acPitch = (-180 - acPitch);
The Flight Simulator
This function performs a final rotation on the view angles to get the proper viewing
direction from the cockpit. Note that the rotation values in the AirCraft’s state vector
mm
.
are assigned to the file scope variables acPitch, acRoll, and acYaw. Final view calculation
is done using these variables so that no changes have to be made to the actual AirCraft
object.
EL
Listing 16-30 The MapAngles() function
static
{
void near MapAngles()
floorCacYaw);
curview.zangle floorCacRoll);
}
The MapAngles() function changes the rotation system being used from the 180 to
-180 range in the flight model to the rotation range in NUMBER_OF_DEGREES
(FIX.H) used in the view system. The value of degree_mul is given by
NUMBER_OF_DEGREES/360, and is calculated once in InitAircraft().
rep movsw Ne
Otherwise, move run of pixels
randrun2:
jnc endloop Ne
Jump ahead if even
movsb Else move the odd byte
endloop:
cmp bx, 64000 Have we done all 64000?
jb Looptop If not, go to top of loop
done:
pop si
pop di
pop ds
pop es
pop cX
pop bx
pop bp
ret
_ctransput ENDP
592 ®
The Flight Simulator
mm m
theKphDial->Set( AC.h_speed );
theRpmGauge->Set( AC.rpm );
theFuelGauge->Set( AC.fuel );
theAltimeter->Set(AC.altitude);
direction = floor(AC.yaw);
if ( direction < 0)
direction += 360;
if (direction)
direction = 360 -
direction;
theCompass—->Set( direction );
theSlipGauge->Set( —-(AC.aileron_pos / 2) J); // slip gauge shows
// controls
theIgnitionSwitch->Set( AC.ignition_on );
if ((AC.brake) &&
(current_view == 0))
Line(23,161,23,157,12);
else
Line(23,161,23,157,8);
HE BB
8 BE = 593
The Three-
Dimensional
Future
How much will the flight simulators of tomorrow resemble the flight simula-
tors of today? Will three-dimensional animators continue to think in terms of polygon-
fill graphics of the sort that we've discussed in this book? Or are there bigger and better
597
"a Build Your Own Flight Sim in C++
3D Accelerators
The first type item to consider is not an alternative to polygon-fill graphics, but an
improvement to how that graphic is implemented. Instead of writing a book full of rou-
tines to transform, shade, and paint the polygon, why not let hardware do it? 3D
accelerator cards are video cards that include specific chips to perform such tasks as
matrix mathematics, perspective texture mapping, automatic Gouraud shading, anti-
aliasing, and Z-buffer sorting/clipping. Most of these cards are fully compatible with
normal VGA modes and many include advanced special effects like fogging effects and
mapping video on curved objects for use by the programmer/developer. The market for
these cards today is very similar to the market for sound cards when they were first
introduced. These 3D accelerator cards are not yet considered to be necessary for the
home multimedia PC, because there are only a few software companies to write games
for them and the price is anywhere from $100 to $250 higher than a high-end SVGA
card. Gradually, as more of these cards are introduced and improved, the prices will
come down and they will be accepted by the mass market, which will make more soft-
ware companies interested in developing for them.
There is no standard for 3D accelerator cards, so the method of describing a poly-
gon changes from one board to another. Some only work with triangles (so all polygons
have to be broken down into triangle components). Most come with a prepared API
library or driver by which to take advantage of the 3D chip set. Since the design of the
terrain and objects in the world become increasingly complex, it is important that the
data can be extracted from current 3D modeling tools (e.g., Autodesk's 3D Studio MAX,
MacroMedia’s Extreme 3D, and Caligari’s trueSpace2). Often programming develop-
ment time must be set aside for filtering and/or massaging data files from one format to
another.
The ATI Graphics Pro Turbo and Matrox MGA Impression Plus were early out on
the market with 3D boards. More recently, Creative Labs’ 3D Blaster and the Diamond
Edge 3D entered into the fray. Undoubtedly more are on their way. (At this time,
Western Digital and 3DO have announced development of similar boards.) Be prepared
for comparing graphics performance in terms of frame rates and polygons per second!
Polygon Smoothing
In Chapter 12, we investigated three methods of providing more realistic shading to
polygons. We used light sourcing in the final flight simulator, but no amount of light
sourcing can turn a polygon-fill object into a completely realistic representation of an
actual object. Whats needed is a combination of the effects explored in Chapter 12,
light sourcing, Gouraud shading, and texture mapping. Gouraud shading, as you
Em
might recall, helps to disguise the sharp edges of polygons so that they appear to be
The Three-Dimensional Future
"wu u
Ray Tracing
One of the most realistic of all the techniques currently in use for rendering three-
dimensional graphics on a computer screen is also one of the simplest. Unfortunately,
it’s one of the most time-consuming as well. That technique is ray tracing.
You've probably heard of it. Ray tracing has enjoyed tremendously good press in
recent years among computer-graphics aficionados, and with good reason. Ray-traced
images can be startlingly realistic, often eerily so. Fantasy universes can come alive in
a ray-traced picture to a much greater degree than in any motion picture. (As ray-trac-
ing techniques find their way into the cinematic special effects arsenal, though, this dis-
tinction may cease to exist.)
How does ray tracing work? It’s remarkably simple. (See Figure 17-1) A ray tracer
treats the video display of the computer as a window into an imaginary world and each
pixel on that display as a ray of light shining through that window into the eye of the
viewer. One by one, each of those rays of light (one for each pixel) is traced backwards,
from the viewer’ eye into the world inside the computer, and the intersections of that
ray with objects in that world are noted. If the ray passes through the world without
is
intersecting an object, the pixel assigned a background color, often black. If it inter-
sects the surface of an object, the intensity of all light sources falling on that surface is
calculated and the pixel is assigned the combined color of those light sources minus
any colors absorbed by the surface. If the surface is reflective, the angle of incidence of
the ray against the surface is calculated and the reflected ray is traced until it intersects
a second surface (at which point the surface calculations are performed) or passes out
of the world (and the pixel is assigned the background color). If the second surface is
also reflective, the process is repeated, ad infinitum (or until the recursive depth of the
tracer is reached). As each pixelis assigned a color, a bitmap is created and either dis-
played on the monitor or output to a file on the disk—usually both.
Although the description in the previous paragraph skimps on some details, that’s
really just about all there is to ray tracing. Conceptually, ray tracing is probably the
Build Your Own Flight Sim in C++
"EE NEN OE
Aol
simplest graphics rendering technique ever developed. But tracing all of those rays
takes time. A typical full-screen ray trace may take anywhere from half an hour to sev-
eral days, depending on the complexity of the scene being rendered. The more objects
in the scene, the longer it takes to determine
longer the trace requires.
if a ray intersects any of them and the
Bitmapped Animation
So, is
it possible to produce real-time animation with ray-tracing techniques? Not now
and probably not for a while to come. Even with clever optimizations, ray tracing just
takes too darned long to perform. About the only way it would be feasible at present
would be to build a custom animation computer with hundreds of thousands of par-
allel CPUs, so that each pixel on the screen could have its own processor assigned to
trace rays through it. With all pixels thus being traced simultaneously, real-time ray
tracing just might be possible. Such a custom animation computer would probably be
fabulously expensive, but perhaps not beyond the means of some Hollywood studio
interested in speeding up the process of computerized special effects (with an eye on
possible video game spin-offs).
Yet there is a way that we can have ray-traced animation on current generation
it
microcomputers. It isn't slow, isn’t fabulously expensive, and it’s actually being used
in computer programs that are available as you read these words. It’s called bitmapped
3D animation, and it
has become popular largely as the result of a series of space games
published by Origin Systems, Inc.
The principle behind bitmapped 3D is this: you don't build images on the fly, while
the program is running. You build them in advance, store them in memory, and copy
them to the video display in real-time. We've talked at some length about bitmapped
techniques in earlier chapters of this book, but we've ignored their application to
three-dimensional animation in favor of polygon-fill techniques. However, it’s time to
pull them out of the closet and take a look at them again.
If you've ever played the Wing Commander games from Origin Systems, you've seen
bitmapped 3D in action. (The most recent version is Wing Commander III.) These
games are space-combat simulators, in which spaceships maneuver about in real-time
on the video display. At their best, the spaceship images can look nearly photographic,
but they are not photographs. They are ray traces. Not real-time ray traces, though.
They are images that were ray traced while the game was being developed, and then
saved as bitmaps. These bitmaps are animated while the program is running.
Yet these bitmaps behave much like polygon-fill images. They can be rotated and
scaled, moved around on the screen on all three coordinate axes. How is this possible?
Lets look at that question in some detail. In order to
store all of the images necessary
a
to depict single spaceship being rotated to all possible angles at all possible distances,
a huge amount of RAM would have to be used. But the original Wing Commander game
ran on microcomputers with only 640K of RAM, so this is
clearly not what is happen-
ing in these games. Apparently, the Wing Commander graphics are a clever combination
of stored bitmaps and graphic images created on the fly.
I am not privy to the programming secrets of the people who created Wing Com-
mander, but I can guess some of the basic techniques. As you watch the game you can
see that the rotation of the spaceships isn’t especially smooth, certainly not as smooth
The Three-Dimensional Future
mm
m ~~ ~N
N
as it would if the game used polygon-fill techniques (though the bitmap techniques
make up for this through sheer realism). This means that the objects haven't been
rotated to all possible angles, but probably only increments of about 30 degrees on
each axis. Even so, that’s a lot of images, especially if all three axes are used. But
of
it isn’t
necessary to store images a bitmap rotating on its z axis, because z-axis rotations
don't bring any new information into the picture. These rotations can be calculated on
the fly by rotating each pixel around the center of the image mathematically. The cal-
culations for doing this are fairly slow, but there are no doubt techniques that can be
used to speed them up. In fact, many of the techniques described in this book for rotat-
ing vertices are probably at work here, in somewhat modified form.
Rotations on the z axis can be calculated while the game is running, but rotations on
the x and y axes cannot, at least not in all situations. Thus, these rotations would need
to be performed while the advance is taking place and stored for later animation.
Assuming 30-degree increments of rotation on the x and y axes, only 12 images would
need to be stored for each axis. But because rotations can be performed on the two axes
simultaneously, the images for both x- and y-axis rotations that need to be stored
equal 12 times 12, or 144. That's still a lot of bitmaps, but the number is becoming
manageable. And there are techniques that can prune this list still further. For instance,
if the object being rotated is bilaterally symmetric—that is, its left side looks like its
if
right side—then a 30-degree, y-axis rotation will be the mirror image of a 330-degree,
y-axis rotation, so there's no need to store both of them. One can be created by revers-
ing the other (i.e., copying the pixels to the screen in the opposite order on each scan
line). That means that only 6 y-axis rotations need be stored, for a total of 72 images
instead of 144. There may be other tricks that can cut down on the number of images
as well, particularly if the object exhibits other useful symmetries.
Even when we've taken rotations into account, though, it remains necessary to
scale the images so that these same rotations can be depicted at a variety of distances.
It would surely be impossible to store all of these images in memory not just once but
several dozen times, for each distance from which we wish to view them. Fortunately,
there are methods for scaling a bitmap to make it grow larger and smaller, as though
the image were coming closer or receding into the distance. The simplest is to add and
subtract pixels while the image is being drawn. Removing every other pixel, in both the
horizontal and vertical dimensions, makes the image seem twice as far away. Doubling
every pixel makes the images seem half as far away. Of course, this has the unfortunate
side effect of making the image seem strangely blocky, but anybody who's ever had a
close encounter with a Wing Commander spaceship knows that this is precisely what
happens in that game.
The ray-tracing method is put to a different use in 3D maze games, the most popu-
lar being Doom and DOOM II by ID Software. In these games, the ray-tracing engine is
made into a demon, which is an interrupt-driven process constantly being checked and
given a slice of the processing time. Similar to Wing Commander, the game uses a
~~ Build Your Own Flight Sim in C++
~~ 4A Em HE mE
EH
limited set of views of the major characters in the game. These are stored as a sequence
of sprite animations. Since the screen will only draw things that are in the viewing
volume, a sequence of rays are cast at a fixed angle from the left side to the right side,
and the intersecting background, wall, and/or sprite are projected onto their offscreen
to
buffer. This idea of tracing rays identify the objects within a viewing volume (view-
ing volume was discussed in Chapter 13) can be used in other 3D based programs.
Do-It-Yourself Bitmaps
Since bitmapped 3D requires powerful image-creation techniques such as ray tracing,
you might think that they would be beyond your means. But all you need to get
started with bitmapped 3D is a ray tracer, a little imagination, and the time for exper-
imentation.
Where are you going to get a ray tracer? There are a number of commercial ray trac-
ers on the market, but they tend to be expensive, often costing thousands of dollars.
On the other hand, there are also public-domain ray tracers available, which won't cost
you a cent. The best, and most popular, of these is the Persistence of Vision (POV) ray
tracer, which is available for download from several commercial information services,
as well as a number of local BBSs. It can also be purchased as part of the books Ray
Tracing Creations and Ray Tracing Worlds, both from Waite Group Press.
Although free, POV is a remarkably powerful program. The main difference
between POV and a commercial ray tracer is not in the quality of the images, which are
often quite stunning, but in the lack of a slick interactive interface for creating three-
dimensional scenes. With POV, images are created by editing an ASCII text file, just as
they are in the programs we've presented in this book. POV boasts a surprisingly sub-
tle scene-description language that can create a wide variety of objects and put them
together in elaborate settings. The program is currently in its third version. A number
of POV images are available through public-domain software channels. These range
from pictures of fish descending marble staircases to jade tigers having encounters with
purple snails. What all of these images have in common is that they look far more vivid
and realistic than any computer-scanned photograph possibly could.
We don't have space in this book to discuss three-dimensional bitmapped tech-
niques in any more detail than we've gone into here. But if you want to explore this
important aspect of 3D programming on your own, I suggest you obtain a copy of POV
and experiment with it. (The program is available for the IBM PC, Apple Macintosh,
and Commodore Amiga computers.) Create a small object and learn how to save
images ofit to the disk. (POV comes with a documentation file explaining how to do
these things, but Ray Tracing Creations also offers invaluable tips on such matters.) Build
a library of bitmaps showing the object rotated at 30-degree increments on its y axis,
then write a bitmap display routine (using the techniques we showed you earlier in this
book) to display these images in sequence on the screen. Presto! Instant ray-traced ani-
mation! In real time, too.
604 =
The Three-Dimensional Future m
mm
Betz has modeled the cockpit graphics after genuine cockpits of the era (while taking a
few liberties that aficionados of early manned flight will doubtlessly notice and quite
possibly tell me about).
To boot the program from DOS, change to the directory where the executable file is
stored and type the following at the DOS command prompt:
FSIM
FSIM will also run under Microsoft Windows. If you'd prefer this option, the file
FSIM.ICO (included in the FSIM directory) should be placed in the same directory
with the file FSIM.EXE, so that the program can have its own Windows icon.
The FSIM command can optionally be followed by a space and one of several com-
mand-line arguments, which are listed in Table A-1. (These arguments are explained in
greater detail later in this appendix.) Note that if you forget any of these prompts or the
controls for the simulator itself, you can invoke the simulator with the command:
FSIM H
or
FSIM ?
to activate help mode. This will cause a help screen listing the command prompts and
control keysto be printed. The program will then abort.
If you have a joystick plugged into your machine, you'll be asked if you wish to use
it. If you type Y (for “Yes”), the program will step you through a joystick calibration
routine similar to the one discussed in Chapter 5. If you type N (for “No”), the program
will check to see if you have a mouse connected to your machine. If you do, it will ask
you if you want to use the mouse.
Once you're into the program proper (assuming you haven't activated any special modes
using one of the command line arguments), you'll see the title screen. Press any key to con-
tinue or wait a few seconds for the program to continue on its own. You'll then find your-
self in the cockpit of the generic Build Your Own Flight Sim in C++ biplane, staring out a
window above a control panel covered with dials, lights, and switches. (See Figure A-1.)
Appendix A
mm “
A
Figure A-1 The cockpit of the Build
in C++ biplane
~
Here's a quick rundown on what these dials, lights, and switches do.
® The reddish light on the left-hand side of the panel with the letters BRK
beneath it is the brake light. It indicates whether or not the wheel brakes are
currently turned on.
® Below the brake light is the compass. It rotates as you turn the aircraft, with
the letters S, E, N, and W indicating headings of South, East, North, and West,
respectively.
® To the right of the brake light is the fuel gauge. In the current version of the
FSIM,
it is inoperative; it was thrown in mostly for show (and to allow future
fuel.
expansion). You can fly indefinitely without running out of
® To is
the right of the fuel gauge the altimeter, which tells you how high above
sea level you are flying. It conveniently starts at zero when you are sitting on
the landing field and will move clockwise as you climb.
®
is
To the right of the altimeter the airspeed indicator, with your airspeed
marked off in miles per hour (MPH). As you go faster, itwill move clockwise.
® Below the altimeter and airspeed indicator is the slip gauge, which shows your
angle of bank (i.e., your orientation on your local z axis, as described in Chap-
ter 16). Although the information provided by a real slip gauge is complex, this
one simply follows your joystick motion. After watching it for a while during
flight, you should be able to tell how the position of the ball in the slip gauge
corresponds to your current angle of bank (z-axis rotation).
og Build Your Own Flight Sim in C++
7 al
(F]
[+]
‘® Below the tachometer are three switches. The two on the left don't do anything
(B)
and are there just for show. The one on the right turns on your engine; that is,
when you activate your engine (which you'll learn how to do in a moment),
this switch will go down. In effect,itis an engine indicator. When it is down,
(F4)
(F1]
your engine is on; when it is up, your engine is off.
® Above and to the right of the engine switch is the real engine indicator. When
this small light comes on, your engine will be on. When it goes off, your
engine will be off. (You'll also hear an engine noise when your engine is
on.)
The basic flight controls are listed in Table A-2. Using these controls, you should be
able to take off, climb, turn, and even look out the four sides of your craft.
Let’ step through a short flight with the FSIM. This walkthrough (or flythrough)
will get you off the ground; figuring out what to do then—like how to land your
plane—is up to you.
Before you even leave the landing field, press the (F1), (F2), (F3), and keys in any
order to take a look around your craft. Each key lets you look in a different direction,
as described in Table A-2. Notice that the control panel is only visible in the front view;
you'll see other parts of your craftto the side and back. (You can still fly while looking
to the sides, even work the controls; you just can't see the instruments.)
Now return to the front view by pressing and press the key. (It doesn’t mat-
ter whether you press or have the key on when you do this, since the
FSIM recognizes both lowercase and uppercase control keys.) The brake light will go
off, indicating that your wheel brakes are no longer on. Start your engine by pressing (I)
(for Ignition). You'll hear the sound of your engine revving up and see your tachome-
ter start to move clockwise. Rev your engine to about 15 or 20 RPMs by tapping (or
holding down) the key. After a few seconds, your airspeed gauge will start moving
clockwise and your craft will begin taxiing forward.
When you reach about 80 MPH, press the (1) key and keep on pressing it (or pull
back on the joystick) until you start to climb. Now you're in the air!
What do you do next? That’s up to you. Play with the controls until you get the
hang of flying the aircraft. (If you frequently fly microcomputer flight simulators, you
shouldn't have much trouble. If you're a real-life pilot, it might take a moment for you
to adjust to some of the simplified aerodynamic assumptions built into the flight
model, but if you don't expect too much realism, you should be okay.) Once you figure
out what you're doing, take a look around. Enjoy the scenery. Remember that you have
four views that you can examine that scenery from. The scenery database included on
612 ®
AppendxA m m m
Table A-2 M Flight controls for the Build Your Own Flight Sim
in C++ flight simulator
Engine controls:
(+)
lori Toggle ignition/engine on and off
(Y)
(On numeric keypad)
Aircraft controls:
~
View controls:
| FA) Look forward
Look right
|
[3 Look behind
| Look left
|
Sound control:
| Sors Toggle sound on/off
the disk is relatively small, but has a few surprises in it (not all of them realistic). Most
of all, enjoy your flight!
When you get tired of looking around, hit the key and you'll drop back to DOS
or Windows, whichever you booted the program from. Next time you run the pro-
gram, you might try some of the special command-line options that are included. The
special debug mode, for instance, fills the screen with a dump of the variables used in
the flight model, as described in Chapter 16, so that you can see how they change with
your input. The version-number mode simply tells you which version of the program
you are using. The world traverse mode is intended for debugging scenery databases;
it allows you to move around using an extremely simplified flight model, in which you
climb and turn using the cursor arrows and accelerate and decelerate using the and
(J) keys. I don't recommend using this mode for your initial exploration of the FSIM
:
Build Your Own Flight Sim in
0 ——————
a EEE
flying along through perfectly open space, then suddenly find yourself heading
directly toward a mountain—or inside of one! In a commercial flight simulator, this
problem would be avoided by loading a new scenery database when the user reaches
the edge of the current one, but this is a no-frills, single-database simulator.
Have fun! Although it simulates an aspect of the real world, the FSIM is above all
a game—a game that you are encouraged to examine, alter, and learn from, to your
hearts content.
614 ®
Bibliography
Berger, Marc, Computer Graphics with Pascal, Benjamin Cummings, Menlo Park, Cali-
fornia, 1986.
Foley, J.D., and A. Van Dam, Fundamentals of Interactive Computer Graphics Second
Edition, Addison-Wesley, New York, 1990.
HE BE BE
EB 615
Build Your Own Flight Sim in C++
————& Bm HB
BER
Hearn, Donald, and M. Pauline Baker, Computer Graphics Second Edition, Prentice-
Hall, Englewood Cliffs, New Jersey, 1994.
Hyman, Michael, Advanced IBM PC Graphics: State of the Art, Brady, New York, 1985.
LaMothe, André, Black Art of 3D Game Programming, Waite Group Press, Corte
Madera, California, 1995.
Stevens, Roger T., Graphics Programming in C, M&T Publishing Inc., Redwood City,
California, 1988.
617
~~" Build Your Own Flight Sim in C++
ee HE BE
EEE ER
One solution to this problem is to use a programming tool library. In this appendix,
we'll show you how to use DiamondWare'’s Sound ToolKit to make the flight simulator
sound even better, and we'll add some music (by David Schultz), too! The sound
library which we’ll use is the Diamond Sound Toolkit, available from DiamondWare.
DiamondWare’s STK supports MIDI music played through the FM chip, and 16 chan-
nels of digitized sounds.It can also auto-detect the port, DMA, and IRQ settings used
by the sound card. This is a shareware product and should be registered with Dia-
mondWare at 2095 N. Alma School Rd., Suite 12-288, Chandler, AZ 85224. All of the
code for this version of the program can be located in the FSIM_STKSOUND directory
(and its subdirectories) on the distribution CD.
The remainder of this chapter shows you how to put DiamondWare’s Sound ToolKit
into the flight simulator. We're going to design the code to fit the same API as the FM
module; thus this code will plug into the FSIM without any other changes. For a com-
plete tutorial, guide, and reference to the STK, please read STKMAN.DOC, included
with the shareware distribution.
We'll present sound_Init() first, and then sound_Update() because you're already
familiar with them from our FM implementation.
dov.baseport = (word)-1;
dov.digdma = (word)-1;
dov.digirqg = (word)-1;
These three lines initialize a struct required by the auto-detect function, below. In this
case, we wantto auto-detect port, DMA, and IRQ, without any manual overrides.
if (dws_DetectHardWare(&dov, &dres)) {
else {
err_Display(dws_ErrNo(), err_DWS);
}
The first thing to notice about this sequence of code is that the return value of
dws_DetectHardWare() is compared to 0. Like most functions in the STK, itreturns 0
if there is an error. We should not encounter an error in this auto-detect function
(failure to find a sound board installed is not an error). dws_DetectHardWare() takes
two pointer parameters: The first is a pointer to dov, the dws_DETECTOVERRIDES
structure which contains the parameters set in the lines above; the second points to the
dws_DETECTRESULTS struct dres, which will be filled in during the call. Later,
dws_Init() will need these auto-detect results.
Now we want to initialize the STK. The following lines specify how we want
it set
up and should be self-explanatory from the comments.
ideal .musictyp Bs
1: //0PL2 FM music
ideal.digtyp = 8; //8-bit
ideal.digrate = 11000; //11kHz
ideal .dignvoices = 16; //16 channels
ideal .dignchan = 1;
if (dws_Init(&dres, &ideal)) {
//mono
Appendix C
nN
.w
This function call works the same way as dws_DetectHardWare(); itreturns nonzero if
everything worked. In this it
case, uses the dws_IDEAL struct just initialized, and
we
the results of the auto-detect to set up the sound board (and the STK) properly.
initted = true;
atexit(Kill);
LoadIt("music\\moon2.dwm", &song);
LoadIt("sound\\starter.dwd", &starter);
LoadIt("sound\\swtch.dwd", &swtch);
LoadIt("sound\\wind.dwd", &windorg);
LoadIt("sound\\stallbuz.dwd", &stallbuz);
LoadIt("sound\\plane.dwd", &planeorg);
LoadIt("sound\\plane.dwd", &planel);
LoadIt("sound\\plane.dwd", &plane2);
LoadIt("sound\\brake.dwd", &brake);
LoadIt("sound\\landing.dwd", &landing);
LoadIt("sound\\crash.dwd", &crash);
We're just loading the song and sounds we'll need later.
//starter is too loud, make it more quiet
if ('dwdsp_ChngVol(starter, starter, 160)) {
err_Display(dwdsp_ErrNo(), err_DWDSP);
}
//0nly
Your Own Flight Sim in C++
if (dres.capability
do
&
dws_capability_DIG)
a
Updates if digitized sound is available
When we called dws_DetectHardWare() (during sound_Init()),
——Sa
it
filled in the
dws_DETECTRESULTS struct. Theres a lot of information in there, including the
A EER EE®
capabilities of the sound board installed in the machine. The conditional above is
checking to see if the sound board can play digitized sounds. If not, let's not bother
with anything else; the music will keep playing without our help. Fortunately, most
cards support digitized sound playback.
if (cntrl->sound == false) {
}
dws_XMaster (0);
else {
dws_XMaster (242);
}
The sound field of the cntrl structure tells us whether the FSIM wants sound/music to
play at the moment. If not, we'll turn the master volume down to 0.
UpdateEngine(cntrl);
UpdateBrake(cntrl);
UpdateLanding(cntrl);
UpdateStallB(cntrl);
UpdateCrash(cntrl);
UpdateWind(cntrl);
UpdateSwitches(cntrl);
This updates the individual sounds, just as we did for FM.
We'll leave UpdateEngine() and UpdateWind() for last, since they're the most com-
plex. First, lets take a look at UpdateBrake().
dplay.snd = brake; //set up ptr to snd
dplay.count = 1; //set up for 1
rep
dplay.priority = 3000; //give it a priority
dplay.presnd = 0; //start it immediatly
if ((lastbrake == false) &8&
//brake was just hit
(cntrl->brake == true) &%&
Flight simulators, like most interactive entertainment programs, are state-driven. What
the player is doing now, and what he was doing last update, determines how the pro-
gram should react. This function is responsible for playing a brake-squeal noise. We
want to make sure that we only play it once, that its played when on the ground, etc.
if (!dws_DPlay(&dplay)) {
//play a brake noise
err_Display(dws_ErrNo(), err_DWS);
}
620 =
Appendix Cm m m
}
if ( (soundplayed true)
== &%& //sound was played and
C
(cntrl->airspeed == 0) || //plane has stopped or
(cntrl->brake _== false)]|| //brake has turned off or
(cntrl->airborne == true) ) //plane went airborne
)
This conditional expression merely determines when it’s time to stop the brake-noise
sound.
{
HE HE BE BE B®
621
—
Build —
Your Own Flight Sim in C++
————————— mE
EE
if ((soundplayed == true) &%&
//engine sound has
(!Cresult & dws_DSOUNDSTATUSPLAYING))) <
//run out
and died
}
count++; //play it Longer next time
Note how we wait until the sound drops out before incrementing the count. We're
dynamically tuning our application to the user's machine, without making assumptions
in advance. This is a very good habit to get into.
Next, the result is tested to determine if the sound is not playing, or playing and not
sequenced.
Whereas pitch varies with RPMs, volume varies with throttle position. Obviously
for our purposes.
this is quite oversimplified from reality, but it works well enough
if (ldwdsp_ChngVol(planel, planet, volume)) {
err_DWDSP);
err_Display(dwdsp_ErrNo(),
}
buffer
Unlike pitch change, volume can be changed in place; that is the destination
this, then UpdateWind() falls
can be the same as the source buffer. If you understand
quickly into place.
DiamondWare’s Sound ToolKit is well suited to games; with the included
DSP code,
which
its also suited to flight simulators, auto-racing games, and other applications
DSP. There are two functions
must simulate engine noise. Lets take a look at this
we've been using; one to change volume and one to change pitch. Since volume is sim-
errnum = dwdsp_NOTADWD;
return(0);
}
if
{
x
(srcdwd
CopyDWDHdr(srcdwd,
!'= desdwd)
—_—
desdwd);
//if
eS BREE
the source is different from
//the destination copy the header
}
tmp = MAXPOSVOL;
//clip if it exceeds the
//dynamic range
tableltblidx] =
(byte)tmp;
/* Negative numbers */
volume = MAXNEGVOL + (int32) loop;
tblidx = (word) (HALFTABLE + Loop);
tmp = ((volume *
(int32)vollev) / dwdsp_IDENTITY);
if (tmp < MAXNEGVOL)
{
}
tmp = MAXNEGVOL;
//clip if it exceeds the
//dynamic range
tablelLtblidx] = (byte)tmp;
}
dd->hdr.maxsample = tmp;
Now, let's take a look at dwdsp_ChngLen(). As mentioned earlier, length and pitch
are integrally related. Changing the length can be accomplished simply. If we want to
make a sound longer than it was, we can repeat some or all samples several times. If we
want to make it shorter, we can omit some samples. This may sound too simple to
work, but it actually works passably well!
lcm = LCM(srclen, deslen);
need the Least Common Multiple later. Check out the LCM() function;
We it’s
for (x=0;x<deslen;x++)
{
tmp ((double)x * (double)lcm) / (double)deslen;
=
interpidx = (dword)tmp;
tmp = ((double)interpidx * (double)srclen) / (double)lcm;
interpsmp = (dword)tmp;
desdatal(word)x] = srcdatal(word)interpsmpl;
}
The work-loop of pitch change is surprisingly simple. The entire desdata buffer array is
assigned a srcdata sample from the computed index interpsmp. The arithmetic deter-
mines which source sample is closest.
Thats it! It was pretty straightforward to convert to
the use of the STK. Now we
have the FSIM playing several digitized sound effects plus music.
HE BE BE EB
= 625
il Your Own Flight Sim in C++ "EEE
Possible improvements from here would be: 1) you could make a better model for
throttle and RPM affecting volume of engine noise. Clearly in the real world, throttle
affects volume, but RPM affects it more. You could also model wind noise better,
higher speeds probably mean higher frequency wind noise. 2) You could reduce the
memory footprint of the sounds, by dynamically loading many of them on an as-
needed basis.
Index
asm, 566-567
ENDP, 61, 65
address, memory, 14-15, 62-64 EQU, 74
address operator (&), 15, 88 LOCAL, 185
address, pixel, 31-34 PROC, 61
ailerons, 558, 563, 565 assembly language, 21, 61-67
AirCraft class, 560-562 assembly language instructions. See instructions
algebra. See math assignment operator override, 373-376
algorithms assignment statement, 41
Bresenham’s, 176-178, 183, 261, 266, attack, angle of, 556, 576-577
416-423 AX (AH/AL) register, 62, 68-69
Painters, 322-333, 336-337, 344, 346 axis, 29-31, 35
polygon-drawing, 261
Sutherland-Hodgman, 364-366
Z-Buffer, 333-335
backface removal, 281-284, 322, 338, 340, 346,
aliasing, graphics, 181
473-474
aliasing, sound, 524-525 See alsohidden surface removal
ambient light, 396-398, 403-404, 409, 478
background color, 73
AND operator (&), 130-131, 141
background, save/restore, 107-108, 110, 120
AND operator (&&), 130
bank, angle of, 609
angle of attack, 556, 576-577
EE BE BE
EB 627
2 BB
HE HE BE BE BE
HE HE BE BE B=
629
Build Your Own
—
632 m
Index m w
634 ®
optimization
animation, 112
animation restriction, 312-313
pixels continued
number of, 10-12
transparent, 93, 111-112, 120
Index
"se
design tradeoff, 300 plane coefficients, 349-353
fixed-point math, 300-305 plane equation, 349-353
fractals, 511 point-slope equation, 368-369, 381, 383
Gouraud shading, 432 pointers, 15-17, 228-229
look-up tables, 305-309 array, 254, 256
loops, 294-295, 309-312 assembly language, 62-64
matrix math, 52 POLY.H file, 253, 284
maximizing, 294-295, 298-299 POLY2 H file, 336-337
profiling, 294-300 polyBase class, 370-373, 404, 407
optimization, texture mapping, 433, 441 PolyFile class, 286-288
OR operator (|), 130-132 polygon (defined), 251-252
OR operator (II), 130 Polygon class, 339, 341, 347, 370, 373, 500
origin, 31, 33, 45, 50, 216-217, 221, 225, polygon clipping, 357-358, 363-366
239-240 edges, 366-369, 376-378
bitmap, 434, 440 functions, 375-389
local, 192-193 polygon color, 254, 269
overlap, polygon, 325-332, 346-353 concave, 257-261
convex, 257-261, 284
polygon descriptor, 251-256, 479
polygon drawing, 257-280
paint programs, 80-81 polygon-fill graphics, 5, 168-171, 251
Painters algorithm, 322-333, 336-337, 344, 346 polygon list, 337-341, 353, 458, 471-472, 508
palette, 19-20, 75-79 polygon manipulation, 280-284
and shading, 400-405, 408-411 polygon ordering, 325-332, 346-353
See also color polygon shading. See shading
Palette class, 400-401 polygon_type class, 253-256, 282, 284
parameters, passing, 65-66 PolygonList class, 337
parsing functions, 286-288 port number, 129
Pcx class, 85-87, 433, 436 ports, 128-129
PCX files, 80, 82-85 POV ray tracer, 604
PCX loader/viewer, 82-99 pressure differential, 553-556
PCXMODE, 90 primary colors, 20, 394-395
perspective private members, 85, 88
calculations, 433, 438, 441, 466 PROC (procedure), 61
and clipping, 363, 375 profiling, 294-300
effect of, 165, 359 program code (by name)
and projection, 223-229, 240, 363 3DWIRE.CPP, 240
Phong shading, 167, 599 BRESN.CPP, 178-180
pitch (movement). See x axis rotation BRESNHAM.ASM, 183-185
pitch (sound), 516, 526, 537, 617, 619, 621, 623 CUBE.CPP, 243-244
pixels, 6 DRAWPOLG.CPP, 423-427
address, 32-34 DRAWPOLY.CPP, 271-280
color, 19-20, 32-34, 59, 70-73 FIX.CPP, 306-309
coordinates, 31-34 FIX.H, 306, 309
matrix, 172-173 FRACDEMO.CPP, 494-511
- ~~ Build Your Own Flight Sim in C++
HE BE BE
NB 637
~~ Build Your Own Flight Sim in C++
——
transformation continued view volume, 358-366
view, 451-452 viewer position, 446-450
translation, 44-45, 50
matrix, 53, 206-207, 210, 235, 452-454
shapes, 192, 194-197, 199, 229-230, 235
transparent pixels, 93, 111-112, 120 W (3D letter), 245-248
triangle, 186, 191, 201, 204-205 Walkman animation, 116-122, 154-162
Turbo Debugger, 310 WIRE H file, 228
Turbo Profiler (TProf), 185, 294-299, 309-310 wireframe graphics, 168-172
typecast, 17, 70 WORD, 63, 66
types. See structures World class, 339, 399, 402, 411, 501, 506-508
world coordinates, 216, 219, 221, 455-456, 508
world_type class, 256-257, 283-284
world units, 218
unary minus operator, 180
unsigned int type, 54, 73
explained, 47-48
vectors, 51-52
in flight, 549, 558-559, 580, 603
light, 396, 398-399, 405-409, 427-429, 478
and horizon, 461, 465
math, 435-439
normal, 405-409, 427-430 math, 50, 53, 229-230, 236
x coordinate, 30, 35
texture-mapping, 434-439
vertex, 37-40, 49-50
array, 254
defined, 169
descriptor, 170-171, 188-189, 193, 206, 479 y axis, 30-31, 35-36, 216
list, 169-170, 255-256 y axis rotation
normals, 427-430 explained, 47-48
in flight, 548, 558-559, 579-580, 603
polygon, 254
shared, 169, 171, 428, 496-497 and horizon, 461
VESA (Video Electronics Standards Association), math, 50, 53, 229-230, 237
9-10 y coordinate, 30, 35
VESA mode, 9-10 yaw. See y axis rotation
VGA (video graphics array), 8-12, 59-60, 394-395
card detection, 582
programming, 20-22
video BIOS, 22, 60-61, 67-70 z axis, 36, 216, 229-230
video display, 6-8 z axis rotation
video memory, 12-19, 73-75 explained, 47-49
video modes, 8-10, 19, 59-60, 68-70 in flight, 558-559, 580, 603, 609
video RAM (vidram), 70, 79 and horizon, 461, 465-466
view, aligning, 450-455 math, 53, 229-230, 237-238
View class, 375, 380, 387, 456-457, 474 z coordinate, 36, 215-223, 322-325
view field, 223-229. See also perspective z sort (depth sort), 322-333, 336-353, 472
view, horizon, 460-471 Z-Buffer algorithm, 333-335
view system, 445-446, 546-550, 586, 589-591 zeroing digits, 131-132
Books have a substantial influence on the destruction of the forests of the
Earth. For example, it takes 17 trees to produce one ton of paper. A first
printing of 30,000 copies of a typical 480-page book consumes 108,000
pounds of paper, which will require 918 trees!
Waite Group Press™ is against the clear-cutting of forests and sup-
ports reforestation of the Pacific Northwest of the United States and
Canada, where most of this paper comes from. As a publisher with sev-
eral hundred thousand books sold each year, we feel an obligation to give
back to the planet. We will therefore support organizations which seek to
preserve the forests of planet Earth.
JIE
i Eee
be
Black Art
u
Ta a AT
1&4
AST
el0NCh Fn Ne
Ta
((Jindows
Programming 2,
ATE "
®t)
__((oame
Toe
Conjuring Professional
Games with Visual Basic 4
QE RET
Lr
LRT Ue
CIT THE BLACK ART OF
TTT
{eT [e
CCQ VISUAL BASIC GAME
WRITING YOUR OWN HIGH-SPEED ROGRAM
3D POLYGON VIDEO GAMES IN C CONJURING PROFESSIONAL GAMES DEVELOPING HIGH-SPEED GAMES
André LaMothe WITH VISUAL BASIC 4 WITH WinG
This hands-on, step-by-step LET RT] Eric R. Lyons
guide covers all essential game- Ihe Black Art of Visual Basic The Black Art of Windows Game
writing techniques, including Game Programming shows you Programming [EH CERT EET)
wire [rame rendering, hidden how to
exploit the power of use the WinG DLL to write
surface removal, solid model Visual Basic and the Windows high-performance Windows
ing and shading, input devices, API to write games with blaz- games, No) you've never
artificial intelligence, modem ing graphics and full sound. developed Windows programs
communications, lighting, Step-by-step instructions and before, This book teaches real-
transformations, digitized VR
sound and music—even voxel
carefully explained code reveal
cach phase of high-end game
world game
LETT
techniques, featuring practical
graphics, The CD-ROM development. A CD-ROM with examples, hints, and code.
includes a 3D moadem-to- the games and source code is LTH ES
DIVER TTTT
modem space combat simula- included, so that you can add ISBN: 1-878739-95-6
tor that you build from the working code modules to [VESEY HCL CTRL UT
UTTER Your own projects 1~CD-ROM
Send for our unique catalog to get more information about these
ight. Lt
LS
~~
CELI
Pari
ULE
Spells of Fury shows both game Scott Stanfield and Ralph Arvesen
The best-selling OOP programming
programming beginners and Master developers share hun
hook now incorporates the sugges-
veterans how to use the dreds of tips, tricks, and tech-
tions [rom an extensive survey of
Windows 95 Game SDK, niques for programming in
instructors—updated chapters on
DirectX to build visually elec- HREM TERE TB RESETS RETRY
objects, classes, overloading, con-
trifying Windows games, clear- C++ development environment
ly demonstrating the astound-
structors, inheritance, and virtual This book/CD-ROM package
functions, and the newest features
game development capabil- offers practical, step-by-step solu-
ities of Microsoft's new APls
of the C++ programming language, tions to thorny VC++ problems
The CD-ROM includes fully
including templates and excep- in an easy-to-use Q&A format,
tions, It's fully compatible with the
operational Windows 95 The CD includes all the examples
newest Borland C++ 4.5 compilers,
games, complete with source from the book as well as free cus
and the disk includes all the pro-
code and art tom controls and classes,
grams and source code.
:
Available July 1996 700 pages Available July 1996 © 700 pages
ISBN: 1-57169-067-0 PATER TARR LTE ISBN: 1-57169-069-7
ISBN: 1-878739-73-5
TERRA TEC XRD] U.S. $44.99 Can. $59.99
U.S. $34.95 Can, $48.95
1-CD-ROM
1-3.5” disk
(EOS)
TO ORDER TOLL FREE, CALL 1-800-368-9369
TELEPHONE 415-924-2575 o FAX 415-924-2576
OR SEND ORDER FORM TO: WAITE GROUP PRESS, 200 TAMAL PLAZA, CORTE MADERA, CA 94925
US/Can Price Total Ship to:
Qty
™ hk Art of 3D Game Programming $49.95/72.95 Nome
So
The
The
Black Art
Black Art
of VB
of Windows
Game Programming $34.95/50.95
Company
Game Programming $34.95/50.95
Spells
Address
of Fury $39.99/53.99
$44.99/59.99
Shipping
Payment Method
USPS (S5 first book/S1 each add)
UPS Two Day (510/52) 07 Check Endosed 1 VISA C0 MasterCard
Ce (510/34)
— (urd#
Signature
Exp. Date
.i
A
a!
(@)
PY This is a legal agreement between you, the end user and purchaser, and The Waite Group®, Ir
pr and the authors of the programs contained in the disk. By opening the sealed disk package, you
wn
3]
agreeing to be bound by the terms of this Agreement. If you do not agree with the terms of
Agreement, promptly return the unopened disk package and the accompanying items (including t
NP
(2)
related book and other written material) to the place you obtained them for a refund.
ILL
p.”) SOFTWARE LICENSE
1. The Waite Group, Inc. grants you the right to use one copy of the enclosed software pi
I
grams (the programs) on a single computer system (whether a single CPU, part of a licen:
network, or a terminal connected to a single CPU). Each concurrent user of the program m
have exclusive use of the related Waite Group, Inc. written materials.
The program, including the copyrights in each program, is owned by the respective autl
and the copyright in the entire work is owned by The Waite Group, Inc. and they are therefi
protected under the copyright laws of the United States and other nations, under internatio
treaties. You may make only one copy of the disk containing the programs exclusively
backup or archival purposes, or you may transfer the programs to one hard disk drive, usi
the original for backup or archival purposes. You may make no other copies of the prograr
and you may make no copies of all or any part of the related Waite Group, Inc. written ma
rials.
You may not rent or lease the programs, but you may transfer ownership of the programs
related written materials (including any and all updates and earlier versions) if you keep a
copies of either, and if you make sure the transferee agrees to the terms of this license.
You may not decompile, reverse engineer, disassemble, copy, create a derivative work,
otherwise use the programs except as stated in this Agreement.
GOVERNING LAW
The following warranties shall be effective for 90 days from the date of purchase: (i) The Waite
FIVMLIOS
Group, Inc. warrants the enclosed disk to be free of defects in materials and workmanship under nor-
mal use; and (ii) The Waite Group, Inc. warrants that the programs, unless modified by the purchas-
er, will substantially perform the functions described in the documentation provided by The
Waite
Group, Inc. when operated on the designated hardware and operating system. The Waite Group,
Inc. does not warrant that the programs will meet purchaser’s requirements or that operation of a
that
program will be uninterrupted or error-free. The program warranty does not cover any program
has been altered or changed in any way by anyone other than The Waite Group, Inc. The Waite
Group, Inc. is not responsible for problems caused by changes in the operating characteristics of com-
ISNIDI
puter hardware or computer operating systems that are made after the release of
the programs, nor
for problems in the interaction of the programs with each other or other software.
EXCLUSIVE REMEDY
The Waite Group, Inc. will replace any defective disk without charge if the defective disk is
returned to The Waite Group, Inc. within 90 days from date of purchase.
This is Purchaser's sole and exclusive remedy for any breach of warranty or claim for contract,
tort, or damages.
LIMITATION OF LIABILITY
THE WAITE GROUP, INC. AND THE AUTHORS OF THE PROGRAMS SHALL NOT
IN ANY CASE BE LIABLE FOR SPECIAL, INCIDENTAL, CONSEQUENTIAL, INDI-
RECT, OR OTHER SIMILAR DAMAGES ARISING FROM ANY BREACH OF THESE
WARRANTIES EVEN IF THE WAITE GROUP, INC. OR ITS AGENT HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
THE LIABILITY FOR DAMAGES OF THE WAITE GROUP, INC. AND THE
AUTHORS OF THE PROGRAMS UNDER THIS AGREEMENT SHALL IN NO EVENT
EXCEED THE PURCHASE PRICE PAID.
COMPLETE AGREEMENT
This Agreement constitutes the complete agreement between The Waite Group, Inc. and the
authors of the programs, and you, the purchaser.
Some states do not allow the exclusion or limitation of implied warranties or liability for inci-
dental or consequential damages, so the above exclusions or limitations may not apply to you. This
limited warranty gives you specific legal rights; you may have others, which vary from state to state.
BH
oy
I 1:
BUILD YOUR
a t
PTT #od
f
<
#
¥
a
CURR
»
WHAT
"Flights
hottest
READERS
instrumental
Fantasy...shines
software technology behind
games.” PC
CRITICS
GAMER
fans, this is
GAME
EANTASS
in writing
a
some of
Light on
today's
the book.
DEVELOPER
my
JERRY
the
MA
o©
9 COMPUTER il
~~»
GAMINGa T Y
Ed
Sp
he best just got better! Build Your Own Flight Sim in C++ from Build Your Own Flight Sim in C++ giv
ome info
e best-sellingFlights of Fantasy, launches programmers programming trade: Expanded discu
[IER
i
the 21st century. Take your programming skills to new
STE IE methods, like Sound Blaster
you ete
ground up.
your own [eae 1 quality flight
LLL il the and ecto Mountain
programming E
gener
M All-new advanced polygon shadir
ch
~
pg
PLATFORM: IBM-Compatible PC
or better
OPERATING SYSTEM: MS-DOS 3.1
LEVEL:Intermediate to Advanced
REQUIRES: Borland C++ 3.1 or 4.5
ce KL 17:
£1 $53.99 Canada pissy Hp
ISBN 1-57169-022-0
690227