• | Table of Contents
Focus On Photon Mapping
By John Marlon
Publisher | : Premier Press
Pub Date | : 2003
ISBN | : 1-1-59200-008-8
Pages | : 344
Buy Print Version

From the basics of the nature of light to ray tracing and photon rendering, you'll learn how to bring a new dimension of reality to the graphics in your games. By beginning with the standard foundations of ray tracing and radiosity, you'll quickly learn how photon mapping has been able to combine the positive aspects of each while eliminating their inherent drawbacks. As your skills advance through the book, you'll learn how to effectively apply natural lighting effects. Focus On Photon Mapping deals with a number of complex 3D graphics concepts and shows you how to develop photon mapping from the ground up. In order to attain maximum benefit from the concepts that are discussed, it is recommended that you have some experience in 2D and 3D graphics.

Features

  • The book examines the photon mapping concept and techniques, including building block structures of the application and implementing it.

  • The author demonstrates the use of photon mapping in different applications and its limitations.

  • Readers learn the various simulated lighting techniques, such as color operations, dot products, and vector normals, reflections, and refractions.


• | Table of Contents
Focus On Photon Mapping
By John Marlon
Publisher | : Premier Press
Pub Date | : 2003
ISBN | : 1-1-59200-008-8
Pages | : 344
Buy Print Version
Dedication
Acknowledgements
About the Author
Letter from the Series Editor
Introduction
Chapter 1. Understanding the Nature of Light
 | Understanding Waves
 | Understanding the Electromagnetic Spectrum
 | Considering Shadows
 | Summary
Chapter 2. What Is Color?
 | Colors and Computers
 | Creating the Color Class
 | Using Color Operators
 | Defining the Basic Colors
 | The PPM Format
 | Using the Chapter's Source Code
 | Summary
Chapter 3. Reflection and Refraction
 | What Is a Ray?
 | What Is Reflection?
 | Diffuse Reflection
 | Specular (Mirror) Reflection
 | What Is Refraction?
 | Summary
Chapter 4. Global Illumination
 | The Photon
 | Light Shading
 | Flux
 | Light Transport Notation
 | Direct and Indirect Illumination
 | Summary
Chapter 5. Other Rendering Techniques
 | The Basics
 | View or Image Plane
 | Frustum
 | Field of View
 | Ray Tracing
 | Distributed Ray Tracing
 | Radiosity
 | Hybrid/Multi-passRadiosity
 | Backward Ray Tracing Illustration
 | Summary
Chapter 6. Basic Structures
 | Using Vectors and Points
 | Finding the Plane in Which a Polygon/ Triangle Sits
 | Creating a Light Source
 | Using the Ray
 | Adding Reflection and Refraction Features
 | Using the Chapter's Source Code
 | Summary
Chapter 7. Developing Your Application
 | Defining Your Materials
 | Using Primitives for Simulation
 | Incorporating the Sphere Primitive
 | Creating a Triangle Primitive
 | Creating and Using the Object Class
 | Designing the Scene
 | Using the Chapter's Source Code
 | Summary
Chapter 8. The Rest of the Puzzle
 | Understanding the Coordinate Systems
 | Implementing the Camera into the Scene
 | Importing the Scene from a File
 | Declaring Attribute Primitives
 | Setting Up the Scene Classes
 | Determining the Scene Primitives
 | Reading the Data into the Scene
 | Upgrading the cScene Class
 | Using the Chapter's Source Code
 | Summary
Chapter 9. Ray Tracing
 | Using Backward Ray Tracing
 | Implementing the Shading and the Lighting
 | Upgrading cScene for Ray Tracing
 | Using the Chapter's Source Code
 | Program 1: Ray Tracer 1
 | Revisiting Reflection and Refraction
 | Program 2: Ray Tracer 2
 | Simulating Shadow Rays
 | Program 3: Ray Tracer 3
 | Summary
Chapter 10. Photon Mapping in a Nutshell
 | The Goal
 | Problems with Ray Tracing and Radiosity
 | The Concept Behind Photon Mapping
 | The Solution
 | Summary
Chapter 11. Photon Tracing
 | Understanding the Structure of a Photon
 | The Photon-Mapping Algorithm's Two-Pass Method
 | Designing the Photon Map Class
 | Incorporating Simple Scattering
 | Incorporating Proportional Light Scattering
 | Storing Your Photons
 | Absorbing Light
 | Transmitting and Reflecting Light
 | Playing Russian Roulette
 | Summary
Chapter 12. Photon Rendering
 | Implementing Photon Mapping
 | Using the PhotonMap Class
 | Using the Upgraded cScene Class
 | Using the Chapter's Source Code
 | Summary
Chapter 13. Advanced Searching
 | Using Spatial Subdivision
 | Using the KD Tree Algorithm
 | Building the KD Tree
 | Searching the KD Tree
 | Implementing the KD Tree
 | Using the Chapter's Code
 | Summary
Chapter 14. The Power of Photon Mapping
 | Simulating Diffuse Interactions
 | Simulating Diffuse Reflections
 | Simulating Specular Reflections
 | Simulating Caustics with Photon Mapping
 | Simulating Indirect Illumination
 | Creating Better Penumbras in Shadows
 | Using the Chapter's Source Code
 | Using Photon Mapping in Games
 | BSP Trees and Photon Mapping
 | Summary
 | Wrapping Up


Dedication

I dedicate this book to my Father in Heaven who is the source of all knowledge. Many people dedicate books to family members, friends, or close associates, but I choose to focus on my spiritual blessings and offer my acknowledgements to the Fatherthis book is dedicated to my Father Jesus Christ, who gives me the knowledge that enables me to do many great things.

I also dedicate this book to my Grandmother who passed away during its writing.


Acknowledgements

First, Andre' LaMothe made his book possible. I've known Andre for some years now since his first book, Game Programming Gurus, was published. Andre' is a big kid who is very humble even though he is one of the most intelligent people in the computer industry. He's seen this industry grow from a small group of individuals and watched computer-based games develop into a multibillion-dollar industry. He's never too busy to answer a question or two and, for me, that's a very big plus. I would like to personally thank him for coming to me and proposing a photon mapping book.

Second, I would like to thank all the people from Premier Press who stood by me through this book. I would like to thank Mitzi Koontz for being a sweetheart in working with my busy schedule. I would like to also thank Kezia Endsley for editing my book so that it would make sense. She helped bring meaning to my words and I'm very grateful.

Third, I would like to thank Harry Siegel for helping on the copy edits. I've worked with him for many years now and have great respect for his command of the English language.

Fourth, I would like to thank Brian Smith for his in-depth insight into pho-ton mapping. This book wouldn't be possible if it weren't for his help in getting portions of the code to work properly. I am very thankful for his contributions.

Fifth, I would like to thank Adrian Perez (Advanced 3d Game Programming with DirectX 2002) for letting me use his color3 structure.

The people in my life are very important to me. To my mom Victoria, dad Lucius, brother Marvin, and my two sisters Simone and Shawna, I would like to thank you for being in my life.

A very important person to me was Frank Willemsen, who was nice enough to teach me how to write my first 3D engine. He took time out of his busy schedule to teach me important parts of Wolfenstein and Quake. Even though he lives in the Netherlands, I feel he's one of my closest friends. He's getting married soon and I would like to say congrats!

Finally, the most significant event that's impacted my career was being given the opportunity to work in a corporate environment at the young age of 16, so I would like to thank Dominic Mignogno, Gala Miller, Brian Colins, and Larry Shirley for being my work family.


About the Author

Marlon John has seven years experience in Web programming as well as four years experience in hard-core programming. He is a graduate of Laguardia Community College, A.A.S in Science, 1998-2000. His primary objective is to learn as much as possible and familiarize himself with the latest and greatest technologies. His expertise has grown to learning numerous programming languages and software applications, including Microsoft .NET, Assembler, HTML, SQL, ASP, Perl, JavaScript, Cobol, C/C++, C#, and Cold Fusion. He also has experience in a few API languages such as Windows SDK, Direct X, Real Networks Helix SDK, Windows Media Corona SDK, Winsock Programming, and Microsoft Speech SDK. He likes to write 3D games for Direct X and the Microsoft X Box. He has a good networking background with Microsoft Networking for Advanced Server 2000 and Exchange Server 2000. On a personal note, Marlon loves to have fun and enjoy his life. He likes to spend time in quiet places with his girlfriend and travel to beautiful areas around the world. Marlon hopes to be an influential figure in the computer industry in the near future. He has an insatiable appetite for game programming, which is his most loved interest.


Letter from the Series Editor

Although, "photon mapping" sounds like something from Star Trek Next Generation, it's not! Photon mapping is the process of modeling the true interactions of light and energy within an environment to create truly photo-realistic lighting effects and visuals that make current shading technologies look like they are something from the 8-bit era. Nonetheless, photon mapping is very complex on a theoretical level, and deep understanding of it can only be achieved with advanced knowledge of physics, optics, mathematics, and 3D graphics. However, if you're like me, sometimes you just want the facts; the bottom line on how to do something. That's what Focus On Photon Mapping is all about. The author, Marlon John, is a programmer and graphics enthusiast who has studied photon mapping for years now. Originally we met many, many years ago; he asked me how to perform some matrix operations for a 3D graphics engine he was developing. Since then, Marlon has become fascinated with photon mapping, and has implemented numerous photon mappers.

This book is a quick-and-dirty treatise on the subject to get you started with the technology without needing a Ph.D in mathematics! However, the book does assume general 3D graphics knowledge, and an intermediate level of programming skill. A beginner will be able to get someone out of the book, but I suggest you brush up on your C++ before attempting this.

In conclusion, when I first started studying the subject myself I was frustrated that no one with a practical sense of programming had written anything about the subject. Learning photon mapping was like learning quantum mechanics[md]slow tedious, boring. However, Focus On Photon Mapping takes a more down-to-Earth approach, and leaves the heavy theory out. By the end of this book, you'll have a working photon mapper, not more questions about how to integrate some triple integral that is meaningless.

Sincerely,

Andre' LaMothe

Series Editor of Premier Press's Game Development Series


Introduction

Welcome to my book. I am happy that you can sit down and spend some time with me. This book introduces photon mapping to an intermediate graphics programmer. This book is not as easy as reading a "how to" or "dummies" book because it deals with some of the more complex concepts in 3D graphics. The Focus On prefix on the book cover is an indication that the book concentrates on a specific subject in detail. The book begins with an explanation of the nature of light and the many natural lighting effects. It then discusses applying lighting to computers by using rendering algorithms such a ray tracing and photon mapping. If you read this book you should get an idea of how to implement ray tracing as well as photon mapping. It's best not to jump to photon mapping without understanding the basic foundation of ray tracing; think of it as a building-block process.

Henrik Wann Jensen invented the photon-mapping algorithm. His motivation was to create a rendering algorithm that had all the positive aspects of ray tracing and radiosity without any of their inherent problems. Many other scientists and college graduates have shared this same goal. The key to Jensen's success was that his algorithm is much simpler than radiosity and includes many features of ray tracing. Jensen's book, Realistic Image Synthesis Using Photon Mapping (ISBN 1-56881-147-0), provides a detailed look at pho-ton mapping from a mathematical perspective. It explains other topics that aren't covered in this book, such as filtering, irradiance, caching, and participating media. This book is a more practical application because it shows you how to develop photon mapping from the ground up. It's also different in that I don't focus exclusively on photon mapping; I also explain the nature of light as well as ray tracing.

To get the most out of this book, you should have some experience using graphics. Learning about 3D graphics isn't an overnight task[md]it takes many years to master. There is so much information out there you can never stop learning. This book is best suited for someone who understands the basic fundamentals about 3D graphics[md]someone, for example, who has written a rendering program like a ray tracer or a video game using his/her own libraries or DirectX and OpenGL. You must know C and it's a good idea to know C++ because I use it a little bit. I purposely write the photon mapping code in C because I don't want to alienate C programmers who just want the code for their programs.


Chapter 1. Understanding the Nature of Light

Light is the first thing we see when we open our eyes. But what is light? In general, light is an electromagnetic disturbance that travels through the fabric of space/time at 186,300 miles per second! Sound weird? Well, it gets weirder! Light is energy in its purest form. Light has both the properties of a wave and a particle, making it doubly vexing. Now, from our point of view here on Earth, we have lots of light during the day and limited light at night. The light we receive at night is mostly reflections from the moon and the sky. We perceive the sun as one big point of light giving off energy in all directions. Consider Figure 1.1, for example.

Figure 1.1. Directional light is limited and directed in a certain direction,where-as point light emits equally in all directions.

All planets are shaped like spheres, and each planet receives some of the sun's energy on one side (or hemisphere) at a time. This is how we perceive day and night. This limited amount of light directed on the lit hemisphere is called directional light. The sun that is emitting light equally in all directions is called a point light. But the sun is not the only source of light on the planet. We have other light sources, such as fire, white-hot filaments in light bulbs, and glowing gas in tubes.

Let's take a closer look at light's properties. According to physics, all light (or electromagnetic waves in general) is generated by the motion of charged particles, such as electrons (electrons have a negative charge). Light is an electromagnetic wave and visible light only accounts for a small portion of the bigger picturethe electromagnetic spectrum.


Understanding Waves

Before going any further, it's important to understand light waves. Look at Figure 1.2 for an example.

Figure 1.2. Waves have an amplitude (or height) and a wavelength.

Almost all objects in nature proceed with a twisting or turning motion in time, called vibration. When sound is released from a speaker, the vibrations continue for some time before they fade outthis changing motion in time and space creates a wave. A wave cannot be still because it exists only in movement from one point to another.

Take a look at the sine wave in Figure 1.2. The distance from one ascending part of the wave to the next is the wavelength. Just think of each maximum point being repeated in the next wave, and measure the distance between them. The sine wave for light is repeated billions of times a meter (hence visible light's wavelength is measured in nanometers), whereas water waves are measured in centimeters or meters based on the water's area.

The number of times each vibration occurs per second is the frequency. Figure 1.2 shows a midline running straight through the sine wave from left to right. The measurement of distance from the midline to the peak of the wave (also known as the crest) is called the amplitude. The speed of a wave is equal to its wavelength times its frequency. Another important point to understand is that each vibration per second is called a hertz.

Consider the following equations:

Frequency = 1 / Period;
Wave_Speed = Frequency * Wave_Length;

A 60-hertz wave, therefore, vibrates at 60 times a second and has a period of 1/60 of a second.

It's hard to visualize electrical waves because we can't see them with the naked eye. If you run a bamboo stick back and forth through still water, you'll create waves on the water's surface. Electromagnetic waves can be visualized in the same manner as the ripples.

Electromagnetic waves are created by an electrical current as well as a magnetic field. An electrical current can come from a power source such as a battery. Electrical current can produce the magnetic field.

Let's say you connect a rod to a power source with a high current. What happens? The rod becomes electrically charged. Now if you go into an open room and shake the rod around, it will produce electromagnetic waves in space. This creates an electrical current, which is overlaid with a magnetic field. The magnetic field is produced by the force of the moving electrical charge. Because you're shaking the rod, it's a changing electrical current with a changing magnetic field. Figure 1.3 shows an electromagnetic wave.

Figure 1.3. An electromagnetic wave is made up of an electrical current,overlaid with a magnetic field.The magnetic field is produced by the force of the moving electrical charge.The two fields are perpendicular.

These two fields are perpendicular to each other, but travel in the same direction, thus the total wave is described as being transverse and travels through space in a linear motion at one constant velocity, the speed of light C.

Electromagnetic Wave Velocity

You may be saying to yourself, "This is great to know, but why do these waves travel at one constant speed?" Should you believe it just because I said so? Well, let's think about it for a moment with the help of Albert Einstein. Einstein said that no mass can accelerate to the speed of light; he never said something can't travel faster than light if it's already going faster! However, he did say that nothing with mass can be accelerated to light speedperiod. The equations that govern this rule say that the amount of energy you would need to accelerate an object to light speed is infinite. Thus, nothing with mass can travel at light speed, but objects without mass travel at exactly light speedthis is what light ispure energyso it doesn't break the rule.

Visualize a light wave moving in space, traveling to infinity with no loss of speed. Now, it is possible to change the frequency of light, which will in turn cause the color of the light to change.

I still haven't explained why light moves at a constant speed. Let's say you're able to slow a light wave down. This causes weaker electrical and magnetic fields. As the wave moves through space, the fields become weaker and weaker until the wave fades out. Is this possible? Well, not according to the laws of energy conservation and electromagnetic induction, which say energy can't just disappear. Remember, energy can never disappear; it can only change form.

Or let's say you speed up the light wave. This creates stronger electrical and magnetic fields, which causes the wave magnitude to increase exponentially in energy. This is also illegal according to the laws of energy conservation. Energy must flow at one constant speed to keep it in an equal balance.

James Maxwell calculated the speed of this wave at around 300,000 kilometers per second. Maxwell also found out that visible light was just radiation within a fixed segment of the electromagnetic spectrum. Visible light's range is measured from 4.3x1014 to 7x1014 vibrations per second. Our eyes are only equipped to see waves within this range, that's why it's called visible light!

If you look in Extras/Chapter1/ on the CD there is a Java example of the electromagnetic wave. You can actually see a demonstration of the electromagnetic wave in action by opening Propagation of Electromagnetic Wave.htm in your favorite Web browser. I found this page at http://www.phy.ntnu.edu.tw/.


Understanding the Electromagnetic Spectrum

The electromagnetic spectrum is a vast array of different types of waves. All electromagnetic waves travel at one constant speed but differ in frequency (and wavelength). The spectrum is divided into segments based on each wave's frequency. Unique frequencies relate to unique wavelengths. Low-frequency waves produce a long wavelength, and higher-frequency waves have a shorter wavelength.

A simple way to understanding the electromagnetic spectrum is to divide the spectrum into ten parts. Parts one to four refer to radio vibrations, four to eight refer to light vibrations, and eight to ten refer to gamma vibrations. Now in order to find a particular wave, you filter out the relevant waves from the list. Nature does this in a similar way, filtering out which waves are needed from the spectrum. Humankind has harnessed this vast spectrum as well, producing appliances such as TVs, cell phones, AM/FM radios, wireless networking, optical networking, and much more. All of these devices amplify and filter out each type of vibration from the electromagnetic spectrum. The spectrum is not divided into equal segments, and many regions overlap each other.

NOTE

NOTE

Did you know that electromagnetic waves are all around us moving in and out of our bodies in space? Only a very small portion of the electromagnetic spectrum is visible to the human eye.People might therefore think of space as empty,but this is definitely not the case.Space is in fact filled with waves traveling to and fro.

Let's take a look at the following major sections of the electromagnetic spectrum. The spectrum is classified by a number of waves in each section. It is important to understand the different areas of the spectrum because you gain a broader understanding of how light falls into the bigger picture. The nature of light in accordance to the electromagnetic spectrum is also known as the wave model of light.

The following list describes the parts of the electromagnetic spectrum, starting from low frequency, low wavelength waves:

  • Radio waves fall in the range of a few thousand hertz (vibrations per second). They are considered low frequency radio waves. If you move up to one million vibrations a second, you'll find the AM radio band. After that, you move up to 50 million hertz to VHF (very high frequency) television frequencies. (Remember those old days of TV, when you had to adjust those annoying rabbit ear antennas to improve your picture quality?) The FM radio band falls between 88 and 108 million vibrations per second. Finally, there is UHF (ultra high frequency), which occupies the high end of the radio spectrum. Today, some radios and televisions use these frequencies.

  • Microwaves operate around one billion vibrations per second. Today, we use these waves in microwave ovens, which decrease cooking times dramatically.

  • Infrared waves are also commonly referred to as heat waves and start at around one trillion vibrations per second. Ever wonder how your TV remote control works? Well, almost all remote controls today use infrared. Without your remote control, just think of the hard work you'd have to dogetting up and changing the channel! IR is also used to transfer data wirelessly using infrared ports on laptops. Folks can send data to an infrared-equipped printer from their PDA devices as well. It's a quick and handy way of working around a local area network.

  • Visible light is a tiny percentage of the electromagnetic spectrum, but it's all we can see around us with the naked eye. Visible light waves vibrate at around 430 trillion times per second. The lowest frequency of light our eyes can see is red followed by orange, yellow, green, blue, and finally violet (remember ROYGBIV?). Violet's frequency is close to double the frequency of red.

  • Ultraviolet rays, x-rays, and gamma rays, can be hazardous to humans because they have such high energy levels. They are generated in the cosmos naturally as the result of highly energetic reactions such as those found in stars (we can also generate them synthetically with the proper equipment). For example, ultraviolet rays can cause sunburn; hence the need for sunblock. The '70s TV show The Incredible Hulk was about a man who was exposed to gamma radiation, which caused him to turn into a horrific-looking beast. Of course, this isn't relevant to the book, but it's peculiar to see how imaginary stories can originate from some essence of scientific fact.

Light and sound are vibrations found in the electromagnetic spectrum that travel throughout space. Sound is the spreading of vibrations through a physical formanything that is solid, liquid, or gas. Light is generated only by electrical and magnetic fields. In a vacuum (without any gravitational warp-ing), light travels in a straight line from one point to another. It is important to remember that all waves begin from vibrations of some sort.

NOTE

NOTE

TV stations,radio stations,and cell phone stations are constantly emitting waves in local cities.Some people fear that these waves can cause long-term danger to us humans because we are constantly interacting with them.

Newton's first law states that anything in motion will stay in motion and will move in a straight line, unless something else causes it to stop or change direction. This is the nature of light, which will travel forever unless stopped or blocked. Today we can see the destruction of stars that occurred millions or billions of years ago. This is because the light takes many years to travel into our field of view. Countless trillions of light beams travel from the sun to the Earth every day.

Transparency

Transparency is one of the most amazing effects in nature. We have transparent objects all around us. We have clear glass in things like vases, ornaments, windows, and bottles. We also have transparent plastics like Zip-lock bags and Saran wrap. We have clear water in a pool or a glass of water. Transparency is everywhere, and most of us fail to realize its presence.

Transparency is basically when objects allow light to pass through them.

Electromagnetic waves travel in straight lines from one point to another. Now, if for any reason, the wave interacts with some type of matter, some of the electrons in the matter start to move and vibrate. Picture some small children playing, running back and forth. When they accidentally run into each other (which they always do), they cause each other to shake based on their force. Similarly, matter reacts to light based on the matter's properties and the light's frequency. When the light hits an object, it causes the electrons in the object to vibrate (see Figure 1.4).

Figure 1.4. Light waves have both wave-like and parti-cle-like behavior,as discovered by Einstein.

In the representation shown in Figure 1.4, electrons are connected to atoms by what look like springs. The springs stretch and move because of the source vibration from the light waves. The incoming energy is either converted to heat and is spread out to the neighboring atoms, or it's retransmit-ted as light.

The wave's frequency determines which vibrating electrons are trapped and which ones break out of jail. The lower the wave frequency, the less time the atoms in the material hold onto the vibrating electrons of the wave. This causes the wave to have less interaction with neighboring atoms, thus allowing the wave to move faster. And because the wave moves faster, some of the energy slips through the material to the other side.

The clear glass in a window is almost perfect in distributing this incoming energy, as illustrated in Figure 1.5. The frequency in which the light hits the window is identical to the way the light is retransmitted from the window. The main point to focus on here is the time it takes for the light to travel from one side of the glass to the other.

Figure 1.5. Light travels through clear glass more or less unencumbered.

When light travels inside of materials, its speed varies. Remember, light waves travel at around 300,000 kilometers (or 186,300 miles) per second, but that assumes it is traveling through a vacuum. When light travels through water, its speed is 75% of that. When light travels through a diamond, its speed is reduced to 41% of that. As a graphics programmer, you must study all of this information and try to simulate it using 3D algorithms. I am still trying to find an engineering materials book to fully simulate real-world models in nature.

So now you've learned that light's continuous progress through an object is based on the object's properties and the light's frequency. Let's consider some more light-related properties that objects can have.

Opaqueness

In Adobe Photoshop, you change the opacity of a layer to make it more or less transparent. A setting of 0 makes the layer totally transparent, and a setting of 100 makes it totally opaque.

Opaque means to block the passage of radiant energy, especially light. Almost everything around us is opaque.

Opacity and transparency work hand in hand. Consider objects that absorb light without any retransmission. Objects like walls, chairs, people, books, furniture, and oranges are all opaque. If an object is 100% opaque, it absorbs 100% of the light it receives.


Considering Shadows

A shadow is a simple concept to understand. When you stand in the sun next to a wall, you cast a shadow. If light beams can't penetrate an object, sometimes they're absorbed. Because the world is three-dimensional, light hits objects that are in front of other objects first. Now, because the light is blocked, this creates darker areas on the second object. Figure 1.6 shows some examples of shadows.

Figure 1.6. Shadows are,in essence,a void of light.

Based on the light source and your position relative to an object, you create two types of shadows. The closer you are to the object, the more you generate a sharp-edged solid shadow. If you move back a few steps, your shadow becomes blurrier around the edges. This is the nature of shadows, based on the light source's position and brightness. The next sections discuss these two types of shadows in more detail.

Understanding How an Umbra Is Formed

To explain the umbra, let's go back to the example of shadows on a wall. This time, consider a puppy standing next to the wall in the sunlight. Now, when the puppy is very close to the wall, it casts a solid, sharp-edged shadow. This is called the umbra. The reason this happens is that light travels in massive amounts. When there is little distance between the object and the wall, the light doesn't get a chance to hit the object from too many angles. This causes the light to hit the object directly on the surface with very little scattering, thus causing the shadow to look sharp. Figure 1.7 shows this concept.

Figure 1.7. An umbra is a particularly sharp shadow that results from a short distance between the object creating the shadow and the location of the shadow.

Understanding How a Penumbra Is Formed

Now, if the cute little puppy moves back a few steps, its shadow becomes blurry. This is the penumbra. It is created when the object blocking the light is far away from the path of light. The penumbra happens when the light from one source is partially blocked and light from another source fills in. Because light waves travel in massive amounts, the distance between the puppy and the wall is far enough to give the light beams other angles to intersect the object. This causes more scattering from different incoming directions, thus making the shadow blurry around the edges. Again, the umbra is the dark solid part of the shadow, and the penumbra is the blurry outer area of the shadow. See Figure 1.8.

Figure 1.8. A penumbra is essentially a blurry shadow.

NOTE

NOTE

A more interesting example of a shadow occurs when the moon and the Esarth cast a shadow when the sun's light is directly facing them.When the two objects overlay each other,a shadow appears where a shadow from the moon is cast on the earth.This is known as a solar eclipse.


Summary

In this chapter, you've learned about light and how it's formed. Visible light comprises only a small part of the electromagnetic spectrum (in fact, it comprises only an infinitely small part to be exact since there is no limit to the frequency of an electromagnetic wave). It can be visualized like waves of water. You now know that light travels in straight lines, and when it intersects a transparent object, it scatters, and is absorbed before it comes back out. It is very important to remember that when a light beam hits matter, numerous types of effects can occur. It is the objective of this book to explain each effect and simulate it. This is the reason why you're learning about the properties of light before you begin development.


Chapter 2. What Is Color?

Without color, our perception of the world would be much different. Colors are everywhere. We have brown chairs, blue skies, red cars, yellow bananas, black belts, white snowflakes, and many more things around us that have unique colors. Most people are accustomed to color because we have experienced it since we were born.

But what is color? As youngsters, we are taught that red is red and green is green. But many people are color-blind and can't even see red or green at all. So we define color as the visual perception of a specific variation that most of us can agree upon. Interestingly though, if you were to see out of another person's eyes, you would probably be in shock since everything would be tinted and look surreal due to slight frequency shifting in the response of your eyes to his/her eyes. This brings new meaning to "beauty is in the eye of the beholder."

In the previous chapter, you learned all about electromagnetic waves, which are classified within the electromagnetic spectrum. Each wave has a different frequency and wavelength. Visual lightmade up of red, orange, yellow, green, blue, and violetis a specific segment of the electromagnetic spectrum. An important point to note is that our eyes can interpret many variations of colors, but they're mostly sensitive to green. Visual light is illustrated in Figure 2.1note how it is a very small portion of the whole spectrum.

Figure 2.1. Visible light is made up of red,orange,yellow,green,blue,and violet.The spectrum here moves (from left to right) from low frequency,long wavelength to high fre-quency,short wavelength.

When an electromagnetic wave travels and interacts with an object, it causes some of the natural frequencies of the object to vibrate. Different objects have different natural frequencies for absorbing and emitting energy. In certain objects, where the amplitude of the vibrating electrons is high, some of the energy is absorbed. If the incoming frequencies are below a certain threshold, some of the light is reflected.

Normally, some of the energy would be absorbed into the object and some would be reflected. If an object absorbs all colors except green, the object appears green. If the object absorbs all colors except red, the object will appear red. If the object absorbs all colors and reflects nothing, it will appear black. So the color of an object is determined by the color that reflects.

The color of a transparent object is based on the natural frequency of the object in conjunction with the frequency of incoming electromagnetic wave. Clear glass transmits the color the exact same way the wave is entered in the object. This is because almost all the visual frequencies are passed through the object perfectly well. If the glass absorbs all the visual light but blue, it will not only appear blue but will transmit blue as well. This is a very nice effect; as you know if you've ever been to a club with color spotlights, you can see all types of colors projected on the walls and floors.

The visible frequencies (colors of the rainbow) of light added together produce white; however, strangely enough, red, green, and blue alone can also produce white. What this means is that the visible spectrum can be simplified into red, green, and blue regions.

When mixed together, these separate components can generate almost all the colors of the visual spectrum. This is a much simpler method of defining colors. When one component mixes with another, they create a new color. For example, if you mix red and green you produce yellow, or if you mix blue and green you produce light blue. This method of defining colors for red, green, and blue is called the additive primary colors method, as shown in Figure 2.2.

Figure 2.2. The additive primary colors method uses red,green,and blue as a starting point for all other colors.

    [ LiB ] Colors and Computers


Colors and Computers

Throughout history, people have always tried to use physical media to express what they see and experience. It started with ancient humans drawing on walls, pyramids, and caves. It continued with hand drawings, paintings, pictures, projectors, televisions, and the VGA card. We have come a long way toward simulating what we see, and the creation of new 3D hardware will take us to new heights. Moving beyond older media, three-dimensional games take us away from our physical world, sucking us in as if we're the actors in these virtual stories. The VGA card and the computer monitor work hand in hand, whereby one sends data and the other receives the data and draws the image. This is how the computer monitor operates.

The visual image that appears on the monitor is broken down into pixels. The pixels change color based on the directive from the VGA card. A computer monitor is just a two-dimensional picture with many colors that can change images very fast. That's all it is; just think of it as a very fast-changing picture. The first monitors were black and white; then in a few years colors were incorporated. Today, most monitors refresh images at around 60 hertz, with high resolutions and millions of colors. Figure 2.3 shows one pixel of a monitor represented either as 32 bit or 24 bit. If you change enough pixels to specific colors, you can generate a very detailed image. It can be a picture of your loved one, your pet, your Mustang, or a beautiful landscape.

Figure 2.3. 32-bit mode includes an alpha channel.

NOTE

NOTE

The flickering image on a monitor is evidence of a low number of hertz.The illusion of real-time animation begins at 30 or more hertz.

What you want to do is to control what appears on the monitor by placing your own data on the video card's memory. In order to do this, you need to change each pixel in video memory to your own specified value.

Most video cards today run in 32-bit graphics mode. This is 32 bits, or 4 bytes, per pixel (8 bits for each color channel; red, green, and blue, and 8 bits for alpha or transparency). Why? Well, 24 bits of color can select one of 16.7 million colors, and this is more than enough to cover most of the color space range of any human being. Additionally, graphics cards and video monitors create the display using combinations of red, green, and blue phosphors (picture elements that give off light when excited), so there is a 1:1 correspondence between the pixel value and the image displayed on the screen. Finally, red, green, and blue can be used to create any color because they are the additive primary colors (recall that any color can be created as a sum of red, green, and blue).

Enter the Data Buffer

To create an image on the screen, you change specific pixel colors in the 2D matrix that represents the screen display (video buffer). The alpha portion of the 32-bit pixel is used to make the pixel transparent to some degree. For example, if you had two overlapped rectangles, the first rectangle pixels could be transparent so that the second rectangle is seen. With 32 bits per pixel, the computer has enough information to change a pixel's color to one of 16.7 million variations as mentioned. Figure 2.4 shows how this data is stored in the computer.

Figure 2.4. The data buffer stores the color information of an object in the computer.

During the course of this book, you'll develop a 3D rendering environment in order to incorporate your 3D algorithms. What you'll initially need is an image buffer so that when you create your 3D images they are projected on the 2D monitor. This means you need an image buffer to store your projected 3D images.

Projected image data can be stored as a file or in video memory directly, which causes it to appear on the computer monitor. If you save or output to a file, you can use some type of program to read and copy the RGB data buffer into video memory for viewing. The data buffer is just a two-dimensional array of bytes grouped together in red, green, and blue (RGB) data chunks.

You write to the RGB data buffer using this common algorithm:

for(height=0; height< MY_OUTPUT_HEIGHT; height++)
    for(width=0; width< MY_OUTPUT_WIDTH; width++)
       pBuffer[MY_OUTPUT_WIDTH * height + width] = myColor;

Copying chunks of data on the fly using this simple algorithm can be a slow process, especially when the image resolution is high. One method for optimizing the speed of copying data to an image buffer is to reduce the amount of memory that is copied. The best method is to reduce the memory for each pixel by converting the 32-bit pixel to 24 bits, 16 bits, or 15 bits.

NOTE

NOTE

Copying data into video memory and projecting a 3D image using software memory buffers is known as software rasterization.

Removing the Alpha Component

Today, most images that are rendered on the monitor are opaque without any transparency. The alpha component makes an image transparent to some degree as previously mentioned. Windows XP uses the alpha component in pixels for windows, controls, effects, and so on. But you really don't need it in this book. Today, programmers omit the alpha component and just use 24 bits for their image output. They do this because red (8 bits) plus green (8 bits) plus blue (8 bits) plus alpha (8 bits) combined is really a waste of memory.

With 24 bits (32 - 8 = 24) per pixel, you can create realistic images because of the large number of available colors. A range from 0 to 255 is set for each color component. That's 256 shades for each component, making 256 (red) times 256 (green) times 256 (blue), which equals 16.7 million colors.

Anything that falls outside the range of 0-255 is normally ignored. Table 2.1 shows an example of color mixing.

Table 2.1. RGB Color Mixing

Output Color

Input Color Mixtures

White

Red (255) + Green(255) + Blue(255)

Black

Red (0) + Green(0) + Blue(0)

Gray

Red (128) + Green(128) + Blue(128)

Red

Red (255) + Green(0) + Blue(0)

Green

Red (0) + Green(255) + Blue(0)

Blue

Red (0) + Green(0) + Blue(255)

Yellow

Red (255) + Green(255) + Blue(0)

Aqua

Red (0) + Green(255) + Blue(255)

The alpha component for pixels is natively used in a few file formats such as Truevision Targa TGA and Photoshop PSD but is nonexistent in popular formats such as BMP, TIF, and JPG. It can be used extensively utilized in 3D graphics. It also has some uses in 2D graphics and digital video editing. Many times you can use it in DirectX to create masks to filter out specific colors on surfaces. However, you won't use the alpha component of pixels in any of the applications in this book.

Converting Colors

Sometimes programmers don't want their output images to be 32-bit, so they down-sample them to a lower number of bits16-bit for example is common. 16-bit mode eliminates the highest bits of each 8-bit component and scales the component down to maybe 5 or 6 bits. Many programmers use red (5 bits), green (6 bits), and blue (5 bits) to fill up each 16-bit pixel. The good thing about this is that sometimes your application's performance will improve. The bad thing is that it only has 65,536 possible colors to choose from. It doesn't matter, because our eyes can't really differentiate that closely between colors anyway. The only time we find differences is when colors are clustered up next to each other.

Here are a few macros you can add to your application to build 15-bit, 16-bit, and 32-bit pixels from individual red, green, and blue values:

// for r5.g5.b5 = 15 bit
#define _RGB16BIT555(r,g,b) ((b & 31) + ((g & 31) << 5) +
((r & 31) << 10))

// for r5.g6.b5 = 16 bit
#define _RGB16BIT565(r,g,b) ((b & 31) + ((g & 63) << 5) + ((r & 31) << 11))

// for a8.r8.g8.b8 = 32 bit
#define _RGB32BIT(a,r,g,b)  ((b) + ((g) << 8) + ((r) << 16) + ((a) << 24))

NOTE

TIP

Some programmers add the extra bit for green in 16-bit modes because our eyes are more sensitive to green.This is because the cone cells responsible for color vision in the green range overlap the higher and lower frequency ranges.They are called M-class cones.

Unit Component

In most of the mathematical operations in this book, you'll be using vector mathematics (a vector in this case is a 2D or 3D quantity that represents a point in 2D or 3D space). In your work, for some equations to work properly, you must normalize any vector and color components before you perform operations with them. To normalize a vector, you must compute its length and then divide each component of the vector by this length value. This has the effect of forcing the length of the vector itself to 1.0.

For example, you'll need to scale your color components down to a range of 0.0 to 1.0 to perform calculations. Once you're done with your math, you'll convert the components back to their original values in a range of 0-255. It's not really a problem to convert numbers to a very small amount; just think of it as a different way of storing them.

The following code will help you understand how to normalize colors. It uses the float data type to hold the color components.

To normalize the color:

float color, scale      // some data
color = gray ;          // make it gray (128)
scale = (color / 255);  // you get 0.50196078431372549019607843137255

To return to the original:

scale *= 255;          // 0.5 * 255 = 128;

NOTE

NOTE

Although I'm doing the comparison with a floating-point number with only one number for gray,in a 24-bit color space,gray is actually composed of red,green,and blue components,all equaling 128.I conduct the test with only one component to simplify the process,but this is technically incorrect.To actually conduct color testing on each R,G,and B component,you must also test for the gray comparison.

Using an Epsilon

Integers are very simple when it comes to testing for one-to-one comparison. This is because integers are all whole numbers.

For example, look at the following:

int White = 255;
If (255  == White)
    Get_My_Mojo(); // called!!

But if you were to do this with floating-point numbers it wouldn't be so simple. For example:

float Gray   = 0.500;
If (0.501 == Gray)
    Get_My_Mojo(); // not called!!

This is because floating-point numbers must be exactly the same (whole number part and decimal part) when testing for equality. The difference past the precision to a certain degree can be filtered out in order to have a successful result.

You need to worry about this because your red, green, and blue components will be defined in a range of 0.0 to 1.0. If you want to test whether 0.500 (gray) is equal to 0.501 (mostly gray too), you must filter out certain numbers past the precision.

The problem with floating-point numbers is their imprecision, so you must create averages. This requires an epsilon, which is equal to 0.001. You'll use the epsilon to determine whether two floating-point numbers are close enough to be equal, for all practical purposes.

By performing a simple subtraction, finding the absolute value of the difference, and testing with an arithmetic comparison, you can determine whether two floating-point numbers are equal:

(fabs(float2  float1) < EPSILON) is equal


Creating the Color Class

You'll use a data type of float to hold your red, green, and blue color components. You'll call your data structure color3, and you'll implement a few custom constructors, operators, and member methods. You'll also add a few internal color objects to help in your application development:

struct color3 // name
{
     union  {
           struct
           {
                float r, g, b; // r, g, and b color data
        };
          float c[3];
     };
     color3 (){} // do nothing
     // custom constructor
     color3( float inR, float inG, float inB   ) :
          r( inR ), g( inG ), b( inB )
     {
     }
     // assign data using a method
     void Assign( float inR, float inG, float inB )
     {
          r  = inR;
          g  = inG;
          b  = inB;
     }
     // take in r,g,b (0-255) full sized components
     // and convert     them
         void SetRGB( int R, int G, int B )
     {
             r = (float) R / 255.0f;
             g = (float) G / 255.0f;
             b = (float) B / 255.0f;
     }
   // combine two colors
     void Composite( color3 color1, color3 color2 )
   {
        r =: .r;
        g     = (color1.g > color2.g) ?  color1.g : color2.g;
        b     = (color1.b > color2.b) ?  color1.b : color2.b;
   }

    /* convert and scale the three floats and combine
       them into one double word chunk
       this might come in handy */

   unsigned long MakeDWord()
   {
         // scale up to 255 before shifting
         unsigned long iR = (int)(r  * 255.f )<< 16;
         unsigned long iG = (int)(g  * 255.f )<< 8;
         unsigned long iB = (int)(b  * 255.f );
         return   0xff000000 | iR | iG | iB;
   }

// if any of the values  are >1.0 or <  0.0.
// whatever falls outside the unit range is clamped

     void Sat()
     {
          if( r > 1 )
               r = 1.f;
          if( g > 1 )
               g = 1.f;
          if( b > 1 )
               b = 1.f;
          if( r < 0 )
                r = 0.f;
          if( g < 0 )
                g = 0.f;
          if( b < 0 )
                b = 0.f;
     }

   // color operators
   color3& operator += ( const color3& in );
   color3& operator -= ( const color3& in );
   color3& operator *= ( const float& in );
   color3& operator /= ( const float& in );

   // some basic  colors.
   static const color3 Black;
   static const color3 Gray;
   static const color3 White;
   static const color3 Red;
   static const color3 Green;
   static const color3 Blue;
   static const color3 Magenta;
   static const color3 Cyan;
   static const color3 Yellow;
};


Using Color Operators

C++ allows you to define custom operators using standard arithmetic operators (+, -, /, *). Let's add this extra functionality to the structure:

// Accumulative addition of two colors
inline color3& color3::operator += ( const  color3& in )
{
     r += in.r;
     g += in.g;
     b += in.b;
     return *this;
}

// Accumulative subtraction of two colors
inline color3& color3::operator -= ( const color3& in )
{
     r -=  in.r;
     g -=  in.g;
     b -=  in.b;
     return *this;
}
// Accumulative multiplication of a color by a  scalar
inline color3& color3::operator *= (   const  float&  in )
{
     r *= in;
     g *= in;
     b *= in;

     return *this;
}
// Accumulative division of a color by a scalar
inline color3& color3::operator /= ( const float& in )
{
      float inv   1.f / n;
      r *= inv;
      g *= inv;
      b *= inv;
      return   *this;
}
// Adds two colors together: ret = a + b
inline color3 operator+(color3 const &a, color3 const &b)
{
      return color3
     (
           a.r+b.r,
           a.g+b.g,
           a.b+b.b
     );
};

// Subtracts two colors: ret = a - b
inline color3 operator-(color3 const &a, color3 const &b)
{
      return color3
      (
           a.r-b.r,
           a.g-b.g,
           a.b-b.b
      );
};

// Scales a color by a float: ret = a * b
inline color3 operator*(color3 const &a, float const &b)
{
      return color3
      (
           a.r*b,
           a.g*b,
           a.b*b
      );
};

// Scales a color by a float: ret = a * b
inline color3 operator*(float const &a, color3 const &b)
{
    return color3
(

          a*b.g,
          a*b.b
     );
};

// Divides a color by a float: ret = a / b
inline color3 operator/(color3  const &a, float const &b)
{
     float inv = 1.f / b;
     return color3
     (
          a.r*inv,
          a.g*inv,
          a.b*inv
     );
};

// Color Equality, epsilon used due to numerical imprecision
inline bool operator==(color3 const &a, color3 const &b)
{
if(FastFabs(a.r-b.r)<EPSILON)
           if(FastFabs(a.g-b.g)<EPSILON)
                if(FastFabs(a.b-b.b)<EPSILON)
                     return true;
      return false;
};


Defining the Basic Colors

Here, you define the values of the basic colors. The basic colors are defined as unit values for each red, green, and blue component.

const color3 color3::Black( 0.f,0.f,0.f );
const color3 color3::Gray( 0.5f,0.5f,0.5f );
const color3 color3::White( 1.f,1.f,1.f );
const color3 color3::Red( 1.f,0.f,0.f );
const color3 color3::Green( 0.f,1.f,0.f );
const color3 color3::Blue( 0.f,0.f,1.f );
const color3 color3::Magenta( 1.f,0.f,1.f );
const color3 color3::Cyan( 0.f,1.f,1.f );
const color3 color3::Yellow( 1.f,1.f,0.f );


The PPM Format

You now have a nice color data structure with methods to work with, and it would be wise to test it. The problem is that you haven't written any software that allows you to draw to the video card directly, so you need a simpler way of outputting colored pixels to the monitor. A common way is to write the colored pixels to a file where it can be read in by a program into video memory. This means all you need to do is select a file format that is commonly used.

I've found that the PPM (Portable Pixel Map) format is so simple that I can't pass up the opportunity to use it. It may be old, but it has what you need. Please install the companion program called IrfanView (http://www.irfan-view.com), located in path Software/IrfanView on the CD. This application makes it easy to open *.PPM formatted files. It also lets you convert the PPM files into many other file formats.

Let's look at the PPM file format. A PPM file normally has two parts, the header information and the raw image data. The header information normally consists of at least three parts, each part separated by a carriage return or a linefeed (because the PPM specification requires only white space for character separation). It can also include the hash character (#) for user comments, and it can be placed anywhere in the header. The first line is the PPM identifier (P3 or P6), similar to how the BMP format has a hex code at the beginning. The second line consists of the width and height of the image, or its resolution. The data is stored as ASCII, and it can be any image size you choose. The third line consists of the maximum range in color values you can choose for each pixel. For example, you're using single bytes for each color component, so your range will be from 0-255.

NOTE

NOTE

The PPM file format is a bit old and many people choose not to use it because it has no compression and thus results in large files.However,it is extremely easy to use because the data is packed in a non-optimized layout.

Here's an example file:

Header Information
P3     # identifier
5 4    # width and height
# Marlon is cool!
255    # range for each color pixel
Raw Data Stream
0 25 0   0 3 0   0 0 0   1 0 0   0 5 0   0 0 128
0  0 0   0 0 6   2 0 0   0 0 0   0 0 0   0 4 0
32 0 0   0 0 0   0 0 0   0 0 0   0 0 6   3 0 255

Consider the following function, which writes a data buffer to a PPM formatted file:

bool WritePPM(const char * FileName, color3 *Image, int iWidth, int iHeight)
{
     // define some locals
     FILE * FileHandle;
     int i, j;
     // return FALSE if you can't get the file handle
     if ((FileHandle = fopen(FileName,"w")) == NULL)
          return false;
     // write the PPM Header
     fprintf(FileHandle,"P3\n%d %d\n255\n",iWidth,iHeight);
    // scene description
    int index;
    for (i = 0; i < iHeight; i++)
    {
          for (j = 0; j < iWidth; j++)
          {
// computer offset
index = iWidth * i + j;

// write colored pixel + scale up
  fprintf(FileHandle,"%1.0f %1.0f %1.0f\t",Image[index].r * 255.0F ,
                            Image[index].g * 255.0F,
                            Image[index].b * 255.0F);
          }
          // end this line
          fprintf(FileHandle,"\n");
    }
    // close file handle
    fclose(FileHandle);
   // return good :)
   return true;
}


Using the Chapter's Source Code

In the file path /Book Code/Chapter 02/on the CD are two sample programs. I wrote them to help you simulate color mixing. Each program copies color information into a data buffer and save the information to a file. In /Book Code/Chapter 02/Commonare the classes we've just created. The two programs will share this common folder as it utilizes these classes. Here is the list of the files:

Color3.cpp
Color3.h
MathTools.h
PPM.cpp
PPM.h

Color 3

The Color3 program is located in /Book Code/Chapter 02/Color3. Color3 just prints each basic color from your class to a file. The entry point for the program is inside main(). This is how you dictate to the application the logic used to output the colors to a PPM formatted file. The main logic of the program is in a file called /Book Code/Chapter 02/Color3/main.cpp. The program includes the Color3 class, the PPM file importer method, and a few math functions. The program copies each basic color of the Color3 class to a memory buffer that's outputted to a PPM file. The color buffer is created. The colors blue, red, green, cyan, yellow, and white are all copied to the memory buffer. The application then cycles through the pixels in the memory buffer and assigns a basic color from the Color3 class. The color buffer is then out-putted to a PPM formatted file. After the PPM files are exported, the memory buffer is released. Take a look at the general logic of the program defined in main.cpp (note that cyan, yellow, and white aren't shown for brevity's sake, but they use the same general logic):

#include <stdio.h>

// INCLUDE CLASS FILES
#include "..\Common\MathTools.h"
#include "..\Common\Color3.h"
#include "..\Common\PPM.h"
int main(int argc, char* argv[])
{
   printf("Color Program!\n");
   // create a 320x240 blue image
   int height, width;
   color3 * pBuffer = NULL;
   pBuffer = new color3[320 * 240];

   // LET'S DO BLUE
   printf("Doing Blue!\n");
   for(height=0; height< 240; height++)
      for(width=0; width<320; width++)
         pBuffer[320 * height + width] = color3::Blue;

   // write ppm
   WritePPM("blue.ppm", pBuffer, 320, 240 );

   // LETS DO RED
   printf("Doing Red!\n");
   for(height=0; height< 240; height++)
      for(width=0; width<320; width++)
         pBuffer[320 * height + width] = color3::Red;

   // write ppm
   WritePPM("red.ppm", pBuffer, 320, 240 );

   // LET'S DO GREEN
   printf("Doing Green!\n");
   for(height=0; height< 240; height++)
       for(width=0; width<320; width++)
          pBuffer[320 * height + width] = color3::Green;
   // write ppm
   WritePPM("green.ppm", pBuffer, 320, 240 );
}

To compile this program, follow these steps:

  1. Open the project workspace (Color3.dsw or Color3.dsp).

  2. From the File menu, choose Build, Rebuild All.

  3. From the File menu, choose Build, !Execute Color3.exe.

You can also run the program directly by executing the following file: /Book Code/Chapter 02/Color3/Color3.exe

The program will begin exporting each basic color from the Color3 class to the current directory. However, I've already created the exported files for you and placed them in the following folder:

/Book Code/Chapter 02/Color3/Output/

The files in this folder are as follows:

12/21/2002  07:27p        614,898 blue.ppm
12/21/2002  07:27p        768,498 cyan.ppm
12/21/2002  07:27p        614,898 green.ppm
12/21/2002  07:27p        614,898 red.ppm
12/21/2002  07:27p        922,098 white.ppm
12/21/2002  07:27p        768,498 yellow.ppm

Color3Mix

The Color3Mix program is located in /Book Code/Chapter 02/Color3Mix. Color3Mix mixes two colors to create a gradient and saves the effects to a file. This program follows the same format as the previous one, but varies in the logic located in Main.cpp. To compile the program, open the project workspace called Color3Mix.dsw in Microsoft Visual C++ 6.0. Then press Build | Execute Color3Mix.exe. Then follow the same instructions as previously. Again, you can go to the /Output folder and open up each generated file.

Here are the names of the generated files:

green_gradient.ppm
red_gradient.ppm

Be sure to review both sample applications before you go to the next chapter.


Summary

In this chapter, you learned about colors and how they're used in computers. You learned that visible light is just a small segment of the electromagnetic spectrum. You learned about the epsilon and why you remove the alpha component and use 24-bit color modes. You developed a robust class that will be used in all your future applications. You can output color information to a medium to create an image, analogous to an artist who uses oil painting on a canvas. The color class with its member methods is like your virtual crayon box. You now have some basic knowledge to build the core layers for photon mapping.


Chapter 3. Reflection and Refraction

    What Is a Ray?

    What Is Reflection?

    Diffuse Reflection

    Specular (Mirror) Reflection

    What Is Refraction?

    Summary


What Is a Ray?

Light is such a beautiful wonder; the way it moves and travels is such an amazing notion. Light can be emitted from many sources such as a lamp or a light bulb. As the light source gives off energy, each light wave moves away from the light source, traveling through space until it interacts with matter. Consider the example in Figure 3.1.

Figure 3.1. Electromagnetic waves being emitted from a light source.

Now if you stop and think for a moment about each light wave moving from one point to another in a straight-line path, you can deduce that light is similar to something in mathematics called a ray. A ray in mathematics has an origin from where it begins, a direction to where it's heading, and a destination to where it ends. This is perfect for what you need. So you can deduce that a very thin beam of light is classified as a ray. Now let's look at the revised model in Figure 3.2 using a series of rays being emitted from the light source instead of electromagnetic waves.

Figure 3.2. Collection of rays being emitted from a light source.

The reason why people use the ray is because there is no simple way to actually simulate electromagnetic waves in computers (of course a quantum computer could do this, but we need to wait like 20 years!). Remember computers are just machines, so we need to develop methods of replicating what nature actually does and transform them to a language the computer knows. The ray simulates the path light takes from one point to another. When a ray interacts with an object, the ray is bounced in some arbitrary direction based on the ray's direction and the object's position. Let's look at some examples of ray interactions in Figure 3.3.

Figure 3.3. Examples of ray interaction.

    [ LiB ] Chapter 3. Reflection and Refraction What Is Reflection?


What Is Reflection?

When the electromagnetic wave of light interacts with a material, it is either emitted from the material or absorbed into the material as heat. Because light travels in straight lines it normally takes the most guaranteed path and uses the shortest distance to travel. When light hits a white sheet of paper, the paper appears white because of the material's properties. The electrons in the white paper emit light the same way it is entered with very little energy being turned into heat. The energy that's emitted from the material and returned into space is called reflective energy. It's important to think of light in the ray form of movement from now on. In order to understand reflection you must understand the fundamentals of incoming and reflected energy. The angles for the incoming and reflected light are broken down into two terms.

The angle in which the ray is incoming is normally the angle in which the ray leaves the material. The angle is typically a factor of the material's impurities. Let's look at an example in Figure 3.4.

Figure 3.4. Angle of incidence.

When the ray hits the material, its direction from the light source to the surface will change. The incoming line (light source to surface) is therefore often called the incident ray and the outgoing line (surface to wherever) is often called the reflected ray. See Figure 3.5.

Figure 3.5. An example of the incident and reflected ray relationship.

    [ LiB ] What Is a Ray?


Diffuse Reflection

When a light beam interacts with a material it may be reflected into many directions, depending on the properties of the material. This type of interaction is called diffuse reflection. This normally happens when light hits a rough surface/material. Look at the example of diffuse reflection in Figure 3.6.

Figure 3.6. Diffuse reflection.

Now most books don't show what really happens when the light hits the material. So let's zoom in on the surface and take a closer look at the new random directions generated off the diffuse surfacesee Figure 3.7. The basic law of reflection still is applied to each ray on the diffuse surface. It doesn't seem this way because you can't really see the tiny simple reflections that occur. The different reflected angles on the rough surface cause reflections in many directions.

Figure 3.7. Microscopic view of the reflected random directions based on a material's impurities.

This is called microfacet surface roughness where a rough surface has very small mirror-like faces, where each tiny mirror has its own orientation for reflecting a beam of light to simulate roughness on the surface.

NOTE

NOTE

When light hits some materials,most of the time the light waves enter the material and scatter around a bit before they emit from the material from some arbitrary direction.You will not be using this idea when you simulate diffuse reflection.You'll simulate light being reflected from the point of interaction on the material.The reason why you don't use this method is because it's too complicated and takes up too much time when rendering.


Specular (Mirror) Reflection

When light interacts with a smooth material, the incoming energy is often reflected only in the mirror direction, which means the incoming light reflects to the surface at an equal angle. When the light hits the smooth surface it doesn't get a chance to scatter around the material so it's reflected almost equally to the angle of incidence. A mirror typically is referred to as a perfect specular reflection because it reflects light equally to the way it is incoming. A mirror has very few imperfections. Water and smooth metals are other materials that have specular reflections. The reason why you look skewed or bent out of shape when viewed through many metals is because of the surface impurities. Try to visualize the specular reflection as a small cone in the mirror direction on the material. The degree of imperfection can make the surface look glossy as well.


What Is Refraction?

Refraction is the bending of light through a transparent material. The loss of light speed and the path light takes are both factors of refraction. To better understand refraction, you must consider some of the lessons learned in Chapter 1. You learned that light travels at 300,000 kilometers per second. If light interacts with a transparent surfacefor example water, a prism, or glassit loses speed. This interaction causes the light to bend a little before it leaves the transparent material. This bending of the light is refraction. Refraction mostly occurs when light changes from one medium to another; for example, from air to water. To better understand the bending effect in water, look at Figure 3.8.

Figure 3.8. An example of refraction of a straw,which is submerged in a glass of water.

Figure 3.8 shows how refraction works. Because the light bends when it changes mediums, the straw in Figure 3.8 appears closer than it really is in the water. This is the nature of refraction, for it bends to travel the least amount of time. The angle of incident is much larger than the angle of refraction. You must take this angle into account when you write your computer refraction function. This is sometimes called the index of refraction. Look at the example of the angle (or index) of refraction in Figure 3.9.

Figure 3.9. Index of refraction.

    [ LiB ] Specular (Mirror) Reflection


Summary

This chapter covered two important ideas of light interaction, reflection and refraction. It is essential that you understand each term in detail if you are to write code to simulate each phenomenon. Reflection is the energy that's emitted off a material; it can be defined either as diffuse or specular. Refraction is the bending of light through a transparent material. The loss of light speed and the path light takes are both factors of refraction. You will write functions for both reflection and refraction later in the book.


Chapter 4. Global Illumination

Simulating light in a 3D environment has been the game developer's goal for many years. The different effects that occur in nature must be transformed to algorithms that the computer understands. Transforming the reality of light to a mathematical model is the ultimate goal of researchers.

Global illumination is understanding the nature of light and simulating each effect in a mathematical model. Now of course real-world lighting is so complex that no computer on earth can simulate it fully. They are some aspects of light that are still not yet fully understood. This book covers the physics aspect and the practical aspects of a light scattering in a given model. It will be mostly based on the interaction of light on surfaces seen through the eyes of an observer. But it goes way farther than that.

In a 3-D environment you have all types of objects in the scene. The best way to explain this is to think of a human being in the real world. The human has eyes that record the reflected light on objects that sends the visual information to the brain for processing. The same method of using our eyes to see and observe objects in the world is utilized in the synthetic model. The major advantage of the global illumination algorithm is that it foretells the intensity of light everywhere in the model. This is the light that contributes to the model indirectly in meager amounts or the global lighting in the model that comes from everywhere, thus scattering all around an environment.

NOTE

TIP

The goal of a global illumination algorithm is to simulate a real-world model so realistically that game players can't tell the difference between a synthetic render and a real photograph.

A virtual camera is created also known as an observer that navigates through the scene and records the visual data as a 2D image. Because light is observed from the interaction on surfaces, you must develop robust and realistic methods of copying the physical aspects of light to create synthetic images, which are images that are so perfectly rendered that you can't tell the difference between a synthetic render and a real photograph. Before we get our hands dirty with photon mapping, I want you to know some basic 3D fundamentals for understanding and developing an application. It is essential to know these terms because they are used later on in this book.

For many decades, programmers and mathematicians have been trying to develop rendering techniques that can correctly simulate light scattering in a scene. But almost all of the systems fell short of the goal. Some systems were either too slow or allocated too much memory. Today, Hollywood utilizes high-performance computer-rendering systems that can render full-length movies like Toy Story, Shrek, Ice Age, and Monsters Inc. These movies were a big success at the box office, which implies that Hollywood will be making many more of these types of movies in the years to come. Global illumination is set up by the geometry of the scene, the light sources, the list of materials, and finally the observer/camera. The geometry can be a box, wall, sphere, chair, dog, or car. The light sources can consist of point lights, or directional lights. The materials define the lighting parameters for each object in the scene in order to simulate specific lighting effects. The observer then records the reflected light as an image based on the material properties, scene geometry, and light sources. Look at Figure 4.1 to see the typical components of a scene. The objective for a global illumination algorithm is to foretell the intensity of light at any given point in the scene. You must compute the amount of light leaving each light source and determine the quantity for each object in the scene at any given time.

Figure 4.1. The components of the scene consist of the geometry,materi-als,lights,and the observer.

Simulation in a complex environment such as a room full of objects with unique material properties can be a daunting and painful task if not done correctly. It becomes worse when the overall object count in the model is huge. You must be able to write an application that can render direct and indirect lighting correctly. It should include effects such as specular highlights, translucent surfaces, color-bleeding, and soft shadows. Obviously, some of these terms seem strange to you but don't worry about them because I will cover each one in detail.

Many algorithms have been designed and almost all of them have their flaws when rendering the correct flow of light through a scene. Some algorithms just do simple effects but stray far from the correct mathematics of light scattering. For example, the lighting system that is coded into the current versions of DirectX is far from being mathematically correct. That's why it doesn't look realistic. The lighting algorithms in DirectX are approximations of real lighting to keep the rendering pipeline smooth and fast. Quake Arena and Unreal 2003 did a very good job at using the most DirectX had to offer. This made the game very fast and fun to play.

NOTE

TIP

Tomb Raider 2 was one of the first games that utilized 3D hardware alpha blending techniques.This effect was primarily used on water,where it was transparent to some degree. This added great realism to a game.

Almost all applications output their rendered geometry to a file or to the video card directly. The scene is always rendered using the eyes of the observer looking at the model from a specific direction and standing at some arbitrary position. Normally you would define your geometry from a file and render it using your virtual observer's eyes. State-of-the-art 3D renderers like 3D Studio Max, Lightwave 3D, Maya, and Softimage all follow the same concept of setting up an environment through the materials, objects, lights, and the observer.

The observer is the major element of a scene. The observer records the current position and orientation of the camera as a 2D image. This 2D image is then output to a file for later reference or output to the monitor directly. The object in front of the camera is rendered and recorded in the image. See Figure 4.2.

Figure 4.2. The virtual eyes of the observer generate the final image that is projected on the computer monitor.

You can also render a sequence of still images as an animation. If you choose to render the scene as an animation, you can move the camera around a bit, but you must process all the camera movements before render time. That is the problem with 3D renderersyou must process everything before render time. On the other hand, games let you move around and render in real-time. That's the secret to their magic. The goal of any game programmer is to write a game that has high-resolution graphics (millions of colors and big in width and height) and is fun to play.


The Photon

Rays, as discussed in Chapter 3, are great in simulating the movement of light from one point to another, but they are not perfect. Remember you are trying to simulate electromagnetic waves in space as much as possible, and rays just can't do the job by themselves based on their simple definition in mathematics. Rays can describe most effects such as reflection and refraction (mostly the major makeup of an image) using ray tracing. Again a ray can be seen as a very thin beam of light. Rays are seen as single entities moving around the scene independently of one another. Rays are primarily used in ray tracing. If you recall from Chapter 1, energy flows in massive amounts and as a group. Rays simply lack the capability to correctly simulate the movement of energy as a whole. Therefore, you need another primitive that closely describes the electromagnetic wave based on its properties of wavelength and frequencyhence the photon!

The photon is the basic quantity of light according to physics. A photon carries energy on a given wavelength. When an electron changes state between high and low energy levels, an excited atom emits a particle called a photon. A photon can be seen as the quantum of electromagnetic energya particle of light released from an atom. The frequency of a photon is a related to its energy, where each photon has its own energy level. For example, a photon of blue light falls within the blue frequency portion of the spectrum by carrying that amount of energy. The different energy levels contribute to different colors of light as countless photons interact with matter.

Obviously, we don't have an electromagnetic spectrum in the computer, so the frequency parameter isn't needed. The wavelength parameter can be used to break up the components of white light into red, green, and blue compo-nentsfor example, consider light bending in a prism. The photon's energy level is of principal interest when developing the foundation for photon mapping. We will use its energy level to track the movement of energy as a whole as it interacts with matter. When you add the photon concept, you can then track the movement of energy from one point to another in time on a given wavelength. You can use rays to track the movements of light, and photons to track the intensities and movements of light given a time and place.

Many people classify the movement of rays as radiometry and the movement of photons as photometry. A light beam can be broken into a collection of small tiny photons moving together as a group in straight-line paths. You measure the amount of energy that is released from a light source as the wattage/ power. See Figure 4.3 for an example of photons being emitted from a light source. Try to visualize the photons as small energy particles that leave the light source and scatter in the scene. The amount of energy that's released from a light source is carried in each photon. The power of the light source is measured by the number of released photons.

Figure 4.3. The photons emitted from a light source moving towards a surface.

    [ LiB ] Chapter 4. Global Illumination


Light Shading

The observer looks at the scene and renders the scene based on its current orientation and position. If the observer is looking at a dog, the dog is rendered to the 2D image. To render the scene, each point in every object that's visible to the observer must be shaded accordingly. When rendering realistic scenes, you must shade each point for every visible object correctly to create a desirable image. You have to look at each point on an object and compute its color correctly based on a series of parametersthis is known as light shading. To shade a surface in extreme detail, you must visualize each point as an infinitely small area. This is known as the per-unit projected area. See Figure 4.4.

Figure 4.4. The per-unit projected area is an infinitely small area for every point on a surface.

NOTE

TIP

Light shading is the most important part of simulating light in a model because it's responsible for the final rendered image.

You must compute the amount of energy that is incoming and outgoing on the per-unit projected area based on the timely rate and flow of energy. This is referred to as the flux. Normally, when energy interacts with each infinitely small point on the surface, you use something called the solid angle to determine how much of it reflects off the surface. Let's talk about the solid angle for a moment.

The Solid Angle

A solid angle is a three dimensional version of a normal angle. A normal angle measures the angle between two line segments. The solid angle is constructed by placing a unit sphere on a projected unit area. The solid angle is used in understanding the flow of photons to the intensity of the light source. It is the angle measurement of a ray and the reflected direction. It can be viewed as a very tiny area on a unit sphere that determines the direction of the ray. See Figure 4.5 for an illustration of the solid angle. The solid angle is dimensionless and is expressed in something called steradians. It could also be defined by a surface normal and two orthogonal vectors. You use the per-unit projected area in conjunction with the solid angle to determine which direction the ray will be reflected off the plane of a surface.

Figure 4.5. The solid angle measurement of a ray as well as the reflected direction.

The following sections define the terms used in this book to help you better understand light scattering and interaction.


Flux

Flux can be viewed as the radiant energy flowing through a surface per unit time or the time rate flow of pure energy in a vacuum. Flux is the movement of photons flowing across a given surface based on time. It is very important to understand the energy flowing around the scene because it's the fundamental basis for photon mapping. The energy that is arriving and leaving each surface in the scene must be labeled. We need to break down the incoming energy and outgoing energy into two terms. These two terms are the radiance and irradiance, described in the following sections.

NOTE

TIP

The radiance closely represents the final color of objects seen by the observer.

Radiance

This term is viewed as the flow of energy per unit of projected area. It can be viewed as the amount of electromagnetic radiation leaving a specific point on a surface. Radiance (or radiosity) is the total illuminative power of light leaving a specific point on a surface per unit area. The observer records the radiance. It represents the intensity of light at every point in the scene and mainly contributes to the coloring of objects in the scene. This term is very important to understand because it is extensively used in global illumination.

It also can represent the intensity of light for any point in space in a given direction. Consider radiance the amount of energy flowing from a surface. Radiance is a constant when flowing through space or in a vacuum. You learned (or reviewed) earlier that electromagnetic waves cannot increase or decrease in energy because of the energy conservation and electromagnetic induction laws. The speed can change only if the light goes through some special situations; for example, when passing through a prism or a glass window. Don't worry; you'll learn more about relevant points of this concept later. Radiance is a complex term that goes way beyond the scope of this book.

Irradiance

Irradiance is the total illuminative power of light striking a point on a surface per unit area. It can be seen as the flow of energy arriving at the surface from some specific direction. It is basically the amount of energy flowing to a surface. This is also important to know because it will be used in photon mapping.


Light Transport Notation

In your photon mapping application, you will be developing two types of light interaction functionsreflection and refraction. But as you learned from Chapter 3, reflection can be broken into two sub-categoriesdiffuse reflection and specular reflection.

Most of the time, your reflections will be diffusive. You need to differentiate between which surfaces are diffusive and which ones are specular. Remember, specular surfaces are bright shiny reflections that reflect light in one particular direction. A good example of a specular reflection is a mirror.

In your model, you need to develop some notation that tracks the path light takes for reflection given the light source and different surface types. Let's first define some abbreviations.

L = light

E = observer eyes

S = specular reflection

D = diffuse reflection

Take a look at Figure 4.6 and try to determine each course the light takes. The ray that is released from the light source is recorded either as L, E, S, or D every time it interacts with an object in the scene. However, it may be possible to track the path light takes in the opposite manner (the path can be recorded from the observer as the origin).

Figure 4.6. Light transport notation helps describe the path light takes for each ray.

In Example 1, you can say the path light travels is LSDD. In Example 2, you can say the path light takes is LSSD. In Example 3, you can say the path light takes is LDDS. It is very important to understand the flow of light between different surface types if you are going to correctly simulate it. Light transport notation may be used later on in this book to illustrate the path a pho-ton may take from one point to another.

NOTE

TIP

Light transport notation decreases the complexities when explaining the path light takes as it intersects different types of surfaces in the scene.


Direct and Indirect Illumination

Direct lighting is considered to be the first time a light beam interacts with a surface from a given light source. Indirect lighting is the reflection of light from another surface other than the light source.

A simple example that comes to mind is what might happen if you pushed someone. If the person falls on another person who is standing nearby, the person nearby will probably fall down as well. The reason why the third person fell down is because of you pushing the second person. The person you pushed indirectly falls on the third person. This simple analogy can be applied to lighting.

If light interacts with a surface and some of the light is reflected on another surface nearby, you can say the surface that received the reflected light is being indirectly lit. A shadow is a perfect example of indirect lighting. Sometimes indirect light from a surface is actually reflected onto itself by another close-by surface. It can therefore get a bit tricky and it can also become a nightmare for a recursive function. The direct lighting hits the first surface as it comes directly from the light source. The indirect lighting on the second surface comes from (or is reflected by) the first surface.

Direct lighting is a must in all applications. The first rendering applications were capable of only simulating direct lighting. For example an algorithm named ray tracing was the first to incorporate direct lighting. Direct lighting is also known as local lighting. Indirect illumination is a very important effect that we must simulate using pho-ton mapping. The reason for this is indirect illumination is poor in other rendering algorithms. Our goal is to fix this problem using photon mapping.

NOTE

TIP

Indirect illumination is a very important element of a global illumination algorithm. It helps to contribute to the direct lighting that intersects surfaces from the light sources. The combination of direct and indirect illumination helps in adding great realism to an environment.


Summary

This chapter covered global illumination from a theoretical point of view. It also covered the addition of the photon to track the correct flow of energy through a model. It briefly covered the solid angle and defined a few terms you'll need to help you better understand light transport. You can now use light transport notation to track the path light takes as it intersects diffusive and specular surfaces. You can differentiate between direct and indirect light-ing in a model. In Chapter 3, we covered reflection and refraction. This chapter gives some insight on direct and direct illumination. It is important to understand each lighting effect fully in order to simulate it. This chapter covers some of the most fundamental ideas in developing a 3D environment with a 3D rendering algorithm. We still have a few more subjects to cover before we can start developing an application.


Chapter 5. Other Rendering Techniques

The two most significant rendering techniques that have been developed over the past two decades are ray tracing and radiosity. Ray tracing is a point-sampling technique and radiosity is a finite element rendering technique. Each of these techniques has weaknesses when it comes to rendering natural lighting effects such as reflection, refraction, soft shadows, depth of field, motion blur, color bleeding, caustics, and direct and indirect illumination. A powerful 3D application must be able to correctly render natural lighting effects as seen in nature. This chapter discusses radiosity in general terms and covers ray tracing in detail because it's going to be used in conjunction with photon mapping.


The Basics

The basic idea of any system is to simulate light rays moving from a light source and calculate the intensity based on each infinitely small point on a material. See Figure 5.1. A basic rendering system must therefore have the ability to generate rays, intersect rays with the scene, calculate the lighting per interaction, and perform reflection and refraction. These capabilities can be extended to accommodate more complex natural lighting effects such as diffusive reflections, indirect lighting, and color bleeding.

Figure 5.1. The basic idea behind rendering.

    [ LiB ] Chapter 5. Other Rendering Techniques View or Image Plane


View or Image Plane

In any scene one major component is the observer (or camera). The observer is basically the person who stands and looks at the 3D world from different viewpoints and orientations. The problem for us is that we need to save the information about what is being observed to a medium such as the monitor or a file. In Chapter 2, I said that to be able to view the 3D world you must first project it onto a 2D medium such as the monitor. Rendering whatever is seen from the eyes of the observer is then gathered onto something called the view plane (or image plane). The computer monitor becomes the eyes of the observer. Whatever the monitor conveys is exactly what the observer is looking at in the 3D world. The monitor is commonly referred to as the view/image plane because of this premise. Now you may be thinking to yourself, why do we even need an image plane? The answer is that it was invented with the same idea of how a camera takes a picture.

To better explain this idea, take a look at Figure 5.2. What you see is something called the pinhole camera. The device is set up with a simple closed box with one tiny hole in the front face. As discussed in Chapter 3, light always takes the most guaranteed path to travel to its destination. It will bend if needed to get the job done. The picture is rendered correctly because the light pierces through this tiny hole, hitting the image plane on the opposite side of the box. As each photon hits the image plane this creates the picture. The pinhole idea was created to prevent film on the image plane from becoming overexposed.

Figure 5.2. The pinhole camera.

Similarly, this same image plane is used in computer graphics. This image plane is like looking through a window. If you're standing by the window and move your position around the room a little, you would be constrained to whatever the window shows. Basically the requirement for a virtual window must be utilized to perceive the 3D world. The image plane can be a 4x4 pixel breakdown of a tiny LCD screen or it can be an 800x600 image on the monitor. The bigger the image plane is, the longer it takes to render because you increase the number of pixels per each scan line in the image. The image plane is a red, green, and blue component buffer of colors that programmers apply to their scene class. The correct color information must be calculated for every point in the scene. This calculation becomes more intense if you are working with lots of different colors. High color modes such as 16-bit color (65,536 possible colors) or 24-bit color (16.7 million possible colors) can take a very long time to render as opposed to 256 colors in the old days of rendering. See Figure 5.3 for an example of the view plane.

Figure 5.3. View or image plane of a simple scene facing one sphere and one rectangle.

NOTE

NOTE

Did you know that Hakan Lans was the inventor of computer color graphics and the computer mouse? He is a Swedish inventor who has over 40 patents.He's also responsible for the STDMA satellite navigation system that is the world standard for aviation and maritime industries.Talk about genius,yet so few people know his name.

The image plane records the projected color information of the world seen through the eyes of the observer. This solves the problem of how one sets up a rendering environment to view something if there are infinite positions and orientations to perceive it. Each pixel of the monitor is a breakdown of the color information stored on the image plane, which provides us with a practical way to define a finite instance of visualization within a world of infinite possibilities.

This is how you define an 800x600 view plane in your application:

The top side of the rectangle is: 0
The left side of the rectangle is: 0
The right side of the rectangle is: 800
The bottom side of the rectangle is: 600

Here's the code to do this:

RECT    ViewPlane = { 0, 0, 800, 600 };


Frustum

A persistent problem for 3D rendering is the lack of memory and processing power, two things you can never have enough of. When computers were relatively slow, researchers needed a method that eliminated unwanted objects that weren't being utilized during rendering. This was a major problem because if the image plane didn't include a set of objects in its viewport, the computer algorithm would still process them. This extra unnecessary processing of objects outside the viewport made rendering images extremely slow. Eventually, researchers determined that you could create something called a frustum (a pyramid shaped looking object) to limit the viewing area of the scene. The frustum couldn't be a cube because objects appear larger the closer they are and smaller the farther away they are, so it had to be more advanced to handle the field of view. The frustum is constructed to block out certain objects in the scene that weren't in the viewable area of the observer. A frustum can be seen as a volume that encompasses a selected set of objects in the scene. This idea works well because the frustum basically only acknowledges objects that are within its volume and ignores the objects that are outside. This eliminates unwanted objects being sent through the rendering pipeline, thus saving lots of rendering time. This simple idea became common in 3D rendering applications, especially video games. See Figure 5.4 for an example of a frustum.

Figure 5.4. The frustum is a volume that encompasses a selected set of objects in the scene.

    [ LiB ] View or Image Plane Field of View


Field of View

Now I want to cover the field of view a bit because it's very important when setting up the camera to perceive the world. Everything that can be visualized through a typical camera lens or even the viewing cone of the human eyes is usually measured in angles (mostly degrees). When you look straight ahead at your TV you may not see the walls of the room if they are too far outside the viewable area of your sight. This viewable area is called the field of view. The more you can see is the more the field of view angle increases. Too wide an angle produces a distorted perspective and too narrow an angle produces a flattened image. So you must tweak this angle to create a desirable image. I normally use a field of view of PI/3 (60 degrees) for my DirectX 3D engine at home. See Figure 5.5 for an example of the field of view. The image plane is divided into two events (top-to-bottom and left-to-right), which create a center point. This center point will be used to generate rays for ray tracing.

Figure 5.5. Field of view.

    [ LiB ]


Ray Tracing

Ray tracing, the first computer algorithm for the photo-realistic rendering of 3D objects, is a very simple yet powerful idea. Ray tracing is very good at simulating specular reflections (covered in Chapter 3) and transparency. This is because rays can be easily bounced (or reflected) at mirrors as well as refracted on transparent materials. Major rendering applications like 3D Studio Max implemented ray tracing as the core layer for rendering. Games like Wolfenstein 3D by Id Software used a subset of ray tracing called ray casting as the core layer for rendering. The basic idea behind ray tracing is that you can trace rays in some particular order and calculate the intensity based on each interaction on a material given each light source. Ray tracing excels at curved surfaces because it's a point-based rendering technique. It's also excellent at specular reflections, primarily mirror reflections.

But ray tracing has some major problems in simulating the movement of energy that caused it to incorrectly simulate natural lighting effects such as diffuse reflections and indirect illumination. As you will see later, ray tracing does a poor job at computing the penumbras in shadows. It also does a poor job at simulating the scattering of light on diffuse surfaces. Over the many years of research many new subsets of ray tracing have been developed. The next section briefly covers a few techniques because you will use two of these later. There are many more advanced techniques such as Stochastic Sampling, Beam Tracing, Cone Tracing, and Pencil Tracing that aren't covered in this book.

NOTE

NOTE

Ray tracing was invented by Turner Whitted in 1980.He published a paper called "An Improved Illumination Model for Shaded Display," which got the ball rolling for everyone.

Forward Ray Tracing

Forward ray tracing works with the idea of tracing light particles (photons) from the light sources as they intersect objects in the scene and then finally reach the image plane. See Figure 5.6 for an example of forward ray tracing.

Figure 5.6. Forward ray tracing.

This technique is very good at computing the coloring of each object, but it has some major problems. This technique is very processor-intensive because each ray being traced from each light source has to be calculated as it intersects each object in the scene. This typically included many wasted rays that never hit the image plane. Although significant in its time as a major step in the direction of photo-realistic rendering, it is, obviously, a very inefficient approach.

A better approach developed later is called backward ray tracing, which traces rays in the opposite manner. Forward ray tracing is also known as light ray tracing or photon tracing. You'll use this idea later during the development of photon mapping. For now, just keep it in mind.

NOTE

NOTE

When ray tracing was first developed as the only method available for photo-realistic rendering,the only people who actually used it were operating on big UNIX mainframes and Cray supercomputers. Slow by our standards now,it is (academically) biased to judge the method retrospectively as a "bad approach." In its time,it was the only approach.

Backward Ray Tracing

Backward ray tracing was developed to increase ray-tracing efficiency. The basic idea behind this technique is that a virtual ray originating from the eye is sent out through each pixel of the image plane. This technique eliminates the computational strain on the processor because the work is limited only to where it matters; where rays intersect viewable objects. After the rays leave the eye and scatter around in the scene, the color is computed based on each intersection. The new value is then saved on that specific (x,y) position from where it originally left on the image plane. For example if the image plane was a 2x2 image, a typical application would spawn one ray per each pixel. See Figure 5.7 for an example of backward ray tracing.

Figure 5.7. Backward ray tracing

The problem with backward ray tracing is that it assumes light particles are moving in only one direction and that light is being traced from the image plane and not from the light sources. This creates a problem in correctly computing a pixel's intensity. It also creates other problems with indirect lighting effects such as shadows and multiple specular reflections. You'll soon see how this technique is used extensively for ray-tracing applications. It will also be used in conjunction with photon mapping to render the final image.

Bi-directional Ray Tracing

Bi-directional or hybrid ray tracing was developed to fix the problems inherent in forward and backward ray tracing. Researchers wanted to develop a new technique that was optimized for speed and accuracy. The system was set up where, based on a certain set of conditions, the application would select forward or backward ray tracing. Sometimes it would use both to correctly compute a pixel's intensity. This was an improvement but this too had some problems because sometimes the intensity of a pixel traced backwards was very different from the intensity traced forward. See Figure 5.8 for an example of bi-directional ray tracing.

Figure 5.8. Bi-direc-tional ray tracing.

    [ LiB ] Field of View Distributed Ray Tracing


Distributed Ray Tracing

This technique is just a rollover from the previous techniques. Distributed ray tracing was used for effects such as depth of field, filtering, motion blur, and texture interpolation. It was developed because many times images rendered with only one ray per pixel would have a few calculation problems which made rendered images appear unclear with tiny artifacts also known as noise. To fix the problem, researchers realized that instead of tracing one single ray to determine the intensity of a pixel, many rays would be traced instead to calculate the right pixel intensity. The tracing of a ray to a pixel is called sampling. When a series of sampled rays equals the same value consecutively, this is supersampling. Many researchers incorporated supersampling into their applications because it guaranteed a pixel's color by testing many samples of the same pixel to ensure that the correct color was returned.

NOTE

NOTE

Distributed ray tracing was introduced in 1984 by Robert L.Cook, Thomas Porter,and Loren Carpenter.

Other problems soon were seen in supersampling that needed to be addressed. If many tested samples differed in value, researchers needed to somehow dive deeper into finding a method to subdividing each pixel into small components. A new technique called adaptive supersampling was invented. It kept applications testing until the average became fluent. The idea behind adaptive supersampling was to break up an ever-changing value into smaller regions and test each region as a whole value. This made rendered images appear much clearer but this wasn't perfect. You would have to test a large number of samples to generate a very detailed image. This suddenly became worse if the resolution of the image was significant in size. A higher resolution required lots of processing power that, in the end, made 3D rendering applications slow. Take a look at Figure 5.9 for an example of ray tracing. The image conveys multiple diffuse interactions as well as specu-lar reflection and indirect illumination. These effects are easily achieved using ray tracing.

Figure 5.9. Example of ray tracing that displays multiple diffuse interactions.

    [ LiB ]


Radiosity

Radiosity is a much better approach to computing global illumination because, compared to ray tracing, it's a much more realistic algorithm. Radiosity has been around for some years and took a long time to become a mainstream application. This was because the original design, which utilized a matrix solution to fin the radiance, was very complex. Radiosity works with the simple idea of energy being distributed into a scene or finding the equilibrium of light exchange between surfaces. As photons leave a light source the energy is spattered onto materials. The illumination information is then stored on the surfaces in the scene. The trick to radiosity is that each surface becomes a secondary light sourceand emits light particles onto neighboring surfaces. Then those neighboring surfaces emit light onto their neighbors and the process continues until all of the light energy is distributed into the scene. Of course, not all of the light is re-emitted. Some of the light is absorbed into the material to a degree consistent with the material's properties. This distribution of energy is similar to how nature actually distributes light.

If a red particle hits surface A from a given light source and some of it is then re-emitted onto a neighboring surface, such as surface B, surface B will receive some of this red color information. If surface B was originally white, it would now have some red particles bleeding across the white. Surface B is a composite of red and white particles between two surfaces and the light source. This is a big advantage over ray tracing because energy from one surface can mix with others. This effect on surface B is known as color bleedingand this definitely increases realism in rendering synthetic images. This is because a collection of different colored diffused particles from many surfaces can become the color makeup of one single surface. Radiosity is very good at simulating diffuse reflections because it scatters and distributes the light in a random fashion correctly. This is a big improvement over ray tracing because ray tracing lacks the ability to correctly reflect diffuse lighting. Ray tracing uses a single point being reflected in only one direction, whereas radiosity uses a set of energy particles (photons) that are reflected in many directions. The intensity varies per particle, whereas ray tracing uses only a constant to track energy. With these major improvements, radiosity proved to be superior to ray tracing. See Figure 5.10 for an example of radiosity. The rendered image shows how radiosity increases realism in a scene by simulating diffuse reflections. Everything in the image seems to be more synthetic because of random particles being reflected in arbitrary directions. Radiosity is also better at simulating indirect illumination such as shadows. The shadows aren't perfectly one dark shade; you can actually see the penumbras in the shadows.

Figure 5.10. Example of radiosity shows how it increases realism in a scene by simulating diffuse reflections.

NOTE

NOTE

Radiosity was introduced in 1984 at Cornell University in a paper called "Modeling the Interaction of Light between Diffuse Surfaces" by Goral,Torrance,and Greenberg.

Radiosity soon made its way into professional 3D rendering packages such as Bryce 3D, Lightscape, Light Flow, and Lightwave 3D. Games like Quake and Unreal also utilized radiosity by storing the illumination information as light maps. The illumination information is then combined with the game's level geometry for high-resolution real-time rendering. Radiosity hit the jackpot in rendering realistic images because most of the materials rendered in most scenes were diffuse reflections. But even radiosity, like ray tracing, has its dirty laundry. Radiosity works with the bad idea of taking a given geometry and subdividing it into smaller regions also known as patches. See Figure 5.11 for an example of this subdivision.

Figure 5.11. Subdivision of the geometry using patches.

This creates a subdivided meshof the original geometry in order to break up each surface into many small regions to store the variance in color information. The solution to a better and more realistic render is more patches per surface. I've seen applications that divide each polygon in the mesh into patches of a unit in length (across and down), which guaranteed a realistic render. The tradeoff for this is time and memory. The more patches generated, the more memory is allocated. If the model is very big, the algorithm will keep allocating memory until there is no more physical memory available. This will, in turn, begin paging the hard disks that will probably slow down the render tremendously: The renders will now vary from two to 12 hours.

Another major problem is accuracy: If the model isn't properly subdivided, you will have artifacts in the final render when computing the correct flow of light between different types of materials. This is fine for small models with common materials but becomes trivial for larger-scaled models. A common problem in radiosity is shadow leaking,or shadows being incorrectly computed because of the mesh not being constructed properly. A typical example is jagged shading at surface edges between perpendicular surfaces. This required applications to carefully construct and subdivide the mesh to generate a correct and desirable image. The subdivision and storing of illuminative information on surfaces is a good idea but it creates a few problems of its own and lacks things that were easily implemented by ray tracing. For example, radiosity has problems with specular reflections and transparency that were easily done using ray tracing. Radiosity is currently the best-known rendering technique to date but hopefully one day it will soon be replaced by photon mapping.


Hybrid/Multi-passRadiosity

The next logical step for researchers was to combine radiosity and ray tracing techniques into one single solution. A typical application would take a given geometry with different materials and trace the light from the light sources to the surfaces using ray tracing. It would then select ray tracing to render the specular reflections and transparency effects. Then it would utilize radiosity to render the indirect illumination and diffuse reflections. This would be a typical example of a multi-pass or a hybrid ray tracing/radiosity application.


Backward Ray Tracing Illustration

Now let's do a simple visual example of backward ray tracing. Take a look at Figure 5.12.

Figure 5.12. Four by four view/image plane.

If you subdivide the image plane into a 4x4 grid of pixels you would have a total of 16 pixels (4x4=16). Now all you have to do is generate a ray from the center point to leave in the direction of each pixel on the image plane. The ray leaves the eye and passes through each pixel on the image plane. The ray is then sent out into the world, its job is to return with color information about whatever surface it interacts with first. See Figure 5.13 for an example of ray pixel plotting.

Figure 5.13. Ray pixel plotting.

Here's the general idea in six easy steps:

  1. Traverse from left-to-right on the view plane.

  2. Traverse from top-to-bottom on the view plane.

  3. Set up a ray.

  4. Trace the ray.

  5. Compute the pixel color.

  6. Save the pixel color.

It's as simple as that. Try to imagine the process as putting a sheet of paper in front of you and shooting holes in the paper with a gun. The bullets of the gun would be the rays. As each bullet leaves the gun and moves through space it will hit something. Now try to imagine each intersection of the bullet as a token to return color information of each point it hits. Whatever the bullet hits first is what the pixels on the image planethe holes in the paper record. The image plane is typically much larger (640x480, 800x600, or 1024x768) to accommodate the monitor and also because I certainly don't want to strain my eyes looking at a 16x16 render. It's important to point out that the smaller the image plane is the faster it will render and that's why many applications perform a low-resolution render before they conduct the final render.

NOTE

NOTE

Photon mapping was invented by Henrik Wann Jensen.The first concepts for the photon mapping algorithm were introduced in 1994.


Summary

This chapter covered the two major types of rendering techniques. It discussed in some detail each technique's positives and negatives. It is very important for any graphics programmer to know each technique in detail because many times an application could be easily upgraded to handle a specific effect by choosing a technique best suited for a particular situation. Some researchers think it's bold to go off and begin inventing algorithms on their own without even thinking about the ones that are already available. It may be good to derive something new and innovative but don't re-invent the wheel if the wheel runs well (smile). Take what is here and make it better, build on top of the hard work researchers have already done for you. Henrik Wann Jensen did this when designing photon mapping. You will use backward and forward ray tracing in the development of photon mapping.


Chapter 6. Basic Structures

You have covered a lot of basic material in the previous chapters and haven't really done any practical work yet. In this chapter, you are going to begin building the basic data structures for setting up a 3D rendering environment. You will begin with the basic components for setting up a 3D environment and then develop the required data structures accordingly. This chapter covers each structure in detail. The structures are mostly written in C++.


Using Vectors and Points

A single dimensional value is called a scalar. A scalar is any real number that can be arithmetically used, for example 1, 2, 37, 50.5, 34.9, 100, or 1024. Scalars are used in almost everything from accounting to counting (smile), but are limited because they are only one dimensional in definition. Scalars define magnitude but in the definition of 2D and 3D space, scalars just cannot be used; therefore, points and vectors came to be as the solution.

Vectors and points are simply two- or three-component scalars that have a specific meaning. Vectors and points are covered together because they are like distant cousins. Points just define a location in space as opposed to vectors, which define direction and magnitude. Points can be seen as locations in space that define the area of an object. For example, points are commonly used as the center location of spheres and the vertices of triangles or polygons. Points come in the form of two (x and y) or three (x, y, and z) components, as do vectors.

Vectors are a bit more complex because they use the same (x, y, z) components in a different form. Instead of using them as locations, they use them for direction and magnitude. The direction can be visualized as the movement from one point to another in 2D or 3D space. The magnitude of a vector is the distance from the world origin (0, 0, 0) to the point's current location. This value can be easily found using the Pythagorean theorem. Vectors are important to 2D and 3D graphics because they represent the arrows of direction and length in a simple form.

Let's begin with the construction of the Vector class that will also be used for points. This chapter discusses each method in detail to give you a broader understanding of each area.

The Vector Class

The class data will be composed of the x, y, z components, arithmetic operators, and axis-aligned vector constants. You will also have a variety of member functions that will be defined to follow a set of rules for manipulating vec-tors. It is very important to define this class thoroughly because it's the building block for the 3D graphics application.

struct cVector3
{
   union
   {
      struct
      {
             float x,y,z; // Three real components of the vector
         };
float v[3]; // Array access useful in loops
};

// Default constructor
cVector3()
{
}

// Custom three-member input constructor
cVector3( float X, float Y, float Z ) :
   x(X), y(Y), z(Z)
{
}

// Return the magnitude of a point
float Mag() const;

// Normalizes a vector (makes its length one)
void ormalize();

// Invert the vector
void Invert();
  // Constant vectors
  static const cVector3  vZero;   // <0,0,0>

  // Custom operators
  cVector3& operator += ( const cVector3& in );
  cVector3& operator -= ( const cVector3& in );
  cVector3& operator *= ( const float& in );
  cVector3& operator /= ( const float& in );
  cVector3  operator  - (); // this is plain old negation
};

Constant Vectors

The cVector3 class contains only one constant. The constant is a vector that is set to (0,0,0) for the internal components. By adding a vector constant, you can simplify the process of assigning (0,0,0) to the internal components at any time in the application.=

// for constant zero vector
const cVector3 cVector3::vZero   ( 0.f, 0.f, 0.f );

Using Vector Operators

You are going to use the mathematical arithmetic operators for the cVector3 class. You can add, subtract, multiply, and divide vectors just as you can scalars. Using C++ makes the code more readable and clearer because it takes out redundant structure-related statements and makes them structure-indepen-dent. You'll learn to expand the data structure as much as you can to accommodate any type of situation you might encounter using vectors and points.

NOTE

NOTE

Using the arithmetic operators in the vector class makes vector mathematics much simpler.The different vector methods are independent to the vector class and are utilized via the operators.This helps to make code much easier to read and understand.

Vector Addition

You can add vectors the same way that you add scalars (8+2=10). The only distinction is that the extra component(s) must be taken into account for addition. A typical example is V1<3,2,0> + V2<0,2,0> = V3<3,4,0>. Figure 6.1 shows an example of vector addition.

Figure 6.1. Vector addition lets you add two vec-tors together by using the + operator;for exampleV1<3,2,0> +V2<0,2,0> =V3<3,4,0>.

The process of adding vectors can be graphical or numerical. From a graphical point of view, you take the first vector placed with the starting point at the origin and then "add" the other vector to it simply by placing the second vector's starting point at the end or tip of the first vector. The resulting vec-tor is constructed by drawing a third vector from the origin to the tip of the second vector. On the other hand, to add vectors numerically, you simply add the x components, they components, and the z components, respectively. Let's define the pseudo code. This is all possible by utilizing the + operator.

inline cVector3& cVector3::operator += ( const cVector3& in )
{
   x += in.x;
   y += in.y;
   z += in.z;
   return *this; // This is so we can chain operations together
}

inline const cVector3 operator+(cVector3 const &a, Vector3  onst &b)
{
   return cVector3
   (
      a.x+b.x,
      a.y+b.y,
   a.z+b.z
  );
};

Vector Subtraction

The process of subtracting vectors is the opposite of adding vectors. From a graphical point of view, you take the first vector placed with the starting point at the origin and then "subtract" the other vector simply by placing the second vector's starting point at the end or tip of the first vector. The resulting vector is constructed by drawing a third vector from the origin to the tip of the second vector. To subtract vectors numerically, you simply subtract the x components, the y components, and the z components, respectively.

Vector subtraction is subtracting one vector from another vector or from a scalar. A typical example is V1<3,5,0> V2<1,2,0> = V3<4,3,0>. See Figure 6.2 for an example of vector subtraction.

Figure 6.2. Vector subtraction lets you subtract two vectors by using the operator.For example,V1<3,5,0> V2<1,2,0> =V3<4,3,0>.

Vector subtraction is the opposite of vector addition. The operator is used for vector subtraction. The procedure for vector subtraction is subtracting the x, y, and z components between the two vectors. Vector subtraction can be used to scale and translate objects. Let's define the pseudo code:

inline cVector3& cVector3::operator -= ( const cVector3& in )
{
  x -= in.x;
  y -= in.y;
  z -= in.z;
   return *this; // This is so we an  hain operations together
}

inline const cVector3 operator-(cVector3 const &a, cVector3 const &b)
{
   return cVector3
   (
      a.x-b.x,
      a.y-b.y,
      a.z-b.z
   );
};

Vector Scaling

Vector scaling is just like scalar multiplication. The only difference is that the scalar must be multiplied by each (x, y, and z) component of the vector. Scaling a vector involves multiplying each x, y, and z component of one vec-tor by a scalar. In a graphical example, the first scaled vector yields a new vector that is either less than or more than the first vector's tip. Vector scal-ing is typically used to make 3D objects smaller or larger. For example, multiplying the scalar by the vector is illustrated as V1<2,2,0> * 2 = V2<4,4,0>. See Figure 6.3 for an example of vector scaling. The new point location is determined after scaling the vector, whereby each component of the vector is multiplied by the scalar. The general idea of vector scaling is that it stretches a vector by a scaling factor. This is all possible using the * operator.

Figure 6.3. Vector scal-ing stretches a vector by a scalar;for example,V1<2,2,0> * 2 =V2<4,4,0>.

inline cVector3& cVector3::operator *= ( const float& in )
{
   x *= in;
   y *= in;
   z *= in;
   return *this; // This is so we can chain operations together
}

inline const cVector3 operator*(cVector3 const &a, float const &b)
{
   return cVector3
   (
      a.x*b,
      a.y*b,
      a.z*b
   );
};

inline const cVector3 operator*(float const &a, cVector3 const &b)

    return cVector3
    (
       a*b.x,
       a*b.y,
       a*b.z
    );
};

Vector Division

Vector division can be used to scale a big object into a smaller one or vice versa. The scaling of a vector is represented as V1<6,3,0> / 3 = V2<2,1,0>. See Figure 6.4 for an example of vector division. Vector division is the opposite of vector scaling. It shrinks a vector by a scaling factor. This is all possible using the / operator.

Figure 6.4. Vector division shrinks a vector by a scalar factor;for example,V1<6,3,0> / 3 = V2<2,1,0>.

inline cVector3& cVector3::operator /= ( const float& in )
{
   float inv = 1 / in;
      // do one (expensive) divide and three (cheap) multiplies
   x *= inv;
   y *= inv;
   z *= inv;
   return *this; // This is so we can chain operations together

}

inline const cVector3 operator/(cVector3 const &a, float const &b)
{
   float inv = 1 / b;
   return cVector3
   (
      a.x*inv,
      a.y*inv,
      a.z*inv
   );
};

Vector Inversion

Vector inversion is the process of flipping the signed axes of the internal (x, y, z) components. The invert (or inverse) of a vector is illustrated as V1<4, 0, 0> * 1 = V2<4, 0, 0>. The purpose of invert is to flip a vector in the opposite direction. For example, if a vector were traveling in one direction, it would then be traveling in the opposite direction after being inverted. Think of it as the two lanes on the highwayyou're either going one direction or the other. Vector invert flips a vector by multiplying it by 1, as shown in Figure 6.5.

Figure 6.5. Vector invert flips a vector by multiplying it with 1;for example,V1<4,0,0> * 1 = V2<-4,0,0>.

inline cVector3  cVector3::operator - ()
{
   return cVector3(
      -x,
      -y,
      -z
   );
}

inline void cVector3::Invert()
{
   x = -x;
   y = -y;
   z = -z;
}

Vector Assignments

You are going to write a function that assigns the internal x, y, and z components to three passed parameters. The three passed parameters are X, Y, and Z.

inline void cVector3::Assign( float X, float Y, float Z )
{
   x=X;
   y=Y;
   z=Z;
}

Vector Equality

Vector equality is the same process as testing scalars. It would be nice to test all components (x,y,z = x2,y2,z2) by each other, but again you encounter floating-point number imprecision issues. This requires an epsilon that is equal to 0.001; think of the epsilon as a very tiny number. You can use it to determine whether two floating-point numbers are close enough to each other to be equal. The method passes two vectors as parameters. The internal components are subtracted from each other and compared to the epsilon. If the distance between the two vectors is smaller than the epsilon for the (x, y, and z) components, the method returns true.

inline bool operator==(cVector3 const &a, cVector3 const &b)
{
   if(fabs(a.x-b.x)<EPSILON)
   {
      if(fabs(a.y-b.y)<EPSILON)
      {
         if(fabs(a.z-b.z)<EPSILON)
         {
            return true;
         }
      }
   }
   return false;
};

NOTE

NOTE

The advantage to using the equality operator == for a comparison between two vectors is that it's similar to using the same operator for doing a comparison in C with char-acters,integers,and real numbers.

Vector Magnitude

The magnitude of a vector describes its length. In many cases, you'll want vectors to have unit length of 1.0 because it makes the math easier to work out. The magnitude of a vector is the distance from the origin to the tip of the vector. To find the magnitude of a vector, you must go back to the old days of geometry. The answer lies by using the Pythagorean theorem (A_ + B_ = C_). Normally, all points are offsets that begin at the world origin, which is (0,0,0). See Figure 6.6 for an example. Let's try to find the magnitude of 2D vector (3,2).

Figure 6.6. The magnitude of a vector is its length.

The process of finding the magnitude of a vector is as follows:

  1. You must first determine how far each component is away from the origin (0,0) on each axis.

    You know that the x component of the vector is positive 3 units away from 0 and can be visualized as the adjacent side of the triangle.

    You know that the y component is the hypotenuse of the triangle and is 2 units up.

  2. To solve for the adjacent or the missing side, you simply use the Pythagorean theorem. For example:

    (3*3 = 9 + 2*2 = 13) = square root of (13) = 3.6

    The answer is 3.6 for the magnitude, which means that the vector is 3.6 units away from the ray's origin.

Many programmers normalize the direction vector to 1.0 and use the magnitude to find the offset position for a point that is on the vector's path. You'll learn about this soon when constructing the ray class. The demonstration uses a 2D vector, but the process is the same for 3D vectorsyou just have to add the extra z component.

inline float cVector3::Mag() const
{
   return (float)sqrt( (*this) * (*this) );
}

NOTE

NOTE

The Pythagorean theorem is used to find the magnitude of a vector.

The Dot Product

The dot product is one of the most important procedures in vector mathematics. You'll likely use it more than any other vector function. It is used extensively because it returns very important information on the relative angle between two vectors. One of the dot product's most important uses is to compute the angle between two vectors. The equation of the dot product of two vectors U and V is as follows:

(U . V) = (ux*vx, uy*vy, uz*vz) = cos (theta) * |U| * |V|

Now of course U dot V is just a simple multiplication of the vectors' components but the symbolic meaning goes farther than what you see.

So rearranging terms a little, you can solve for the angle theta:

cos (theta) = (ux*vx, uy*vy, uz*vz)
                      |U| * |V|

This means that (U . V ) can give a gross relationship between these two vec-tors. The cosine function has a range of 1 to 1. If the angle is more than 0, the two vectors face each other. If the angle is less than 0, they are facing away from each other. Figure 6.7 shows an example of the dot product.

Figure 6.7. The dot product finds the angle between the two vec-tors U and V.

To find the angle, you simply multiply vector U * V. Again, the formula is as follows: U dot V = | U | * | V| = cosine (angle). The result is in unit space because in trigonometry, the ratio of division between two sides of a triangle is all that really matters. Unit components can be scaled up to any large number, but are the same in ratio. So it doesn't really matter if you're dividing ( SideA [8] / SideB [4] = 2.0 )or ( SideA [1.0] / SideB [0.5] = 2.0 ), you'll get the same ratio. Try to imagine you are working with very tiny triangles with sides of lengths in millimeters that produce results of (0.5000 = sin(30 °), 0.5000 = cos(60 °))in the division between two triangle sides at some arbitrary angle. Figure 6.8 illustrates the angular meaning between two vectors.

Figure 6.8. The angles between vectors using the dot product.

The meaning of the returned result (shown in Figure 6.8) is as follows:

  • If ( A dot B ) is > 0, the angle between A and B is acute and is less than 90 degrees.

  • If ( A dot B ) is < 0, the angle between A and B is obtuse and is more than 90 degrees.

  • If ( A dot B ) is = 0, the vectors are perpendicular and the angle is 90 degrees.

The dot product is used for hidden surface removal, lighting calculations, and many more operations. It's used in ray tracing to calculate the direct lighting for diffuse surfaces. Let's define it as the *operator for the vector class.

inline const float operator*(cVector3 const &a, cVector3 const &b)
{
    return a.x*b.x + a.y*b.y + a.z*b.z;
}

NOTE

NOTE

The dot product is extensively used in 3D games for hidden surface removal.

The Cross Product

Many times in the development of 3D graphics algorithms, you'll need to find the normal of two vectors. The normal is a vector that is perpendicular to the two vectors in question, thus creating a mutually perpendicular (orthogonal) system. Triangles are defined by three points in 3D space and polygons are just an extension of triangles with n points. Remember that you need at least three points to define the orientation of a triangle and polygon. The cross product is used to find a vector that is facing away from the triangle or polygon face. This value is normally a length of one, which brings up the next topic. See Figure 6.9 for an example of the cross product.

Figure 6.9. The cross product is a perpendicular vector between three points on a surface.

The cross product is used for the following:

  • It can be used to compute the normal for a flat surface (polygon, triangle).

  • It can be used to compute the direction a polygon or triangle is facing.

  • It computes the angle relationship between triangles and polygons. This is conducted by using the simple dot product method (angle between two vectors). See Figure 6.10 for an example of the face-to-face relationship. This is normally used to determine whether a series of polygon/triangles are facing an arbitrary polygon/triangle.

    Figure 6.10. The face-to-face relationship between flat surfaces (such as polygons and triangles).

  • It generates a plane for a triangle or polygon by computing the normal and the distance to the plane.

The dot product method is defined with the ^ operator. The method passes two vec-tors in as parameters and calculates the cross product between the two.

NOTE

NOTE

The cross product is used to generate a plane for a polygon or triangle.

inline const cVector3 operator^(cVector3 const &a, cVector3 const &b)
{
   return cVector3
   (
      (a.y*b.z-a.z*b.y),
      (a.z*b.x-a.x*b.z),
      (a.x*b.y-a.y*b.x)
    );
}

Unit Vectors

Just as you normalized colors in Chapter 2, you can normalize vectors as well. The idea behind normalizing vectors is to change the components of the vector such that the direction is maintained, but the overall magnitude or length is 1.0. During normalization, the length from the origin changes, but the direction does not. To find the magnitude to a vector, you simply divide each (x, y, and z) component into itself (or internal magnitude) to generate a magnitude of 1.0. For example, V(255,0,0) normalized will be converted to ( V(255 / 255, 0, 0 ) = Unit Vector ( 1,0,0) ), similar to how you normalize colors.

Normalizing a Vector

To normalize a vector and make it less than one, you must divide the magnitude by one and then scale each (x, y, and z) component down using this value.

inline void cVector3::Normalize()
{
   float invMag = 1.f/Mag();

   (*this) *=  invMag;
}


Finding the Plane in Which a Polygon/ Triangle Sits

The world in a 3D environment is composed of infinite planes. Now most people don't realize it, but the X, Y, and Z axes create flat planes. These planes sit flat on any two axes at a time. For example, the X and Y axes create a flat plane. This plane is called an axis-aligned plane.

Objects sit in their own planes and you can have infinite planes where each object sits. A common technique used in 3D graphics is to find the plane of a polygon or triangle. The definition of a plane is a unit vector normal depicting the direction a polygon/triangle is facing. A scalar tracks the distance the plane is away from the origin of the world.

The vector normal is computed by using the cross product of three points on the triangle/polygon. This value is then scaled down to a baseline value of 1.0.

To find the plane of a flat surface (triangle or polygon), you take three of the points on the surface and find two line segments between them. These segments are two vectors that originate from one base point on the surface extending to the other two points. This depicts two sides of the surface:

SideA = Vector2  Vector1
SideB = Vector3  Vector1

Crossing the two sides creates a perpendicular vector to the surface. This means the plane normal is found.

Normal = SideA ^ SideB

The perpendicular vector is long and can become a problem, so it's best to normalize the vector. Normalizing a vector forces a vector's magnitude to be 1.0.

Normal = Normal.Normalize()

The next thing you must do is find the plane distance. The plane distance is found by multiplying one point of the surface by the plane normal. The plane distance is a scalar that tells how far the plane is away from the origin of the world.

Plane_Distance = - (Normal * Vertex1)

By using the dot product with the normalized vector and the plane distance, you can determine whether a point lies in front, behind, or exactly on a plane, triangle, or polygon (the flat surface).


Creating a Light Source

The light source is the most important object in a 3D environment. If the light source were nonexistent, the world would be dark. The most common type of light source in most 3D applications is the point light. This point light source is like a light bulb that emits energy equally in all directions in a spherical pattern. The imaginary energy (in the form of photons) that's emitted from the point light source is then spattered onto nearby objects such as planes, polygons, and spheres. The intensity of a point is relative to the angle and intensity of the light source. The farther away the object is from the light source, the weaker the overall intensity. This is known as the intensity falloff effect and can be applied to 3D renderers.

There are many types of light sources. The light that's emitted from a light source can be equal (point light), directional (spot lights), or area based (area lights). Let's define a light class (called cLight) that encapsulates the basic attributes of the point light. The four major components you want to implement in the class are the light's position, its color, the power/wattage, and its active state. You'll have a few methods applied to the class for updating and changing data. Three constructors are to be defined in the class to accommodate the different types of parameters. One method is also required to assign the member data in the class at any time in the application.

NOTE

TIP

You can extend the light class to accommodate different types of lights such as spot lights and directional light.

class cLight {

public:

   cVector3 vPosition;
   color3   Color;
   float    fWattage;
   bool     bActive;

   cLight()
   {
   vPosition   = cVector3::vZero;
   Color       = color3::White;
   bActive     = true;
   fWattage    = 1.0f;
   };

   cLight(float x, float y, float z, float wattage)
   {
   vPosition   = cVector3(-x,-y,z);
   Color       = color3::White;
   bActive     = true;
   fWattage    = wattage;

   };
   cLight(float x, float y, float z, float wattage, color3 color)

   {
    vPosition   = cVector3(-x,-y,z);
    bActive     = true;
    fWattage    = wattage;
    Color       = color;

 };
void Setup(float x, float y, float z, float wattage,
     color3 color = color3::White)
{
 vPosition   =  cVector3(-x,-y,z);
 bActive     =  true;
 fWattage    =  wattage;
 Color       =  color;
 };
};


Using the Ray

You learned about rays in Chapter 3; this section recaps a bit on what you learned there. Typical lines have a starting and ending point. Rays have a starting pointan origin where they begina direction in which they are heading, the distance they have traveled, and a destination. Rays will be used in ray tracing and photon scattering. As Chapter 3 mentioned, rays can be used to simulate the movement of light from one point to another. Rays are constructed to convey the line segment from any two points in 2D or 3D space, as shown in Figure 6.11.

Figure 6.11. An exam ple of a ray.The O refers to the origin of the ray,Dir refers to the direction of the ray,D refers to the destination of the ray,and Distance refers to the length of the ray.

For example, the origin of a ray can begin at the surface level of one object (sphere, triangle, or polygon) and the destination can end at the surface level of another. Rays can also begin at the light source and end at the object's surface level. Rays are perfect for object intersections, as you will soon discover. The four major components you'll declare in the ray class are the origin, the direction in unit vector, distance as a scalar, and the destination. Let's begin constructing the ray class. The class will consist of the independent variables of the cRay class and a variety of class methods. Two constructors are defined to set up the cRay class, one for general construction and the other for parameters.

NOTE

TIP

A very thin beam of light is classified as a ray.Ever heard the phrase ray of light? The ray class is fashioned from a ray of light seen in nature.

class cRay
{
public:
   cVector3 origin,
            direction,
            destination;

   // how far we have traveled
   // from the origin
   float distance;

public:
   // Default constructor
   cRay()
   {
      origin = cVector3::vZero;
      direction = cVector3::vZero;
      distance = 0.0f;
   }

   // Custom  two-member input constructor
   cRay( cVector3 o, cVector3 d )
   {
      origin = o;
      direction = d;
   }

Setting the Ray's Origin

The first thing that is needed for a ray is the origin. The origin can be relative or absolute. For example the origin of a ray can begin at a certain point in space or it can be begin from the world origin (0,0,0). The origin of the ray is very important in order to calculate the ray direction.

inline void Set_Origin( float x, float y, float z )
{
 origin.x = x;
 origin.y = y;
 origin.z = z;
}

inline void Set_Origin( const cVector3 &o )
 {
  origin = o;
 }

Finding the Ray's Direction

To compute the direction vector of a ray, you must subtract the destination from the origin. This returns the direction vector between the origin and the destination point. To find the actual distance between these two vectors in scalar form, you simply find the magnitude from the origin to the destination point. You then normalize the direction vector to unit space.

inline void Find_Direction( void )
 {
  direction = destination - origin;
  distance = direction.Mag();
  direction.Normalize();
 }

Setting the Ray's Destination

When assigning the destination point directly, the class will automatically compute the distance and the direction vector, as follows:

inline void Set_Destination( float x, float y, float z )
 {
  destination.x = x;
  destination.y = y;
  destination.z = z;
  Find_Direction();
 }
inline void Set_Destination( const cVector3 &d )
 {
  destination = d;
  Find_Direction();
 }

Finding the Ray's Destination

To compute the destination point, you add the direction vector to the origin and multiply it by the distance traveled. The formula is as follows (destination = origin + direction * distance). This is used for ray object intersections:

   inline void Find_Destination()
    {
     destination = origin + direction * distance;
    }

Setting the Ray's Direction

This function is used to set the direction and compute the distance and the destination points' given the direction as a parameter.

   inline void Set_Direction( const cVector3 &dir )
    {
     direction   = dir;
     destination = origin + direction;
     distance    = dir.Mag();
    }

Normalizing the Ray's Direction

This method will be used to normalize the direction vector to unit space.

   inline void Normalize_Direction( void )
    {
    direction.Normalize();
    }

};  // end of class


Adding Reflection and Refraction Features

Because this is a 3D book and you're developing a 3D environment, you should try to make all 3D functions as simple as possible. For example, the reflection and refraction code can be incorporated into the cVector3 class because it mostly deals with points. Let's add this extra functionality to the cVector3 class for reflection and refraction. Remember reflection can be broken into diffuse reflection and specular reflection. Diffuse reflections scatter light in arbitrary directions, whereas specular reflections reflect light in one particular direction. You must also write a method to refract light between two mediums (such as from air to water). You then add all of this logic to your reflection and refraction methods.

Diffuse Reflection

The code for diffuse reflection is extremely simple. All you need to do is generate a random number for each x, y, and z component for a vector in a unit sphere. This vector range is between 1 and 1. The function yields a vector pointing in some arbitrary direction in the unit sphere. The trick to implementing this method correctly is to remember that surfaces are flat and light can't diffusively reflect inside the surface. So all reflection must be done in front of the surface. This means the dot product determines whether the random vector is pointing below the flat surface and, if it is, it will invert it. Finally, the function returns with the new random diffusively reflected vector.

This method requires the normal of the surface to diffusively reflect light. The random X, Y, and Z directions are generated by using ((1 + 2*(float)rand() / (float)RAND_MAX)). It will reflect a vector around the normal and determine whether the vector is pointing in the wrong direction. If it is, it will be inverted.

cVector3 cVector3::Diffuse_Reflection( cVector3 Normal )
{

  cVector3 r;

  // get x,y,z random numbers between -1 to 1 for float
  r.x = (-1 + 2*(float)rand() / (float)RAND_MAX);
  r.y = (-1 + 2*(float)rand() / (float)RAND_MAX);
  r.z = (-1 + 2*(float)rand() / (float)RAND_MAX);

  float dot = r * Normal;
  if (dot < 0.0f)
    return r = r*-1.0f;
  return r;

}

Specular (Mirror) Reflection

The specular reflection formula works like this: For the incoming ray direction (given as a unit vector), and the point normal (or vector normal of surface), the reflected ray is given as R.

Angle = - Find the Angle Between (Point Normal, Ray Direction)
R   = Ray Direction + (2.0  * Point  Normal * Angle)

You can now upgrade the cVector3 class to accommodate vector mirror reflections. The method will pass the surface normal in as a parameter and will return with the new reflected vector. The function will return a reflected ray that is the opposite of the incident ray. This function can be used for mirror surfaces that reflect directly in the opposite direction.

cVector3 cVector3::Vector_Reflection(cVector3 normal)
{
   float Angle;
   cVector3 Me;
   cVector3 reflected;

   Me = *this;

   Me.Invert();
   Me.Normalize();
   normal.Normalize();

   Angle = Me * normal ;

   if (Angle < 0)
   {
      normal.Invert();
      Angle = Me * normal;
   }

   reflected.x = 2.0f * normal.x * Angle - Me.x;
   reflected.y = 2.0f * normal.y * Angle - Me.y;
   reflected.z = 2.0f * normal.z * Angle - Me.z;

   reflected.Normalize();
   return reflected;
}

Refraction

The refracted ray direction is a bit more complicated than the specular reflection method. The refracted method must change indexes between two mediums. This must be simulated in order to simulate refractions on many objects such as diamonds, glass, crystals, and water. The refracted ray direction is given as Ir.

Angle  = - Find the Angle Between (Point Normal, Ray Direction)
Medium1 = Index of refraction of the original medium (ex. Air)
Medium2 = Index of refraction of the new medium (ex. Water)
Ratio = Medium1 / Medium2
Delta = SquareRoot (1  Ratio_ * (1 - Angle_))
Ir = (Ratio * Ray Direction) + (Ratio * Angle   Delta) * Point Normal-

You can now upgrade the cVector3 class and add a new vector method that simulates vector refractions. The method will pass the surface normal, Medium1, Medium2, and a pointer to the reflected vector as parameters. The function will also return true or false based on the success of the algorithm.

bool cVector3::Vector_Refraction( cVector3 Normal,
     float Medium1, float Medium2, cVector3 *pOutput )
{
   float Ratio, Angle, Delta;
   cVector3 Refracted;
   Ratio = Medium1 / Medium2;

   Angle = -( *this * Normal );
   Delta = 1.0f - Ratio * Ratio * ( 1.0f - Angle * Angle );

   if( Delta < 0.0f )
      return false;

   Refracted  =  ( *this * Ratio ) + ( Normal *  Ratio *  Angle
(float)sqrt( Delta )));

   Refracted.Normalize();
   *pOutput = efracted;
   return true;
}

NOTE

TIP

You can add other lighting effect methods to the vectorclass if you want to.


Using the Chapter's Source Code

The source code for the information covered in this chapter is contained in /Book Code/Chapter 06/Common on the CD. The files stored in this folder are the vector class, the light class, and the ray class.

The folder contains the following files:

cLight.h
cRay.cpp
cRay.h
cVector3.cpp
cVector3.h

These files are the basic layer for developing a 3D rendering environment and are very common in 3D books. These files are necessary in order to develop a structured 3D rendering layer. Please review the source code before proceeding to the next chapter.


Summary

This chapter explains the basic structures and methods of constructing a 3D environment. You still have a few more steps to go before you can actually begin rendering. These classes can be used for 3D renderers as well as 3D games. You now have an understanding about scalars, points, and vectors you know that points are locations and vectors symbolize directions. You have developed a vector class that can be used for both points and vectors. You have created a set of vector operators so that your future code will be more legible and easier to understand. You have developed a point light fashioned from a light bulb that emits energy equally in all directions. The point light also has a color attribute that can give a nice color bleeding effect on lit objects in the scene. You've developed the ray structure that can be used to cast rays into the scene and return relevant information. You can reflect and refract light by using the reflection and refraction methods. These classes are the foundation of your future development, so it's important to understand them well in order to move to the next level. Try to think of this process as getting prepared for development.


Chapter 7. Developing Your Application

This chapter continues with the development of the 3D environment. As I said in the previous chapter, you still need to take a few more steps before you can begin rendering. This chapter will convey more information pertaining to setting up a 3D rendering environment. You will design the materials, primitives, objects, and the scene in this chapter. The scene will be the final data structure that encapsulates all lights, objects, and methods. You are going to design a data buffer to store the rendered information into a medium of storage for later output to a file or memory. Let's begin constructing the required classes. The code for this chapter will be located in path /Book Code/Chapter 07/Common.


Defining Your Materials

Light brings the visual awareness to our brains by which we perceive how light interacts with different kinds of surfaces. To define the different types of natural lighting effects, you must use numbers in the computer to differentiate each type of material. The size, weight, and appearance of an object are the base values that our brains use to interpret our reality. You must create values that can track each phenomenon for an object's color, opacity, specu-lar reflection, diffuse reflection, and the index of refraction. These are just a few factors you have to take into account as you develop your 3D environment. Most of the numbers will be defined in a unit range that will have a maximum value of 1.0 and a minimum value of 0.0. The following code defines a structure that includes these attributes:

class cMaterial {
  public:

       // for global lighting
        float fAmbientFactor;
        color3 Color;

        // for diffuse related effects
        float fDiffuseFactor;
        float fOpacity;

        // for specular related effects
     float fShininess;
     float fSpecularFactor;
     float fSpecularExponent;

     // for refracted effects
      float fIndex_Of_Refraction;

     // texture filename
     char Texture[16];
      cMaterial()
      {
          fAmbientFactor              = 0.1f;
          Color                       = color3::White;
          fDiffuseFactor              = 0.5f;
          fOpacity                    = 1.0f;
          fShininess                  = 0.5f;
          fSpecularFactor             = 0.0f;
          fSpecularExponent           = 26.5f;
          fIndex_Of_Refraction        = 0.0f;
          memset( Texture, 0, 16);
      };
};

The following sections briefly discuss each member variable, so you'll have a better understanding of the Material's class design.

Ambient Factor

The ambient factor applies to ray tracing. It's primarily used to give the scene a predetermined baseline value of lighting in order to shade dark areas. If this value is 0.10 or 10% it means that 90% of the lighting will be calculated in the scene. This factor produces better realism in shadows.

Color

The color of an object is very important to defining a beautiful scene; you have 16.7 million colors to choose from. Colors on surfaces just give an overall appearance but the real magic is shading each point on the surface with a variation of the overall color. This is what creates the illusion of lighting.

Diffuse Factor

The diffuse factor indicates how much of the light coming from an arbitrary light source is diffusively reflected. This is the light that scatters on a surface as well as reflects arbitrarily in random directions. For example, a diffuse factor of 0.9 states that 90% of the light observed from the surface comes directly from the light. The brightness and scattering of the light on the surface determines the diffuse factor value. This value ranges from 0.0 to 1.0.

Opacity

The opacity of an object, or how solid it appears, is very important. For example, if an object is 0.5 opaque, it will look half transparent and half solid. The maximum opacity value is 1.0, which means that the object is perfectly solid. A smaller value can be used for glass, water, and diamonds.

Shininess

The shininess of an object is the smoothness of the object. The smoother the object is, the less imperfection in the material, which causes the light to reflect off the surface. Metals such as chrome, mercury, gold, and aluminum are all examples of shiny surfaces. This value is in a range of 0.0 to 1.0.

Specular Factor

The specular factor defines the light that reflects off of the same angle of incidence as it hits the object. Specular reflections mostly reflect light directly off of the surface in a particular direction instead of many directions as in diffuse reflection. This value is in a range of 0.0 to 1.0. Mirrors typically have a specular reflection of 1.0.

Specular Exponent

The specular exponent generates a specific highlight on a smooth surface. This is a specific spot on the surface that directly reflects the intensity of the light source. For example, an apple has a bright shiny highlight on the surface when placed next to a lamp. The specular exponent on a surface contributes to this type of highlight.

Index of Refraction

The index of refraction depicts the bending of the light as the light changes from medium to medium. The following table illustrates this fact.

Material | Refraction Index
Air (STP) | 1.00029
Water | 1.33
Ice | 1.31
Glass: |
Light flint | 1.58
Heavy flint | 1.65
Heaviest flint | 1.89
Diamond | 2.42

Texture

The texture parameter is a character array of the texture image file you would like to use when rendering the surface. Adding texture support to an application increases realism tremendously. Although we have a texture parameter it won't be used in this book.


Using Primitives for Simulation

The world we live in is very complex. Our environment is filled with all types of geometric objects. Some objects can be easily defined, like tables, basket-balls, rooms, houses, desks, beds, and so on. These objects fall into a common set of objects that are called primitives in computer graphics. Primitives can be used to simulate our real-world environment by using them singularly or by combining them to define complex geometry. For example, a sphere can be used to define a baseball, the planet, or a marble. A scene is composed of many types of objects. The collection of geometric objects in the scene is called the model and is composed of numerous types of primitives.

Primitives come in many forms and can make the modeling time less painful for an artist. The following sections briefly cover some of the common types of primitives in most 3D applications.

Plane

The most basic form of a primitive is the plane. A plane extends infinitely in four directions by definition but can be used to represent the boundary of objects such as the ground, the sky, or perhaps an infinite wall. Planes are very simple and easy to test for ray intersections. A simple example of a plane is a sheet of paper extending infinitely with no width or height.

Triangle

The triangle is composed of three points, but can be used to construct very complex geometry. Testing ray interactions on triangles can be a bit tricky but, if done correctly, triangles can define any type of complex geometry. Because of their simple definition, triangles can be used to easily do texture mapping. A set of triangles grouped together is called a mesh. A simple example of a triangle mesh is a pyramid.

Polygon

A polygon is more common in most 3D applications because you also can create complex geometry with fewer vertices as apposed to triangles. They are not limited to three points and can have namount of points (often called n-gons for this reason). Polygons can be grouped together to create meshes. You can have convex polygons that define simple geometry or concave polygons that define irregular geometry. A simple example of a polygon mesh is a cube.

Box

A box or a cube is basically six polygons grouped together. A box can be a wall, tin, or file cabinet in the scene. Boxes can also be used to optimize rendering with an axis-aligned box surrounding objects. This is also known as a bounding volume. This can speed up rendering by testing bounding boxes against one another. This is a big advantage over testing primitives singularly because grouping a set of primitives in a volume and testing a volume with another volume speed up rendering.

Sphere

The sphere is the simplest primitive that can be tested for ray intersections. Basically almost all ray-tracing applications have this primitive. Scaling a sphere by different values along the different X, Y, and Z axes can cause the sphere to look ellipsoidal. The sphere can either look squashed or stretched. A simple example of a sphere is a basketball.

Cylinder

A cylinder is composed of a long circular shaft with two discs at the top and bottom. Intersecting a ray with a cylinder is similar to a sphere intersection, but the cylinder's length must be taken into account. A simple example of a cylinder is a soda can.

Cone

Cones are similar to cylinders. Cones are in almost all ray tracers because intersecting a ray with a cone is simple. A simple example of a cone is, yes you guessed itan ice cream cone.

Disk

A disk is not common in many rendering applications because intersecting rays with disks can be a pain in the neck when writing an intersection function. Disks are required to implement cones and cylinders without top and bottom faces. A typical example of a disk is a CD-ROM.

This book uses spheres and triangles only, because these are the two basic primitives that the application supports. This is because spheres are easy to set up and test for ray intersections. Triangles can also be used to read in very complex geometry, which is enough to build a very powerful application. You can extend the primitives any way you want to include the rest. Let's begin constructing the two most basic primitives for your 3D rendering environment. The two primitives are very different in definition but have one similarity in their internal intersection functions. The intersection functions will return a point that computes the exact hit point of an incoming ray. You can use C++'s polymorphism capability to aid you with the intersection methods. After you have designed these two primitives they will then be inherited into a final encapsulated layer called object. The material class will also be inherited into the object class that you will define later. This will encapsulate the primitive classes and the material class into one object, which allows you to break each element in your scene into smaller components. This means the elements can be used singularly or as a group, thus taking an object-oriented approach to making code reusable and easier to understand.

NOTE

NOTE

The 3D hardware boards (transformation and lighting hardware abstraction layer) available today can only understand the triangle primitive. They use squashed triangles to create lines,as well as triangle meshes to create other complex primitives.


Incorporating the Sphere Primitive

The sphere has been around for a very long time now. The two components that illustrate the definition of the sphere are the radius and center point. The center point can be seen as the position of the sphere in 3D space. The radius defines the size of the sphere along the X, Y, and Z axes. The reason why the sphere has been extensively used throughout the past two decades is because it's very easy to define, is easy to test for intersections, and is fast to render. You'll define a class that includes two attributescenter point and radiusdiscussed next. You should define the class and all its functionality in the file cObject.h.

NOTE

TIP

The radius of a sphere can also be defined in X,Y,and Z terms,whereby you can squash or stretch the sphere along the X,Y,and Z axes.

Designing the Class

The design of the class will include a position vector and a radius scalar. The class will also include an internal Find_Hitpoint(cRay * pRay) method that passes a ray in as a referenced parameter and computes the destination point of the ray. The ray structure will return with the destination point as the exact hit point on the sphere.

The cSphere class consists of two variables, two constructors, and one member method. The two variables consist of a cVector3 structure for the center point and a scalar for the radius. The constructors are used to set up the member variables as well as two member methods. One member function is used to set up the sphere and the other determines the intersection point for an incoming ray.

class cSphere {
public:
          // Sphere  Definition
          cVector3          vPosition;
          float             fRadius;
 cSphere ()
 {
          vPosition              = cVector3::vZero;
          fRadius                = 0.0f;
 };
    cSphere (float x,float y, float z, float radius, bool left_handed =
true)
    {
             if( left_handed == true )
             {
                vPosition.Assign(-x,-y,z);
             }
             else
             {
                vPosition.Assign(x,y,z);
             }
             fRadius = radius;
    };
     void Setup_Sphere (float x,float y, float z,
                 float radius, bool left_handed = true)
     {
             if( left_handed == true )
             {
               vPosition.Assign(-x,-y,z);
             }
             else
             {
                vPosition.Assign(x,y,z);
             }
             fRadius = radius;
     };
     // find hit point
     bool Find_Hitpoint(cRay * pRay);
};

Intersection

To intersect a ray with a sphere, you determine whether the line intersects the sphere and then actually find the intersection points. Now keep in mind that when intersecting a line with a 3D primitive, such as a sphere, the line may hit the sphere on two sidesone to enter and the other to exit the sphere. Before you go any further, you need to answer two questions:

  • Does the ray intersect the sphere?

  • At exactly what point(s) does the ray intersect the sphere?

The reason you need to answer the first question is that most rays that leave the image plane don't intersect the sphere if it's small enough. Therefore, finding the actual intersection for every incoming ray is a waste of processing cycles. Before you can actually find the intersection point on a sphere, you must first determine whether the ray even intersects the sphere. From there, you can find the actual intersection points. Finding the actual point of intersection involves solving a quadratic equation, as follows.

AT ^ 2 + BT + C = 0

Before using the quadratic equation, you must determine whether an intersection has occurred. To find this information, you simply determine whether the distance from the sphere's center to the ray is less than the radius of the sphere. See Figure 7.1 for the process of finding this information.

Figure 7.1. The distance from the ray to the sphere must be less than the radius.

So now if the procedures of testing a ray intersection with the sphere are true then you must find the actual intersection points. You do this by plugging in the parametric quadratic equation of the line into the sphere equation. See Figure 7.2.

Figure 7.2. The equation for calculating whether the line segment has intersected a point on the sphere.

The sphere equation is as follows:

(a  q) _ = r _
a = sphere's center point
q = point on sphere

The final solution, when using the two equations together, contains values of t.

Here is the code:

bool cSphere::Find_Hitpoint(cRay * pRay)
{
    // declare variables
      float A, B, C;

      float Delta,
          Distance_To_SideA,
          Distance_To_SideB,
          SquareRoot;

      cVector3 From_Origin_To_Sphere;
      // compute distance from ray origin to sphere's position
      From_Origin_To_Sphere = pRay->origin - this->vPosition;

      // find angular difference between the ray direction
      // and the line from the ray origin to the  sphere
      B = ( pRay->direction * From_Origin_To_Sphere );

      // B * 2
      B *= 2.0f;

      // find squared distance between these two vectors
      C = ( From_Origin_To_Sphere * From_Origin_To_Sphere )
        - (this->fRadius * this->fRadius) ;

      A = 1.0f;

      // the formula ( b * b - 4 * a * c )
      Delta = ( B * B ) - 4.0f * A * C;

      if( Delta < 0.0f )
         return 0;

      if( Delta >= 0.0f )
      {
          SquareRoot = (float)sqrt( Delta );

          // find distance to one or two intersections
          Distance_To_SideA = ( -B - SquareRoot ) * ( 0.5f * A );
          Distance_To_SideB = ( -B + SquareRoot ) * ( 0.5f * A );
     // check which distance if it hits one or two points
     if( (Distance_To_SideA > 1.0f ) || (Distance_To_SideB > 1.0f ))
     {
        if( Distance_To_SideA > 1.0f )
           // set ray distance
           pRay->distance =  Distance_To_SideA;

        if( ( Distance_To_SideB < Distance_To_SideA )
        && ( Distance_To_SideB > 1.0f ))

           // set ray distance
           pRay->distance = Distance_To_SideB;
        // compute hit destination
        pRay->Find_Destination();
        return true;

     }
   }
   return false;
}

The function passes a pointer to a ray structure. The intersection code is executed using the quadratic equation for the line-to-sphere intersection, as previously discussed. After the distance to the intersected side of the sphere is found, the ray's destination/hit point is computed by calling Find_Destination().


Creating a Triangle Primitive

Intersecting a ray with a triangle can be a trivial, but slow task. The basic idea of ray-triangle intersections is that most people would intersect the ray with the tangent plane a triangle is sitting in and then do some type of test to determine whether a point lies within the surrounding edges of a triangle. The algorithm would then go to work and return success if it intersects the triangle within the given A, B, and C vertices. The incoming ray is going to hit the triangle at some point within its boundaries. The class and all its functionality are defined in the file cObject.h.

Designing the Primitive

Let's define a class that includes the three points for a triangle. The class will also include the plane definition and center point. The plane definition is composed of the plane distance and the plane unit normal. The center point vector is going to be used to find the center of the triangle. Instead of calling the vertices A, B, and C, you'll define them into an array of Vertex [0], Vertex [1], and Vertex [2]. Upon initialization and setup, the plane and the center point are automatically computed. The class will also include an internal Find_Hitpoint(cRay * pRay) method that passes a ray in as a referenced parameter and computes the destination point of the ray. The ray structure will return with the destination point as the exact hit point on the triangle.

The cTriangle class consists of an array of three vertices. It will contain a unit normal for the plane as well as the plane distance. It also contains a center point variable that determines the center of the triangle between vertices 1, 2, and 3. Two constructors are utilized as well as a few member methods. The constructors set up the member variables of the triangle. One member function is used to set up the triangle, one to calculate the center point of the triangle, and the other to find the intersection point of an incoming ray.

class cTriangle {
public:
   // Triangle Definition

  cVector3 Vertex[3];
  cVector3 vNormal, vCenterPoint;
  float fPlane_Distance;
   cTriangle ()
   {
           vNormal         = cVector3::vZero;
           vCenterPoint    = cVector3::vZero;
           fPlane_Distance = 0;
   };

   cTriangle(cVector3 a, cVector3 b, cVector3 c, bool left_handed = true)
   {
           Vertex [0] = a;
           Vertex [1] = b;
           Vertex [2]       = c;

           if ( left_handed == true)
           {
                     // Note: triangle defined in left handed system
                      Vertex [0].x = -Vertex [0].x;
                      Vertex [0].y = -Vertex [0].y;
                      Vertex [1].x = -Vertex [1].x;
                      Vertex [1].y = -Vertex [1].y;
                      Vertex [2].x = -Vertex [2].x;
                      Vertex 2].y    Vertex [2].y;
      }
         // compute plane
        vNormal         = (c-b) ^ (a-b);
        vNormal.Normalize();
        fPlane_Distance   = vNormal * a;
       // compute center point
       Compute_CenterPoint();
  };
  void Setup_Triangle(cVector3 a, cVector3 b, cVector3 c,
                                     bool left_handed = true)
  {
            Vertex [0]           = a;
            Vertex [1]           = b;
            Vertex [2]           = c;
            if ( left_handed == true)
            {
                  //Note: tri angle defined in left handed system
       Vertex [0].x       = -Vertex [0].x;
       Vertex [0].y       = -Vertex [0].y;
                   Vertex [1].x       = -Vertex [1].x;
                   Vertex [1].y       = -Vertex [1].y ;
                   Vertex [2].x       = -Vertex [2].x ;
                   Vertex [2].y       = -Vertex [2].y;
           }
          // compute plane
           vNormal = ( c-b) ^ (a-b);
           vNormal.Normalize();
           fPlane_Distance = vNormal * a;
          // compute center point
           Compute_Cen terPoint();
  };
       // find hit point
       bool    ind_H itpoint(cRay * pRay);
       void    Compute_CenterPoint();
protected:
       bool    TomasANDBen_Intersect(cRay * pRay,
           cVector3 *a, cVector3 *b, cVector3 *c );
};

Computing the Center Point

This procedure cycles through the three points of each triangle and finds the average of each component. This procedure computes a point that's in the middle of the three points of the triangle, as follows:

void cTriangle::Compute_CenterPoint()
{
   vCenterPoint = cVector3::vZero;
   for (int i = 0; i <3; i++)
   {
      vCenterPoint.x += Vertex[i].x;
vCenterPoint.y += Vertex[i].y;
vCenterPoint.z += Vertex[i].z;
   }

      vCenterPoint /= 3;
}

The method simply sums the vertices of the triangle up into one variable. It then divides each X, Y, and Z component by three.

Intersection

There are numerous algorithms out there that do triangle intersections. The first thing you need to do is determine whether the ray intersects the plane and then find the distance and destination point of the ray to the tangent plane of the triangle. This is conducted with Find_Hitpoint(cRay * pRay). The function calculates the triangle's plane and then finds the actual hit point by calling Find_Destination(). However, we're not done because we still need to find out if the intersection point sits within the boundaries of the triangle. This is found by calling TomasANDBen_Intersect(cRay * pRay, cVector3 A, cVector3 B, cVector3 C), which is discussed next.

bool cTriangle::Find_Hitpoint(cRay *  pRay)
{
   float  A, B, Distance;
   bool   Return =  lse;

      if( ( A = pRay->direction * vNormal ) != 0.0f )
{
         B = - ( vNormal * pRay->origin ) + this->fPlane_Distance ;

         Distance = B / A;
if( Distance > 1.0f  )
       {
          // we found the distance from the ray origin to the plane
          pRay->distance = Distance;

         // calculate destination point
          pRay->Find_Destination();
         // check to see if it lies within the A, B, and C
         // vertices of the unit triangle
            bReturn = TomasANDBen_Intersect
            ( pRay,
             &Vertex[0],
             &Vertex[1],
             &Vertex[2] );
             // (yes or no) that's it
             return bReturn;
         }
      }
    // if it fails no by default
   return false;
}

Using the Tomas and Ben Triangle Intersection

Finding an algorithm to determine if the ray's destination point sits within the three points of the triangle is our objective. I am going to use an algorithm developed by Tomas Moller and Ben Trumbore (Fast, Minimum Storage Ray/Triangle Intersection) that uses a minimum storage ray-triangle intersection function. The algorithm was designed to be very fast and use minimal storage that only utilizes vertices that are needed for the triangle with no preprocessing necessary. The algorithm translates the ray's origin and then changes the vector to a new vector using this equation:

(t u v) ^ t

Where t is the distance to the plane and the local coordinates of the triangle sitting flat on the plane are u and v. These two coordinates are also known as texture coordinates. Coordinates u and v represent the local coordinate system inside the triangle. The coordinate system is 2D because the triangle is sitting flat on the plane.

The u and v are used for texture mapping, normal interpolation, and color interpolation.

NOTE

NOTE

Tomas Moller and Ben Trumbore's Web site (Fast, Minimum Storage Ray/Triangle Intersection) is located at http://www.acm.org/jgt/papers/MollerTrumbore97/ .

The process for finding the intersection is as follows:

  1. Use the definition of the ray with a special formula to determine whether the previously calculated intersection point on the plane sits within the triangle's boundaries.

    Ray Formula:

    A ray has an origina distance scalar and a normalized direction vec-tor. The function is defined as R (t) = O + tD. O is the origin, t is the delta distance, and D is the unit direction vector.

    To determine whether the intersection point sits within the triangle, you need to use the following formula as the input to the ray function R(t).

    Triangle Intersection Formula:

    t (u, v) = (1  u  v) V0 + uV1 + vV2

    Plug in the terms and you'll get R (t (u, v)), meaning a point in the triangle can be found. The function t(u,v) is equal to R(t) = t(u,v).

  2. The input parameters of the function (u and v) are given in Barycentric coordinates. This means that u and v must be equal to or greater than 0 (u >= 0, v >= 0) and u + v must be equal to or less than one (u <= 1, v <= 1).

If you plug in 0,0,0 as the ray's origin meaning the ray begins at the world origin (0,0,0), then you can simplify the terms as: O + tD = (1 u v ) V0 + uV1 + vV2

Rearranging the previous terms depicts:

[   t   ]
[ -D, V1  V0, V2  V0 ]  [ u ]  = O  V0
[   v   ]

These equations basically translate a triangle to the world origin (0, 0, 0) and scale the full-sized triangle down to a unit triangle. Remember, you can always scale these values back up once you find the correct result. The triangle is then transformed to the Y and Z axes with the incoming ray direction aligned to the X axis. See Figure 7.3 for an illustration. If the normalized ray is within the unit triangle edges, the function returns true.

Figure 7.3. Intersecting a ray with a triangle is done by placing the triangle on the Y and Z axes and intersecting it with a ray aligned with the X axis.

Here is the code:

bool cTriangle:: TomasANDBen_Intersect(cRay * pRay, cVector3 *a,
                  cVector3 *b, cVector3 *c )
{
   cVector3 edge1, edge2, tvec, pvec, qvec;
   double det,inv_det;
   double t, u, v;

   edge1 = *b - *a;
   edge2 = *c - *a;

   pvec = ( pRay->direction ^ edge2 );
   det = ( edge1 * pvec );

   if (det > -0.000001f && det < 0.000001f)
     return false;
   inv_det = 1.0 / det;

   // calculate distance from vert0 to ray origin
   tvec = pRay->origin - *a;

   // calculate U parameter and test bounds
   u = ( tvec * pvec ) * inv_det;
  if (u < 0.0 || u > 1.0)
     return false;

   // prepare to test V parameter
   qvec = ( tvec ^ edge1 );

   // calculate V parameter and test bounds
   v = ( pRay->direction * qvec) * inv_det;
   if (v < 0.0 || u + v > 1.0)
     return false;

   // calculate t, ray intersects triangle
   t = ( edge2 * qvec) * inv_det;

   return true;
}


Creating and Using the Object Class

You'll use the object class most frequently in your 3D environment. It is the final encapsulated layer, defining a single entity that inherits all the member

methods and member variables of the Material, Sphere, and Triangle classes.

Designing the Object Class

The object class will have all the attributes of the inherited classes and will consist of a few methods and variables of its own. Because the object class inherits the two primitives, you need a default primitive member variable that will be used to determine which primitive is primary. You should define the classes and constants in the file cObject.h.

#define OBJECT_NOTHING   0
#define OBJECT_SPHERE    1
#define OBJECT_TRIANGLE  2

The Object structure consists of an inheritance of the Material, Sphere, and Triangle classes. It has one variable for selecting the default primitive and includes one constructor that initializes the default primitive variable and calls the inherited constructors. Four internal methods are defined: The first method finds the intersection point of the default primitive, the second returns the normal of the default primitive, the third computes the center point of the default primitive, and the fourth calculates the radius of the default primitive. Here is the code:

class cObject : public cMaterial, public cSphere, public cTriangle {
public:
   int        Default_Primitive;

  cObject()
  {
      // Set to nothing
      Default_Primitive        = OBJECT_NOTHING;
  };

  // NOTE: Z is Positive other angles are correct
  cObject(int Type)
  {
     switch(Type)
    {

     // Primitive is a Sphere
     case OBJECT_SPHERE:
      Default_Primitive        = OBJECT_SPHERE;
     break;

     // Primitive is a Triangle
      case OBJECT_TRIANGLE:
      Default_Primitive        = OBJECT_TRIANGLE;
     break;

    }
       // call inherited constructors
         cMaterial();
         cSphere();
         cTriangle();
  };

  bool      Find_Hitpoint(cRay *pRay);
  cVector3  Normal(cVector3 hit_point);
  cVector3  ObjectCenter();
  float     ObjectRadius(cVector3 );

};

NOTE

TIP

The cObject class can also inherit other primitives you define on your own.

Object Center

This procedure returns the center point of the default primitive. If the default primitive is a sphere, the procedure will return the center point of the sphere. If it is a triangle, the procedure will return with the computed tri-angle's center point that was originally set up during the initialization. The object's center is selected by the default primitive variable.

cVector3 cObject::ObjectCenter()
{

   if (Default_Primitive == OBJECT_SPHERE)
      return vPosition;
   if (Default_Primitive == OBJECT_TRIANGLE)
      return vCenterPoint;

   return cVector3::vZero;
}

The previous code simply tests the default primitive variable with the primitive constants to determine whether to return the center point of the selected primitive. The selected primitive can be Sphere or Triangle.

Object Radius

This procedure returns the radius of the selected primitive. If the default primitive is a sphere, the sphere's radius is returned. If the default primitive is a triangle, the radius of the triangle is found by subtracting each vertex by the input tCenter point. The magnitude of the farthest point on the triangle becomes the radius of the triangle.

float cObject::ObjectRadius(cVector3 tCenter)
{
   float dMax, dThis;
   int i;
   cVector3 Dummy;

   if (Default_Primitive == OBJECT_SPHERE)
      return fRadius;
   dMax = 0.0;

   if (Default_Primitive == OBJECT_TRIANGLE)
   {
      for (i = 0; i < 3; i++)
      {
         Dummy = Vertex[i] - tCenter;
         dThis = Dummy.Mag();
         if (dThis > dMax)
            dMax = dThis;
      }
   }
   return dMax;
}

Finding the Hit Point

The Find_Hitpoint procedure will call the right method from the inherited class based on the default primitive value. If the primitive is a sphere, the ray will return with the destination point on the sphere. If the default primitive is a triangle, it will return with the destination point on the triangle.

bool cObject::Find_Hitpoint(cRay * pRay)
{
 switch(Default_Primitive)
 {
      case OBJECT_SPHERE:
      {
         return cSphere ::Find_Hitpoint(pRay);
      } break;

      case OBJECT_TRIANGLE:
      {
         return cTriangle ::Find_Hitpoint(pRay);
      } break;
   } // end switch
   return false;
 }

Determining the Normal

To find the normal from some arbitrary point in 3D space, you must again branch your code so that it will return the correct information based on the default primitive state. If the default primitive is a sphere, it will calculate the normal by subtracting the hit point from the sphere's center point (or position) and divide by the sphere radius. If the default primitive is a triangle, it will return the unit plane normal vector originally calculated in the triangle initialization and setup class.

cVector3 cObject::Normal(cVector3  hit_point)
{
  switch(Default_Primitive)
  {
   case OBJECT_SPHERE:
     return cVector3 ( ( hit_point - this->vPosition ) / fRadius ) ;
   break;

   case OBJECT_TRIANGLE:
    return vNormal;
   break;
   }
  return cVector3::vZero;
}


Designing the Scene

The scene is the final layer that all of your developed classes and logic will utilize. The logic that determines how the rendering application will perform must be defined in the scene. The scene will load the model, assign the materials, set up the lights, and render the 3D environment. You are going to design the basic scene object and then append more data and functionality to it later during the course of this book. The design of the scene class includes lights, objects, and the image buffer. Ray tracing and photon mapping are rendering algorithms that will be applied to the scene class. For now, let's define the basic attributes of the scene. These attributes encapsulate what you have learned up to this point.

The scene class will have n lights and objects. You declare two storage arrays and counter variables. This is because your scene will have n amount of light and n amount of objects so you need to dynamically load in the data on the fly. The scene will have two methods that add lights and objects to the internal object and light list.

The allocated memory heap will be set up for each array. Every time the array is at the limit for the light array, it will re-allocate five more data blocks for each interval. It will also re-allocate 50 objects at each memory upgrade for the object list and have an internal image data buffer to hold the image that you'll render to later, using one of your selected rendering algorithms. You are going to define a few member methods to manipulate the image data buffer for color operations. The scene will have a few methods to load, unload, and set up the scene. You'll declare the render width and render height for the color buffer so that all of the future renders will be rendered to an 800x600 bitmap. The class and data are located in the file cScene.h.

// 50 objects at a time to re-allocate
#define MEMORY_OBJECTS_CHUNK   50
// Five lights at a time to re-allocate
#define MEMORY_LIGHT_CHUNK      5

#define RENDER_WIDTH          800
#define RENDER_HEIGHT          600

The scene class will contain a few methods for the different types of procedures necessary to loading a 3D environment. Two variables are defined to count the number of lights and objects loaded. Two arrays are used to dynamically allocate lights and objects. The scene constructor and destructor are used to allocate and unallocate memory. Init_Scene() and Kill_Scene() are used to load objects at run time and unload them when we're done. Two methods are defined one to add an object to the object list and another to add a light to the light list. An image buffer is defined to hold the rendered image; this is used after we implement a rendering algorithm. Two color methods are defined to plot a selected color in the image buffer as well as fill the whole image buffer with a selected color. Here is the code:

class cScene
{
public:

   cScene();
   ~cScene();

   // scene load and destroy
   void   Init_Scene();
   void   Kill_Scene();

   // light  methods
   void   AddToObjectList(cObject ** pList, long *lCount, cObject tObject);
   void   AddToLightList(cLight ** pList, int *nCount, cLight tObject);

   // color methods
   void   PlotPixel(int x, int y, color3 color);
   void   FillColor(color3 color);

public:

   cObject * pObjectList;
   long      nObjectCount;
   cLight    * pLightList;
   int      nLightCount;

   color3    ColorBuffer[ RENDER_WIDTH * RENDER_HEIGHT ];
};

The Scene Constructor

The scene constructor resets all pointers to NULL and resets the counting variables to 0 to ensure you don't run into any problems later.

cScene::cScene()
{
    // objects counts
    pObjectList       = NULL;
    nObjectCount        = 0;
    pLightList       = NULL;
    nLightCount       = 0;

}

The Scene Destructor

The scene destructor releases any allocated memory and resets all pointers to NULL to ensure proper cleanup when you're done.

cScene::~cScene()
{
   if(pObjectList) delete [] pObjectList;
   pObjectList = NULL;

   if(pLightList)  delete [] pLightList;
   pLightList  = NULL;
}

Init Scene

This method will be used later to load and set up any code and data into the scene.

void cScene::Init_Scene()
{
   // TODO: code goes here
}

Kill Scene

This method will be used later to perform any shutdown logic and release any allocated data in the scene.

void cScene::Kill_Scene()
{
   // TODO: code goes here
}

Add an Object

This method allocates or re-allocates data when the object array is at its peak. It then assigns a passed parameter into the internal object list.

void cScene::AddToObjectList(cObject ** pList, long *lCount, cObject
tObject)
{
  int cnt = 0;
  if ((*lCount % MEMORY_OBJECTS_CHUNK) == 0)
  {
     cnt = ((*lCount/MEMORY_OBJECTS_CHUNK)+1) * MEMORY_OBJECTS_CHUNK;
     *pList = (cObject *) realloc(*pList,sizeof(cObject) * cnt);
  }

  (*pList)[(*lCount)++] = tObject;
}

Add a Light

This method allocates or re-allocates data when the light array is at its peak. It then assigns a passed parameter into the internal light list.

void cScene::AddToLightList(cLight ** pList, int *nCount, cLight tLight)
{
   int cnt = 0;
   if ((*nCount % MEMORY_LIGHT_CHUNK) == 0)
   {
       cnt = ((*nCount/MEMORY_LIGHT_CHUNK)+1) * MEMORY_LIGHT_CHUNK;
       *pList = (cLight*) realloc(*pList,sizeof(cLight) * cnt);
   }

   (*pList)[(*nCount)++] = tLight;
}

Plot a Pixel

The plot pixel method changes the value of a pixel (using its X and Y coordinates) in the internal color buffer.

void cScene::PlotPixel(int x, int y, color3 color)
{
  if(x < 0 || x > RENDER_WIDTH || y < 0 || y > RENDER_HEIGHT) return;
  ColorBuffer[y * RENDER_WIDTH + x] = color;
}

Fill Buffer with Color

This procedure cycles through the color buffer and assigns a selected color:

void cScene::FillColor(color3 color)
{
  // fill with color
  for(int x=0; x<RENDER_WIDTH; x++)
    for(int y =0; y<RENDER_HEIGHT; y++)
      PlotPixel(x,y, color);

}


Using the Chapter's Source Code

The source code for the information covered in this chapter is contained in /Book Code/Chapter 07/Common on the CD. The files stored in this folder are the cObject and cScene classes. The folder contains the following files: cObject.h, cObject.cpp, cScene.h, and cScene.cpp.

These files are the basic layer for developing a 3D rendering environment. Be sure to review the source code before proceeding to the next chapter. Keep in mind that you will upgrade the cScene class throughout the book. You still have some distance to go before you can begin rendering.


Summary

This chapter covered some of the important components to setting up a 3D environment, including materials and scalars to suit your needs. The material class is used to define the physical attributes and lighting factors for an object. This chapter covered the different types of primitives common in most 3D applications and selected two that you will be working with. The primitive classes (Sphere and Triangle) are used to define the model and the object class combines everything into one entity. You can now define primitives with materials and find the intersection with each primitive. The scene class encapsulates everything to control the rendering environment.

The scene can dynamically allocate lights and objects on the fly. The scene class will also hold the rendered information in an internal image buffer for file or video output. Now all you have to do is write an input method that will load in the geometry of the model, create a camera, apply a rendering algorithm, and begin rendering. This information will be the last piece of the puzzle, so let's continue with the next chapter.


Chapter 8. The Rest of the Puzzle

This chapter explains the rest of the puzzle associated with setting up a 3D rendering environment. Subjects such as how to read in the scene from file and how to set up the camera are discussed. The camera is going to be used to render the 3D world to the image plane. You will also design and code each file system object so you can import the scene from file without any hassles. This way, you can increase speed and efficiency when rendering synthetic images. The only task left to do is to incorporate a rendering algorithm. This will be covered in the following chapters. You are going to incorporate ray tracing and photon mapping. Enough talk; let's begin!


Understanding the Coordinate Systems

Many years ago, mathematicians only visualized objects in 3D space by drawing objects on a graph system with a pencil. But today, due to technological innovations, such people have computers that bring the old concepts of mathematics alive. Computers add a new dimension to doing math. People are more comfortable with visual learning, which might be a good thing for children learning math in future years.

Because we live in a 3D world, learning about 3D shouldn't be a problem for our minds to comprehend. There are two major types of representations that programmers use to define their 3D world. A simple sheet of printing paper defines 2D spacethe width of the paper is the X axis and the height is the Y axis. The trick to defining 3D space is adding the third dimension (the Z axis) that completes the definition. If you take a pencil and hold it perpendicular to the paper, the pencil becomes the Z axis. The Z axis can be defined in two ways. A typical 2D coordinate system is defined in the follow-ing format: left (x) to right (+x), and top (+y) to bottom (-y). When adding the Z axis, the positive and negative ends of the axis can point in or out of the paper. There's really no right or wrong way to define the Z axis.

People drive on the left side in England, yet drive on the right side in the United States. Again it's a preferred choice by the programmer who designs the application.

NOTE

TIP

In most computer systems,the rendering libraries and the graphics memory are arranged such that the Y axis is inverted.This may be confusing at first,but you get used to it after a while.Basically,just think of the computer screen as Quadrant I of the Cartesian coordinate system with the Y axis inverted and (0,0) in the upper-left corner of the screen.

The important distinction when defining the left-oriented system from the right-oriented system is how the points are ordered in defining an object in 3D space. The normal of an object would be flipped based on the direction of the Z axis. This book uses the left-oriented system because it's very common in most modern applications. However, the primitives (Material and Sphere) in the scene can be defined in both systems. The positive end of the Z axis is going into the screen and the negative end is coming out. Take a look at Figure 8.1 and be sure to remember the distinction.

Figure 8.1. In the left-handed system,the Z axis points into the screen.In the right-handed system, the Z axis points out of the screen.

Local Space/Model Space

Over the many years of research and development, three common spaces have been derived for 3D rendering applications. These spaces were designed to speed up the rendering pipeline of an application for efficiency and optimization. This is because in many applications data is constantly changing and objects need to be structured in a complex format to allocate, delete, and preserve data.

Because some applications such as 3D games change data in real-time, the original definition or the vertices of an object must be preserved in order to transform and preview an object. This requires a space to save the original definition of an object in order to make copies and position them in the scene. Hence, the local coordinate system was derived. The original definition of the object must be in a centered manner whereby the vertices are in unit length. The unit vertices are then scaled and transformed when the object is actually used. The system typically reads in the data from file and loads it into structured memory for later use. The vertices of an object are typically centered at the local origin of (0,0,0). This local origin can be anywhere in space and doesn't need to be located at the world origin of (0,0,0). Remember the local origin is relative so try to think of it as the center of mass (versus the center of volume). See Figure 8.2 for an example of local space. After the object is read in, it is moved into another space for manipulation.

Figure 8.2. Local space is defined where the vertices of an object are situated around the origin of the system (0,0,0).

NOTE

TIP

Many games programming companies develop their characters in model space.Sometimes the characters are animated in model space as well as assigned textures.

World Space

As the object is in local space it must then be transformed and positioned within the 3D world. This transformation creates a new object that is a translated and rotated version of the original. The object's vertices have been changed to be at some arbitrary position and rotation. A unit cube can be scaled up by 50 times its original size and positioned somewhere in the world. This new space is called world space. World space includes a set of objects originally defined in model space, which are then are transformed, rotated, and translated.

A modeling application typically takes a set of objects that are in local space and transforms them to world space. By rotating and moving objects around the in world space, the geometry of the scene is determined. A composition of stairs, chairs, ceilings, and picture frames on the walls are all applied to an environment. Artists don't only define the geometry of the scene, they also add the lights, apply the materials, map the textures, align the textures, and position the camera to perceive the scene. The next stage is to perceive the world using the eyes of the observer or the camera.

Camera Space

The camera only sees a portion of the scene at a time. The camera moves around the scene and records visible data to the image plane based on its orientation and position. The camera doesn't follow any alignment rules and can change dynamically if needed. The camera is normally rotated at some degree relative to the world axis alignment and also positioned at some location from the world origin (0,0,0). This local camera movement is called the camera space. See Figure 8.3 for an example of camera space. The camera is a special entity because its local coordinate system must be specifically designed to perceive the scene. Because the world is drawn from the camera you need to render objects relative to the camera so that everything moves and rotates opposite to the camera. If the camera moves forward the world moves backwards; if the camera turns 90 degrees left, the world will adjust in rotation so the scene would be rendered properly.

Figure 8.3. Camera space is based upon the viewer navigating through the scene and then recording the closest viewable objects that are in the field of view.

In many modern applications, you can get away with using only two spaces for rendering. This is because the model can be read in with the objects already defined in world space. For example, Lightwave 3D comes in mod-eler and renderer forms. An artist develops the mesh using the modeler and then renders the scene with all other assets in the renderer. The applications in this book use two coordinate systems because you're creating a post-process rendering application and not a real-time game. I will not reconstruct or change the mesh during the course of rendering. Games typically use all three spaces to manipulate objects in real-time.

NOTE

TIP

The best way to understand camera space is to pretend you are walking around a room with a camcorder. Whatever shows up in the preview is recorded.This is the camera space.


Implementing the Camera into the Scene

Implementing the camera in the scene is our primary goal. You can implement the camera into your application using a few methods. The approach used here is common in most modern applications. This method is composed of a position point and three rotational vectors. The position vector is used to tell where the camera is located in the scene, and the rotational vec-tors are used to rotate the camera relative to the world. For example, the world's X axis never changes, and neither do the Y and Z axis. This means that the camera rotates relative to each static X, Y, and Z world axes. These rotational vectors are going to be used to generate the local X, Y, and Z axes of the camera. See Figure 8.4.

Figure 8.4. The camera's local coordinate system is composed of three orthogonal vectors.These vec-tors represent the local X,Y,and Z axes of the camera relative to the world's coordinate system.

One problem when setting up a camera in the scene is to consider that the camera is constantly changing in orientation and position. This creates a problem when rendering an image because the camera's setup must be perfect so that it records the world in the correct form. The rendered image on the image plane cannot be distorted or irregularly rendered due to an incorrect definition of the three local axes of the camera. The local X, Y, and Z axes must always be orthogonal (perfectly aligned at 90 degrees apart) with each other at all times even after the camera has changed in position and orientation. The local X axis must be perpendicular to the local Y axis. The local Z axis must be perpendicular to the local Y axis, and so forth. See Figure 8.5 for an example of this alignment concept.

Figure 8.5. The camera alignment of the LOOK,UP,and RIGHT vectors must always be orthogonal even after the camera has been rotated and translated.

In order to set up the camera, you need to do the following:

  1. Create a location point for the camera.

  2. Set up the camera so that it will be rotated properly.

  3. Render the world using the pre-computed camera.

Why Use Three Vectors?

Because you are looking at the world from the camera, the camera can be seen as the originthe point from where everything is seeneven though it's an offset in world space. All you're doing is setting up the camera so that it will imitate a real coordinate system. Again, the camera's coordinate system is always changing relative to the world axis alignment and must be kept perpendicular even after being changed. To remedy this, each camera axis must be regenerated each frame for a game or each render for a rendering application, based on the new orientation and position of the camera. Calculating the position for the camera is not a problem, but calculating the rotation can be a bit difficult. Rendering the world to the camera's position can be a pain in the neck if not done correctly.

You are going to define your own camera and apply it to the cScene class created in the last chapter. The goal is to generate the local X, Y, and Z axes for the camera's coordinate system. This requires three offset vectors that are used to track the degrees in rotation for the X, Y, and Z axes relative to the world. Let's begin by defining and naming these rotational vectors.

The three vectors are as follows:

  • LOOK vector

  • UP vector

  • RIGHT vector

The LOOK Vector

The LOOK vector describes which way the camera is facing. For example, picture an observer standing at the world origin (0,0,0) and facing the screen. Consider a picture somewhere into the screenmaybe 30 units in. The observer or camera is focused on the picture and is standing at the world origin.

It is important to point out that this example uses the left-oriented system, so the positive Z axis is pointing into the screen. This basically means that the observer is facing the picture and the LOOK vector is the perfect candidate to represent this notion. In mathematical terms, this means that the LOOK vector is (0,0,1). This is enough information to render the world properly because you can tell where the camera is facing and positioned in the scene. If the observer turned 180 degrees in the opposite direction, the LOOK vector would now be (0, 0, -1). This is still okay because you can still tell where the observer is standing and facing. Imagine now that the observer is facing the picture once again. But now imagine the observer wanted to look at the picture from upside down instead of straight up, similar to how Spiderman hangs from the ceiling.

The observer is still looking at the picture in the same direction because the LOOK vector is (0,0,1), but the observer has changed his orientation to upside down. Another vector is required in order to track the new change in rotation. This vector is called the UP vector because it's vertical as opposed to forward (relative from the observer).

The UP Vector

The UP vector is like a long pole going straight up from the observer. The LOOK vector can be seen as a long pole going forward from the observer. The UP vector defines the rotation of the observer from upright to upside down. The UP vector is many times aligned with the world Y axis of (0,1,0) but may not be if desired. Again, this is a local coordinate system relative to the world's coordinate system and can change if required. These delta vectors just track the change in offset relative to the world alignment.

The UP vector can be rotated in such a way that it's aligned with the world X axis. For example, assume that the observer turned sideways (90 degrees) and is now lying on the floor still looking at the picture. The LOOKvector is still (0,0,1) but the UP vector (1,0,0) is now aligned with the world's X axis. This can become tricky so let's take a look at Figure 8.6 for an illustration. It does-n't take much to figure out what the RIGHTvector is, so let's cover that next.

Figure 8.6. Sideways view of the observer that depicts the LOOK vector facing the world Z axis,and the UP vector parallel to the world X axis.

The RIGHT Vector

You now know that the UP vector points straight up from the observer or straight down. You also know that the LOOK vector dictates the direction of the camera, either backwards or forwards. This basically means that the LOOK vec-tor is the local Z axis and the UP vector is the local Y axis. The local X axis must be the RIGHT vector (1,0,0), which completes the total definition of the camera coordinate system. Figure 8.7 shows an example of the RIGHT vector. The LOOK and UP vectors' values are normally entered in the world definition file of the scene, but the RIGHT vector's value can be automatically generated.

Figure 8.7. The RIGHT vector describes the wind-ing rotation of the observer turning sideways (left to right).

This is due to the fact that, if you have two vectors, you can generate a perpendicular vector from the previous two (LOOK and UP) vectors by using the cross product method. Once you have the correct value for the RIGHT vector, you can generate the three local X, Y, and Z axes for the camera. All three components must be orthogonal and should be computed before render time. A simple way to remember how to calculate the three axes for the camera using these three vectors is to remember the phrase "Look Right Up Into The Sky." This works great because it lists the three steps needed to calculate the camera coordinate system.

Here is the list of newly created vectors:

  • The LOOK vector (0,0,1); manually entered

  • The UP vector (0,1,0); manually entered

  • The RIGHT vector (1,0,0); can be calculated using the cross product

Here is how you generate the three local X, Y, and Z axes for the camera:

  • Find the distance between the camera position and the LOOK vector to find the Z axis vector.

  • Normalize the Z axis vector so that it has unit length equal to 1.0.

  • By using the cross product method, cross the Z axis vector with the UP vector to find the X axis vector.

  • Normalize the result of the X axis vector so that it's 1 unit in length.

  • Cross the Z axis vector with the X axis vector to find the Y axis vector.

  • Normalize the result so the Y axis vector is 1 unit in length.

Let's take a look at the code to see how to set up the camera.

Adding the Camera Member Data

The following code shows you how to add the camera to your project. You need to declare the following new data members to the cScene class:

// New Camera Code
cVector3     CameraPosition,
       Look,
       Up;

Determining the Viewing Direction

You also need another vector that determines the direction of the bright shiny highlights that will occur on objects when rendering. In order to calculate something called specular highlights, discussed in detail in the next chap-ter, you need to take this vector into account. Specular highlights occur when a very bright source of light is reflected off of a smooth surface. For example, you can sometimes be blinded by the bright reflection from the sun if the angle is relative to an object, say the window of a car, in front of you. To simulate this effect in computers, you need to take the viewer or camera direction into account. The specular highlight appears when the light goes directly into your eyes. Let's add a component for this in the cScene class. See Figure 8.8 for an example of the viewing direction vector.

Figure 8.8. The viewing direction is used to produce the bright shiny spot in a specific direction on a surface.

//which way is the camera facing
cVector3    Viewing_Direction;

Tracking the Viewing Distance

You also need a scalar that tracks the distance between the view plane and the camera position point. This value is called the viewing distance. If you recall, the view plane is the plane that you project the final image on, which is then transformed and rendered into screen space. The viewing distance determines the field of view for the camera; large viewing distances result in narrow fields of view, whereas small viewing distances result in wide-angle fields of view. See Figure 8.9.

Figure 8.9. The viewing distance is the distance between the camera position and the image plane.This distance creates the overall perspective of how the scene should be rendered on the image plane.

Let's declare this value in the cScene class. The viewing distance, along with the camera coordinate system and the image plane, create the overall perspective of how the scene should appear. This is typically calculated every frame as the camera navigates through the world. The larger this value is, the smaller the overall view of the scene that is rendered on the image plane. See Figure 8.10 for an illustration.

Figure 8.10. A distance comparison between two images rendered with a viewing distance of 256 in the top image and 512 in the bottom image.

// how much you perceive in the scene
float        Viewing_Distance;

Setting Up the Camera Coordinates

You are going to write a camera method that calculates the orthogonal X, Y, and Z axes for the camera. This function will utilize the camera position, as well as the LOOK and UP vectors listed in the scene definition file. You don't need to use the RIGHT vector because it can be automatically generated. The three passed pointers (X_Axis, Y_Axis, and Z_Axis) will be updated to be perpendicular with each other after the method has commenced:

void cScene::Setup_Camera_Coords( cVector3 * X_Axis,
                      cVector3 * Y_Axis, cVector3 * Z_Axis )
{
   // Look

   *Z_Axis = this->CameraPosition - this->Look;
   Z_Axis->Normalize();
   // Right

   *X_Axis = this->Up ^ *Z_Axis ;
   X_Axis->Normalize();
   // Up
   *Y_Axis = *Z_Axis ^ *X_Axis ;
   Y_Axis->Normalize();
}

The local X, Y, and Z axes of the camera are created. To rotate the system, you simply rotate two vectors at a time for a specific angle on a desired axis. Now if you wanted to rotate 90 degrees around the Z axis (the LOOK vector), you would rotate the UP (Y axis) and RIGHT (X axis) vectors 90 degrees around the LOOK vector. This is done to preserve the perpendicular relationship between vectors in order to keep the camera coordinate system correct. When spawning rays for ray tracing, you use the camera's position vector and these three rotational vectors to guide each ray in the correct direction in 3D space.

NOTE

TIP

You can write your own scene exporter from a famous 3D package such as Lightwave 3D or 3D Studio Max for this custom format by looking at the member elements of the cScene class.


Importing the Scene from a File

Importing the scene from a file is a very important aspect to a 3D rendering application. If you were to compile an application with custom geometry embedded in the code for every render, the result would be a tedious and daunting task. It would take forever to get reasonable results and many times you would lose a perfectly defined scene. I've had bad experiences trying to be cheap and not use a definition file in my applications. Sometimes I would render a beautiful image with all the objects set up in the perfect alignment with the camera and then I lose the magnificently defined scene by changing something accidentally. Importing the scene from a file is a great idea, and all 3D rendering applications include this feature. The first thing you want to do is read in the camera from file, as well as the rendering settings, and then read in the lights, spheres, and triangles. The file will be in ASCII format. The next section explains this concept using a sample file.

A Sample File

This sample file includes the camera, lighting parameters, a light, a sphere, and a triangle class.

Camera { <0,0,-100>, <0,0,1>, <0,1,0>, <1,0,0>, 512  }

Render_Settings {

Diffuse_Shading       1
Specular_Highlights  1
Diffuse_Reflection   0
Specular_Reflection  0
Refraction            0
}

Light { <200,0,-1900>, (1,1,1), 1.0 }
Light { <20,0,770>, (0,1,0), 1.0 }

Sphere {

Material {
fAmbientFactor         0.1
fSpecularFactor        0.2
fSpecularExponent      30.7
fOpacity                1.0
fIndex_Of_Refraction   0.1
fDiffuseFactor         0.8
fShininess             0.5
Color                 (1,1,1)
Texture               texture.ppm
}

<200,0,200>, 200
}

Triangle {
Material {
fAmbientFactor         0.1
fSpecularFactor        0.5
fSpecularExponent      30.7
fOpacity                1.0
fIndex_Of_Refraction   0.1
fDiffuseFactor          0.5
fShininess               0.5
Color                     (1,1,1)
Texture                   texture.ppm
}
<-100,300,0>, <100,150,0>, <-30,-200,0>
}

NOTE

NOTE

The rendering settings parameter must include at least one light-ing effect set to true in order to see anything after the scene is rendered.

Linking the File Library

As you can see, the file format is very simple and everything is self-explanatory. Because you are going to work with a file system object, you need to include the file-handling library. You need to include the following library header:

#include <fstream.h>

Creating a String-Comparison Method

Because each class has special class names that will dictate to the parser what type of handling is needed, you must create a string-comparison method.

// String comparison method
bool COMPARE(char * in, char * out)
{
   if(strcmp(in,out) == 0)
      return true;
   else
      return false;
}

Defining the Buffer Length

As you will see, you are going to constantly create storage arrays to hold text that's read in from the scene definition file. It is smart to declare a fixed-length constant that will help make the code easier to read and change later. Each string array will be 80 characters in length. Here is the definition.

#define BUFFER_LENGTH         80

Defining the Class Format

All of the classes will be defined in the following format. The class begins with the class name followed by an opening brace. Next are the data elements or parameters listed in comma-delimited order. After all the elements are declared, the class is ended by a closing brace. You can also have something called a complex class, which has a subclass. A subclass follows the same format as the parent class except it's listed as a data member of the parent class; try to think of it as a clustered class. Subclasses are used for special classes that have many data elements. Subclasses make code more readable and understandable.

// simple class
ClassName { data, data, data, … }

or

// complex class
ClassName {
Sub ClassName {
data, data ,data, …
}
data, data, data, … }


Declaring Attribute Primitives

The data in each class can come in many forms. For example, element 1 in the list can be a number, element 2 can be a color attribute, and element 3 can be a vector. To read in the attributes of a class, you must declare a list of attribute read methods. Here is the list of methods you need:

  • Read vector reads in a vector in the following format: <0,0,0>.

  • Read scalar reads a number in as a floating point number (1.34334).

  • Read color reads in a color in the following format: (red, green, blue).

  • Read comma flags a new data element is listed (,).

Here are the function prototypes and code:

// Attribute primitives
cVector3    Read_Vector(ifstream& is);
float       Read_Scalar(ifstream& is);
color3      Read_Color (ifstream& is);
void        Read_Comma (ifstream& is);

Setting Up the Read Vector Method

The read vector method reads in a vector in the following format <0,0,0>. It uses the stream object to break up and extract the numbers into the cVector3 class.

cVector3 cScene::Read_Vector(ifstream& is)
{
   char dummy;
   cVector3 r;

   // <0,0,0>
   is >> dummy;
   is >> r.x;
   is >> dummy;
   is >> r.y;
   is >> dummy;
   is >> r.z;
   is >> dummy;
   return r;
}

Setting Up the Read Scalar Method

The read scalar method reads in a scalar in the following format 23, 88.3, or 100. It uses the streamobject to load an ASCII number into a floatvariable.

float     cScene::Read_Scalar(ifstream& is)
{
   float  r;
   // 256
   is >> r;
   return r;
}

Setting Up the Read Color Method

The read color method reads in a color in the following format (244, 45, 1). It uses the stream object to break up and extract the ASCII numbers into the color3 class.

color3   cScene::Read_Color (ifstream& is)
{
   char dummy;
   color3 r;

   // (255,0,0)
   is >> dummy;

   is >> r.r;
   is >> dummy;
   is >> r.g;
   is >> dummy;
   is >> r.b;
   is >> dummy;
   return r;
}

Setting Up the Read Comma Method

The read comma method tells the stream object to keep pumping in data until it passes a slash (/) or to ignore all characters in the stream buffer that include a comma. This method is used to skip statements containing brackets and parentheses when parsing the scene definition file for class elements.

void  cScene::Read_Comma (ifstream& is)
{
   char p;

   // read {
   is >>   p;

   // if the letter { wasn't found find it
   while(p !=   ',')
      is >> p;
}


Setting Up the Scene Classes

The scene classes are critical to the scene definition file. You need to write a few methods to read in the camera, lights, materials, spheres, and triangles.

The data in the files can be listed in random order and you can have n amount of lights, spheres, and triangles. When this data is read in, it is stored in the light and object arrays. You are going to declare three variables. One variable will be used to determine whether data has been loaded into the scene from a file called isDataReady. The other two variables track the number of triangles and spheres that are read into the scene. These variables are declared in the cScene class.

Here is the declaration:

bool     isDataReady;
long     triangle_count,
         sphere_count;


Determining the Scene Primitives

Let's take a look at the function prototypes that are used to read in each type of class listed in the scene definition file.

// scene primitives
bool        Read_Camera(ifstream& is);
bool        Read_Light(ifstream& is);
cMaterial   Read_Material(ifstream& is);
bool        Read_Sphere(ifstream& is);
bool        Read_Triangle(ifstream& is);

NOTE

TIP

You can extend the scene primitives anyway you want by including other primitives such as cones,cubes,planes,and polygons.

The Read Camera Method

This method reads in a camera class from the scene definition file. The scene can only have one camera. If multiple entries are listed for the camera, the system will set up only the last camera class for the camera. The camera class contains five parameters, as follows:

  • The first parameter is the camera position (vector)

  • The second parameter is the LOOK vector (vector)

  • The third parameter is the UP vector (vector)

  • The fourth parameter is the viewing direction (vector)

  • The fifth parameter is the viewing distance (scalar)

Here's the code:

bool   cScene::Read_Camera(ifstream& is)
{
   // local buffer
   char buffer[BUFFER_LENGTH];

   //===============================================================
   // Statement : Camera { <0,0,0>, <0,0,0>, <0,0,1>, <0,0,1>, 256 }
   //===============================================================

   // read {
   is >> buffer;

   // if the letter { wasn't found find  it
   while(!COMPARE(buffer, "{"))
         is >> buffer;

   // set camera position
   CameraPosition = Read_Vector( is );
   CameraPosition = cVector3( -CameraPosition.x,
         -CameraPosition.y,
          CameraPosition.z ) ;

   // read separating comma with white space
   Read_Comma ( is );

   // set LOOK Vector
   Look =  Read_Vector( is );

   // read separating comma with white space
   Read_Comma ( is );
   // set UP Vector
   Up = Read_Vector( is );

   // read separating comma with white space
   Read_Comma ( is );
   // read in Viewing Direction
   Viewing_Direction = Read_Vector( is );
   Viewing_Direction = cVector3( -Viewing_Direction.x,
        -Viewing_Direction.y,
         Viewing_Direction.z ) ;

   // read separating comma with white space
   Read_Comma ( is );

   // Read in Viewing Distance
   Viewing_Distance  =    Read_Scalar( is );

   // We're done; ignore the rest of the statement
   return true;
}

The Read Light Method

The read light method reads a diffuse point light into the scene. The scene definition file can have n number of lights listed in random order. As the parser finds each light class, the light is stored in the light list of the class. The light state is toggled so it can be used during rendering. The light class contains three parameters, as follows:

  • The first parameter is the light position (vector)

  • The second parameter is the color of the light (color)

  • The third parameter is the wattage or brightness (scalar)

NOTE

TIP

You can extend the lights in the scene by including other lights such as spotlights,directional lights,and area lights.

Here's the code:

bool   cScene::Read_Light(ifstream& is)
{
   // local buffer
   char buffer[BUFFER_LENGTH];
   cLight Light;
   cVector3 pos;
   float    power;
   color3   color;
   //=======================================================
   // Statement : Light { <0,50,0>, (255,0,0), 1 }
   //=======================================================

   // read {
   is >> buffer;

   // if the letter { wasn't found find it
   while(!COMPARE(buffer, "{"))
         is >> buffer;

   // set light position
   pos = Read_Vector( is );

   // read separating comma with white space
   Read_Comma ( is );

   // set light color
   color = Read_Color( is );

   // read separating comma with white space
   Read_Comma ( is );

   // set light wattage / power
   power = Read_Scalar( is );

   // set up light
   Light.Setup( pos.x, pos.y, pos.z, power, color );

   // set light active
   Light.bActive = true;

   // add to light list in scene
   AddToLightList(&pLightList, &nLightCount, Light);

   // We're done; ignore the rest of the statement
   return true;
}

The Read Material Method

This method reads in a material subclass. Materials are normally attached to primitives such as spheres, polygons, planes, and so on. The material class is not an independent class; it depends on a parent class. The material class is included as a subclass in each sphere and triangle class. The material class reads in the different lighting coefficients for specific objects and, depending on the coefficient, simulates a specific kind of lighting effect. For example, if the material is specular, it will reflect light in the opposite direction. The material class contains nine parameters, as follows:

  • The first parameter is the ambient factor (scalar)

  • The second parameter is the specular factor (scalar)

  • The third parameter is the specular exponent that contributes to the highlight (scalar)

  • The fourth parameter is the opacity of the object (scalar)

  • The fifth parameter is the index of refraction (scalar)

  • The sixth parameter is the diffuse factor (scalar)

  • The seventh parameter is the shininess of the object (scalar)

  • The eighth parameter is the color of the object (color)

  • The ninth parameter is the texture of the object (string)

NOTE

TIP

Although the materials and lighting models have many more mathematical coefficients,I use only a subset in this book.This does not necessarily mean you should only use these few.You can add other factors for other lighting effects you choose to implement with this code.

Here's the code:

cMaterial cScene::Read_Material(ifstream& is)
{
   cMaterial r;

   // local buffer
   char  buffer[BUFFER_LENGTH];
   cObject cObject( OBJECT_SPHERE) ;

   //=======================================================
   /*
      Material {
      fAmbientFactor         0.1
      fSpecularFactor        0.9
      fSpecularExponent      30.7
   fOpacity                1.0
   fIndex_Of_Refraction   0.1
   fDiffuseFactor         0.8
   fShininess             0.5
   Color              (1,1,1)
   Texture                  texture.ppm
   }
*/
//=======================================================

// read {
is >> buffer;

// if the letter { wasn't found find it
while(!COMPARE(buffer, "{"))
      is >> buffer;

// Ambient Factor
is >> buffer;
if(COMPARE(buffer, "fAmbientFactor"))
{
   // set value
   r.fAmbientFactor = Read_Scalar( is );
}

// Specular  Factor
is >> buffer;
if(COMPARE(buffer, "fSpecularFactor"))
{
   // set value
   r.fSpecularFactor = Read_Scalar( is );
}

// Specular Highlight
is >> buffer;
if(COMPARE(buffer, "fSpecularExponent"))
{
   // set value
   r.fSpecularExponent =  Read_Scalar( is );
}
// Opacity
is >> buffer;
if(COMPARE(buffer, "fOpacity"))
{
   // set value
   r.fOpacity = Read_Scalar( is );
}

   // Index of efraction
   is >> buffer;
   if(COMPARE(buffer, "fIndex_Of_Refraction"))
   {
      // set value
      r.fIndex_Of_Refraction = ead_Scalar( is  ;
   }

   // DiffuseFactor
   is >> buffer;
   if(COMPARE(buffer, "fDiffuseFactor"))
   {
      // set value
      r.fDiffuseFactor = Read_Scalar( is );
   }

   // Smoothness f  urface
   is >> buffer;
   if(COMPARE(buffer, "fShininess"))
   {
      // set value
      r.fShininess = Read_Scalar( is );
   }

   // Read Color
   is >> buffer;
   if(COMPARE(buffer, "Color"))
   {
      // set value
      r.Color = Read_Color( is );
   }

   // Read Texture
   is >> buffer;
   if(COMPARE(buffer, "Texture"))
   {
      // set value
      is >> r.Texture;
}
   // read } for material
   is >> buffer;

   // We're done
   return r;

}

The Read Sphere Method

Read sphere reads a sphere class into the object array. The sphere class is a complex class because it includes a subclass. This subclass is the material class covered previously. You can have n amount of spheres listed in random order. The sphere class is read into the object list of the scene. A very important aspect to this read method is that the sphere class can be defined in both left-handed and right-handed systems. The sphere class contains one subclass and two parameters, as follows:

  • The material class is listed first (class)

  • The first parameter is the position of the sphere in space (vector)

  • The second parameter is the radius of the sphere (scalar)

  • The third parameter is a Boolean that determines which system (left or right) the sphere is defined in (scalar)

Here's the code:

bool  cScene::Read_Sphere(ifstream& is)
{
   // local buffer
   char   buffer[BUFFER_LENGTH];
   cMaterial m;
   cObject tObject( OBJECT_SPHERE) ;
   cVector3 pos;
   float    radius;
   int      handed_system;
   //=======================================================
   /*
      Sphere {
      Material {
      fAmbientFactor         0.1
      fSpecularFactor        0.9
      fSpecularExponent      30.7
      fOpacity                1.0
      fIndex_Of_Refraction   0.1
      fDiffuseFactor          0.8
     fShininess         0.5
     Color             (1,1,1)
     Texture               texture.ppm
     }
     <0,0,0>, 1, 1
     }
  */
  //=======================================================
  // read {
  is >> buffer;

  // if the letter { wasn't found find it
  while(!COMPARE(buffer, "{"))
        is >> buffer;

  // read data into material structure
  m = Read_Material( is ) ;

  // copy data over to object structure
  tObject.fAmbientFactor        = m.fAmbientFactor;;
  tObject.fDiffuseFactor        = m.fDiffuseFactor;
  tObject.fSpecularFactor       = m.fSpecularFactor;
  tObject.fSpecularExponent     = m.fSpecularExponent;
  tObject.fOpacity           = m.fOpacity;
  tObject.fIndex_Of_Refraction  = m.fIndex_Of_Refraction;
  tObject.fShininess            = m.fShininess;
  tObject.Color               = m.Color;
  strcpy(tObject.Texture, m.Texture);

  // set sphere position
  pos = Read_Vector( is );

  // read separating comma with white space
  Read_Comma ( is );

  // set radius
  radius = Read_Scalar( is );

  // read the handed system object was defined
  Read_Comma ( is );

  // left (1) or ight  0)  anded?
  handed_system = (int) Read_Scalar( is );
  // set up object for handed system
  tObject.Setup_Sphere( pos.x, pos.y, pos.z, radius,
               (handed_system == 1 ? true: false));

  // increment sphere count
  sphere_count++;

  // add to light list in scene
  AddToObjectList(&pObjectList, &nObjectCount, tObject);

  // We're done, Ignore the rest of the statement
  return  true;
}

The Read Triangle Method

Read triangle reads a triangle class into the object array. The triangle class is a complex class because it includes a subclass. The subclass is the material. You can have n amount of triangles listed in random order. The triangle class is read into the object list of the scene. A very important aspect to this read method is that the triangle class can be defined in both left-handed and right-handed systems. The triangle class contains one subclass and three parameters, as follows:

  • The material class is listed first (class)

  • The first parameter is vertex one or side A of the triangle (vector)

  • The second parameter is vertex two or side B (vector)

  • The third parameter is vertex three or side C (vector)

  • The fourth parameter is a Boolean that determines which system (left or right) the triangle is defined in (scalar)

Here's the code:

bool   cScene::Read_Triangle(ifstream& is)
{
   // local buffer
   char buffer[BUFFER_LENGTH];
   cMaterial m;
   cObject tObject( OBJECT_TRIANGLE) ;
   cVector3 A,B,C;
   int handed_system;
   //=======================================================
   /*
      Triangle {
      Material {
   fAmbientFactor         0.1
   fSpecularFactor        0.9
   fSpecularExponent      30.7
   fOpacity                1.0
   fIndex_Of_Refraction   0.1
   fDiffuseFactor          0.8
   fShininess              0.5
   Color                    (1,1,1)
   Texture                    texture.ppm
  }
    <-100,300,0>, <100,150,0>, <-30,-200,0>, 1
  }
*/
//=======================================================
// read {
is >> buffer;

// if the letter { wasn't found find it
while(!COMPARE(buffer, "{"))
      is >> buffer;

// read data into material structure
m = Read_Material( is ) ;

// copy data over to object structure
tObject.fAmbientFactor         = m.fAmbientFactor;;
tObject.fDiffuseFactor         = m.fDiffuseFactor;
tObject.fSpecularFactor        = m.fSpecularFactor;
tObject.fSpecularExponent        = m.fSpecularExponent;
tObject.fOpacity           = m.fOpacity;
tObject.fIndex_Of_Refraction   = m.fIndex_Of_Refraction;
tObject.fShininess            = m.fShininess;
tObject.Color                = m.Color;
strcpy(tObject.Texture, m.Texture);

// read Vertex A
A = Read_Vector( is );

// read separating comma with white space
Read_Comma ( is );

// read Vertex B
B = Read_Vector( is );
   // read separating comma with white space
   Read_Comma ( is );

   // read Vertex C
    C = Read_Vector( is );

   //   read the anded system  bject was  efined
   Read_Comma ( is );

   // left (1) or right (0) handed?
   handed_system = (int) Read_Scalar( is );

   // set up object for handed system
   tObject.Setup_Triangle( A, B, C, (handed_system == 1 ? true: false));

   // increment triangle count
   triangle_count++;

   // add to light list in scene
   AddToObjectList(&pObjectList, &nObjectCount, tObject);

   // We're done, Ignore the rest of the statement
   return true;
}

Lighting Parameters

A clever method for developing a 3D rendering program is to be selective about which types of lighting effects you choose to render. For example, there are numerous effects that can contribute to a rendered image. We are going to add a special variable that will be used to set up the scene to render certain lighting effects. The implementation of the lighting effects is really done with the rendering algorithm, but you should still allow the application to choose whether to use it or not. The selection of parameters is read in from file. Here is the list of lighting effects to add:

  • Diffuse interaction or diffuse shading

  • Specular highlights

  • Diffuse reflection

  • Specular (mirror) reflection

  • Refraction

NOTE

NOTE

Other natural lighting effects can be easily added to this list.

Here is the class definition, the read method, and the object that will be defined in the scene class:

   // render settings
   class        Render_Settings
   {
     public:
          Render_Settings()
  {
                use_diffuse_shading     = false;
     use_diffuse_reflection  = false;
                 use_specular_reflection = false;
                 use_refraction           = false;
                 use_specular_hightlights= false;
  }
      public:
                 bool   use_diffuse_shading,
                        use_diffuse_reflection,
                        use_specular_reflection,
                        use_refraction,
                        use_specular_hightlights;
};

// Read lighting parameters from file
    bool        Read_Render_Settings(ifstream& is);

// The render settings object
Render_Settings RenderSetting;

Establishing the Read Render Settings

The body of the read method will take the elements from the Render_Settings class. If the value of the selected effect is the number 1, the Boolean is set to success for the effect in the RenderSetting object. This variable will be used during rendering to branch code, based on the enabled effects as you develop ray tracing and photon mapping.

bool  cScene::Read_Render_Settings(ifstream& is)
{
    // local buffer
    char   buffer[BUFFER_LENGTH];
    cScene::Render_Settings r;
    int     Dummy;
   //=======================================================
   /*
      Render_Settings {
         Diffuse_Shading      1
         Specular_Highlights          0
         Diffuse_Reflection          0
         Specular_Reflection          0
         Refraction             0
         }
   */
   //=======================================================
   // read {
   is >> buffer;

   // if the letter { wasn't found find it
   while(!COMPARE(buffer, "{"))
         is >> buffer;

   // Diffuse Lighting on Surface
   is >> buffer;
   if(COMPARE(buffer, "Diffuse_Shading"))
   {
      // set value
      is >> Dummy;
      (Dummy == 1)
              ? r.use_diffuse_shading = true
              : r.use_diffuse_shading = false;
   }

   // Specular Hightlights
   is >> buffer;
   if(COMPARE(buffer, "Specular_Highlights"))
   {
      // set value
      is >> Dummy;
     (Dummy == 1)
             ? r.use_specular_hightlights = true
             : r.use_specular_hightlights = false;
   }

   // Diffuse Reflection to Other Surfaces
   is >> buffer;
   if(COMPARE(buffer, "Diffuse_Reflection"))
   {
       // set value
       is >> Dummy;
       (Dummy == 1)
                  ? r.use_diffuse_reflection = true
                  : r.use_diffuse_reflection = false;
   }

   // Specular Reflection to Mirror Surfaces
   is >> buffer;
   if(COMPARE(buffer, "Specular_Reflection"))
   {
      // set value
      is >> Dummy;
      (Dummy == 1)
                 ? r.use_specular_reflection = true
                 : r.use_specular_reflection = false;
   }

   // Refraction
   is >> buffer;
   if(COMPARE(buffer, "Refraction"))
   {
      // set value
      is >> Dummy;
      (Dummy == 1)
              ? r.use_refraction = true
              : r.use_refraction = false;
   }
   // read } for render lighting class
   is >> buffer;
   // save data to member variable
   RenderSetting = r;
   // We're done, Ignore the rest of the statement
   return true;
}


Reading the Data into the Scene

One major function is declared that reads in all the data from an ASCII file. It parses and branches code based on the class name. The method reads in a file from the local file system and begins to parse each class. If a specific class is found, the class is read in and the logic is handled. The important four elements that the parser searches for are Camera, Lights, Spheres, and Triangles. The parser also searches for the Render_Settings class, but it isn't considered an element of the scene. This class is solely designed to set up the specific lighting effects for the scene that should be rendered by the rendering algorithm. Recall that you can have n amount of lights, spheres, and triangles, but you can only have one camera and one Render_Settings class.

NOTE

NOTE

The Render_Settings class isn't really considered a component of the scene because it deals with the ren-derer's settings for selecting specific types of lighting effects.

Here is the declaration:

bool   Read_In_Scene(char * filename);

Here is the body:

bool  cScene::Read_In_Scene(char * filename)
{

   this->isDataReady = true;
   ifstream  fin ( filename   ;
   char buffer[BUFFER_LENGTH];

   while(!fin.eof())
   {
       // read in data
       fin >> buffer;

       // branch data

       if(strcmp(buffer,"Camera")        == 0)
       {
          Read_Camera( fin );
       }

       else if(strcmp(buffer,"Render_Settings") == 0)
       {
          Read_Render_Settings( fin );
       }
     else if(strcmp(buffer,"Light")      == 0)
     {
     Read_Light( fin );
     }

     else if(strcmp(buffer,"Sphere")     == 0)
     {
        Read_Sphere( fin );
     }

     else if(strcmp(buffer,"Triangle") == 0)
     {
         Read_Triangle( fin );
      }

      else
      {
      }
   }

   fin.close();
   return true;
}


Upgrading the cScene Class

This section shows the new updated cScene class with its new functionality. The class now includes the primitive and attribute read methods for the scene. Scene assets can be read from the file, which gives you a big advantage. The camera coordinate system is also included in the class. This gives the application the capability to perceive the scene from any position and orientation. The scene class also includes a few counter variables to determine the amount of triangles and spheres loaded by the read methods. It has a variable that's used to tell whether the data has been loaded using a scene definition file. The new code is listed in bold text:

#ifndef _C_SCENE_H
#define _C_SCENE_H

// 50 objects at a time to re-allocate
#define MEMORY_OBJECTS_CHUNK   50
   // 5 lights at a time to re-allocate
   #define MEMORY_LIGHT_CHUNK        5

   #define RENDER_WIDTH         800
   #define RENDER_HEIGHT         600

   // new code
   #define BUFFER_LENGTH              80

   class cScene
   {
   public:
      cScene();
      ~cScene();

      // scene load and destroy
      void Init_Scene();
      void Kill_Scene();

      // light methods
      void AddToObjectList(cObject ** pList, long *lCount, cObject tObject);
      void AddToLightList(cLight ** pList, int *nCount, cLight tObject);

      // color methods
      void PlotPixel(int x, int y, color3 color);
      void FillColor(color3 color);

      // New Code

      void Setup_Camera_Coords( cVector3 * X_Axis,
                  cVector3 * Y_Axis, cVector3 * Z_Axis );
      bool     Read_In_Scene(char * filename);

      // scene primitivesbool
      bool        Read_Camera(ifstream& is);
      bool        Read_Light(ifstream& is);
      cMaterial   Read_Material(ifstream& is);
      bool        Read_Sphere(ifstream& is);
      bool        Read_Triangle(ifstream& is);
// attribute primitives
   cVector3   Read_Vector(ifstream& is);
   float      Read_Scalar(ifstream& is);
   color3     Read_Color (ifstream& is);
   void       Read_Comma (ifstream& is);

        // render settings
        class        Render_Settings
        {
          public:
               Render_Settings()
       {
                      use_diffuse_shading     = false;
          use_diffuse_reflection  = false;
                      use_specular_reflection   =  false;
                      use_refraction  = false;
                      use_specular_hightlights= false;
       }
           public:
                      bool   use_diffuse_shading,
                             use_diffuse_reflection,
                             use_specular_reflection,
                             use_refraction,
                             use_specular_hightlights;
   };

   // Read lighting parameters from file
       bool        Read_Render_Settings(ifstream& is);

public:

   cObject  * pObjectList;
   long       nObjectCount;
   cLight    * pLightList;
   int       nLightCount;

   color3    ColorBuffer[ RENDER_WIDTH * RENDER_HEIGHT ];

   // New Code for chapter 8
   cVector3 CameraPosition,
              Look,
           Up,
           Viewing_Direction;

   float Viewing_Distance;
   bool     isDataReady;
   long     triangle_count,
        sphere_count;

   // The render settings object
   Render_Settings RenderSetting;
};

// String comparison function
bool   COMPARE(char * in, char * out);

#endif

NOTE

NOTE

The Render_Settings class should be defined in each scene definition file.


Using the Chapter's Source Code

The source code for the information covered in this chapter is contained on the CD in /Book Code/Chapter 08/Common. The information stored in this folder is the updated cScene class.

The folder contains the following files: cScene.h and cScene.cpp.

These files are the basic layer for developing a 3D rendering environment. The updated cScene class can now read in the scene from the file with rendering parameters as well as set up the local camera coordinate system. This updated cScene class, along with the previous classes, play an important role in the following chapters. The following chapters utilize all the information you've learned up until now to implement ray tracing and photon mapping. Be sure to review the source code before proceeding to the next chapter.


Summary

This chapter completes the process of setting up a 3D rendering environment. This chapter covered the different types of spaces that are used in 3D graphics. You now know that you can use spaces for rendering. You also defined a camera coordinate system that's used to render the final image. The camera can move and rotate freely without any rendering problems. You've declared three vectors that track the offset in rotations that are used to generate local X, Y, and Z axes for the camera. You can now load in the scene using an ASCII file.

By importing the scene from a text file, you can write a converter that converts almost any major format into the custom format. This increases speed and efficiency because all your scenes can be carefully constructed and preserved as a file. This is wonderful because you have your own file format that reads in the assets of the scene. You can read in the camera and any number of lights, spheres, and triangles. You have a few variables that are used to count the amount of triangles and spheres that are loaded into the scene. With this new information, you are stepping closer and closer to implementing photon mapping. The only step left to do now is to incorporate a rendering algorithm, which is discussed in the next chapter.

If you're a bit tired of reading without any results, please bear with me. In the following chapters, you'll learn to put it all together!


Chapter 9. Ray Tracing

Ray tracing is a technique for rendering three-dimensional graphics. It can render very complex geometry because it's a point-sampling method. It is very good at rendering pictures with curved surfaces, mirrors, and transparent surfaces. Simply put, ray tracing is just very good at calculating lighting interactions on a point level.

This chapter shows you how to implement ray tracing and how to write three ray-tracing applications. Each application simulates a specific natural lighting effect as seen in nature. Ray tracing is based on the simple idea of recording and displaying information about how light interacts and bounces off surfaces as it travels through an environment. Everything that you've been learning comes down to this chapter, where you put it all together. The ray-tracing algorithm will be applied to the cScene class. In the following chapters, you'll incorporate the photon-mapping algorithm.


Using Backward Ray Tracing

You are going to implement backward ray tracing in this book. Backward ray tracing is the best choice in this case because forward and bi-directional ray tracing both take forever to return reasonable results and are a bit more complicated to set up. Almost all images today are rendered from rays being shot from the view plane. Keep this idea in mind because you're going to use this same concept to render the final image for photon mapping by tracing rays into the scene from the view plane. This method guarantees results without wasting processing cycles. But it has all the problems that backward ray tracing poses, which include multiple specular reflections and indirect illumination.

The Basic Logic Behind Backward Ray Tracing

By now you should have a good idea how ray tracing works. Backward ray tracing works by tracing individual rays from the view plane into the scene. The view plane is set up by the camera position and the field of view of the camera. As each ray is launched through the image plane, it travels through the scene by bouncing from object to object. The job of the application is to determine the color of each ray as it intersects each point on a surface. Finding the color is its primary goal. Because rays travel from object to object the same way that light does, the ray-tracing application has to use a recursive algorithm. The ray can bounce from object to object indefinitely unless you limit it. This limit is typically set at a value of eight or ten times. See Figure 9.1.

Figure 9.1. The ray-tracing algorithm calculates the light-ing for a point in 3D space by sampling rays from the eye to determine the coloring of each object projected on the image plane.

The following list outlines the basic logic involved in implementing ray tracing in a practical application. Ray tracing can be broken into three basic components:

  • Begin tracing: This part of the application sets up a ray relative to the camera and guides it through each pixel on the image plane. Here, you need to begin tracing rays from the image plane into the scene. This is done by picking a ray and shooting through one pixel of the image plane into the scene.

  • Trace ray: This component traces the ray into the scene and finds a point of intersection. It calls the shade point method, which shades each point of intersection for illumination. It also traces other rays for the different types of reflections and refractions (more on this later).

  • Shade point: This method shades the point of intersection to the different natural lighting factors. It then saves the shaded color of the point on the pixel that the ray originally left from the image plane.

Here is the algorithm, explained in a bit more detail.

  1. Begin tracing.

    • For each pixel on the image plane, pick a ray from the eye through this pixel

    • Pixel color = Trace Ray In Scene (ray)

  2. Trace the ray in the scene.

    • Find closest intersection with scene

    • Compute intersection point and normal on surface

    • Color = Shade Point( point, normal )

    • Return Color

  3. Shade the point.

    • Color = black

    • For each light source

    • Color = Color + direct illumination

    • Return Color

As you can see, the algorithm is pretty simple and shouldn't be a problem to implement. The three basic sections of the algorithm are going to be turned into computer functions and incorporated into the cScene class. You can begin developing the necessary methods to implement the algorithm.


Implementing the Shading and the Lighting

The scene is always rendered after all light has been propagated and is being reflected onto each surface. Shading is the most important element to a rendering algorithm such as ray tracing. Shading refers to the concept of light being reflected off an arbitrary surface at some particular time. In other words, shading can be called the reflected radiance (the energy leaving a surface). Recall from Chapter 2 that when light is incident on a material, some of the energy is absorbed into the material and some is transmitted through the object to another object or reflected off the surface to another object. If a material absorbs all colors and reflects green, the material appears green. Because you're using backward ray tracing, which is the incorrect way of simulating light, the algorithm records the path light takes from the eye outward.

In the real world, photons originate from light sources and scatter from object to object until they reach our eyes. This is what forward ray tracing does.

Backward ray tracing utilizes a mathematical model that approximates how much light travels from one place to another. The nature of the simulated reflection is dependent on the type of material that the ray interacts with. Rough surfaces normally produce a diffuse reflection, whereas smooth surfaces have a specular reflection. For example, smoother surfaces like a mirror or glass normally reflect light back towards the eye. All physical attributes of the material must be taken into account when rendering. The different coefficients for absorbing and transmitting/reflecting light must be simulated. The overall appearance of the scene is determined by the color of each object, the diffuse shading, or diffuse interaction across each surface in the scene.

NOTE

TIP

The diffuse shading method is very efficient when used with ray tracing.

Adding Diffuse Shading

Recall from Chapter 6 that the dot product calculates the angle between two vectors in space. This is perfect for what you need here in order to shade each point of intersection for diffuse interaction. You must find the relative angle between the point normal and the light source. See Figure 9.2.

Figure 9.2. The tangent plane of the point is relative to the primitive.If the primitive is a sphere, the tangent plane is along its curve.If the primitive is a trian-gle,the tangent plane is the same as the surface normal.

The point normal is the surface normal if the surface is flat. This value can be easily computed because the dot product is a unit length vector method. The range of the dot product method is given as 1 to 1. If the angle is 0 between the light source and the point normal, the returned value is 1.0. If the angle between the light source and the point normal is 90 degrees (or perpendicular), the returned value is 0.The intensity is full if the light source and the surface directly face each other. The intensity weakens as the surface tilts away from the light source. This concept is known as cosine shading. The cosine shading method is the dot product (or cosine) between two vectors in 3D space. See Figure 9.3.

Figure 9.3. The angle between the point normal and the light source determines the intensity value of illumination.

The values that are below 0 to 1 indicate that the surface is facing away from the light source, which means that the surface isn't receiving any energy from the light source. The values that are above 0, yet less than 1, indicate that the surface is facing the light source and should receive some of the light source's power. If the dot product value is 1.0 between these two vectors, this means the point on the surface should be at 100% full brightness; if the value is at 0, that's 0% brightness.

This is perfect for what you need because 0 to 1 or simplified 0 to 100% can represent an illumination range of brightness. By adding the object color into the equation you can shade the desired point of intersection on the object according to the intensity factor. The object color and cosine shade are not the only two coefficients needed to do diffuse shading. Materials have an internal diffuse factor of how much light it reflects across the surface. This is mostly the color of the object. You must add this coefficient too into the solution when using the cosine shading method.

Because ray tracing has indirect illumination problems (as seen in calculating the penumbra in shadows) it would be wise to add another coefficient to the application. In the real world, light is everywhere and even the darkest areas are lit to some small degree. A shadow you cast on a wall isn't pitch black because some of the light waves travel sideways and intersect with the object from different directions. This causes the shadow to appear brighter, based on the light source power. Because nothing is pitch black in the real world, you need an ambient factor that illuminates perfectly black shadows to some small degree of brightness. Remember sometimes the color black RGB(0,0,0) in the computer is returned if the ray is cast as a shadow. In essence, each object in the scene will receive a minimum amount of light. This means that if this ambient coefficient is set to 0.5 or 5%, all illumination in the scene is set to this base value by default. The remaining 95% must be evaluated by the cosine shading method.

NOTE

NOTE

The ambient factor isn't used in photon mapping because photon mapping is great at simulating shad-ows.The penumbras in shadows are easily handled with photon mapping. Simply put,photon mapping is good at indirect illumination.

Let's take a look at the steps required to shade an arbitrary point in 3D space. You first find the angle between the point-normal relative to the light source. This is done using the dot product method. If the angle of the cosine shading method is less than zero, no light is incident on the point of intersection and the point should be shaded as black (no lighting). But if the angle is above zero, you should begin shading the point using the different coefficients of the material as well as the ambient factor. After you've incorporated these different elements into one solution for the specific point of interest, you find the final solution.

Here is the process for shading a point for diffuse shading:

Shade = Find the Angle Between (Light Vector, Point Normal)
If  (Shade < 0 )
        1. Shade = 0
Diffuse = Surface.Color
Diffuse = Diffuse x Surface.AmbientFactor
Diffuse = Diffuse x Surface.DiffuseFactor
Diffuse = Diffuse x Shade

As you can see, it's pretty simple. The object's color,the surface ambient factor, the diffuse factor,and the shadeare all incorporated into the final solution.

Implementing Specular Highlights

Objects such as apples, pool balls, bowling balls, and so on are smooth, thus causing a specular reflection. Unlike diffuse reflections that scatter light in many random directions, specular reflection transmits light in only one direction. When light is incident on a smooth surface some of the light is reflected back towards the eye. Because the object is smooth, the light rays don't scatter in a random fashion as with rough surfaces; the full intensity of the light source is reflected from the surface.

A mirror is a perfect specular reflection and will reflect whatever it's facing because it has few imperfections. But most smooth objects have a higher degree of imperfections. This causes the reflected light to be bounced in a somewhat imperfect fashion. If the eye is in front of the light source, the light that's reflected back into the eye directly off the smooth surface generates a bright shiny spot on the surface. This bright shiny spot on the surface is called the specular highlight and the imperfection the surface has is a coefficient called Shininess. See Figure 9.4.

Figure 9.4. The specular highlight is the bright shiny spot reflecting the full intensity of the light source on the surface.

The location of the highlight is relative to the observer. This viewing vector was originally read in as the Viewing Direction in the cScene class. The viewing direction vector is used to simulate the brightness of the light focused on the surface. The shininess of the highlight is an exponent. A large exponent will create a small sharply focused highlight and a small exponent will generate a larger highlight. This value is calculated as the angle between the reflection of the light relative to the ray and to the light source direction. The specular highlight coefficient indicates how shiny the object is; you must add this to simulate specular highlights.

Simulating specular highlights is rather simple as compared to diffuse interaction. You need only to find the angle between the reflected ray direction and the ray to light direction. If the angle between these two vectors is above zero, the specular highlight is applied to the pixel. The highlight is the Shininess of the object times the Shade to the power of the Specular Exponent. Here is the process for shading a point for specular highlights:

Shade = Find the Angle Between (Reflected Ray Direction, Ray to Light
Direction)
If (Shade > 0 )
      Specular = pow (Shade, Surface.Shininess)* Surface.fSpecularExponent
Else
      Specular  = 0

The next step after finding the diffuse shading and specular highlights contribution to the point of intersection is to combine the two into one final solution. This allows other elements to be incorporated into the solution such as the light source's wattage and color.

Implementing Diffuse Interaction with Specular Highlights

You must now apply the specular and diffuse factors to the final pixel intensity. Finding the final pixel shade of a point on a surface for the view plane is the primary goal. Figure 9.5 shows the two shadings. The important elements to note here are the light source wattage, light source color, the color of the object, the ambient factor, the diffuse shading, and the specular highlights contribution.

Figure 9.5. A side-by-side comparison of diffuse reflection and diffuse reflection with specular highlights.

NOTE

TIP

Diffuse interaction and specular highlights are collectively referred to as local illumination lighting effects.

The process of putting it all together for the different factors is broken into the following steps:

  1. The final color is set to black (no lighting).

  2. The light source's color is summed into the final color.

  3. The light source's wattage is summed into the final color.

  4. The combination of the specular and diffuse shading is summed into the final color.

  5. The final color is set to the pixel on the image plane.

To find the final pixel intensity, use this formula:

Returned Color = Lightsource.Color *  Lightsource.Wattage
                                   *  (Specular + Diffuse)

You can now upgrade the cScene class to accommodate the new changes required for ray tracing.


Upgrading cScene for Ray Tracing

You're going to add ray tracing and photon mapping to the cScene class, so you must differentiate between the two by code. This will be conducted by assigning two constants to a member variable called Render_Algorithm. For now, this variable will be set to the ray tracing flag. You are going to then add a few new functions for the ray-tracing algorithm. These methods will implement ray tracing in full detail. The rendering settings are read in when you load the scene definition file; keep this in mind as you branch your ray-tracing code for specific lighting effects. The new code is shown in bold:

#ifndef _C_SCENE_H
#define _C_SCENE_H

// 50 objects at a time to re-allocate
#define MEMORY_OBJECTS_CHUNK   50

// Five lights at a time to re-allocate
#define MEMORY_LIGHT_CHUNK        5

#define RENDER_WIDTH           800
#define RENDER_HEIGHT          600

#define BUFFER_LENGTH           80

// New Code
#define   ZERO_TRACE_DEPTH      0
#define   MAX_TRACE_DEPTH       8
#define   RAY_TRACING           1
#define   PHOTON_MAPPING       2
class cScene
{
public:
   cScene();
   ~cScene();

// scene load and destroy
   void   Init_Scene(); // will load scene
   void   Kill_Scene();

   // light methods
   void   AddToObjectList(cObject ** pList, long *lCount, cObject tObject);
   void   AddToLightList(cLight ** pList, int *nCount, cLight tObject);

   // color methods
   void   PlotPixel(int x, int y, color3 color);
   void   FillColor(color3 color);

   // camera methods
   void   Setup_Camera_Coords( cVector3 * X_Axis,
      cVector3 * Y_Axis, cVector3 * Z_Axis );

   // read methods
   bool  Read_In_Scene(char * filename);

   // scene primitives
   bool      Read_Camera(ifstream& is);
   bool      Read_Light(ifstream& is);
   cMaterial  Read_Material(ifstream& is);
   bool      Read_Sphere(ifstream& is);
   bool      Read_Triangle(ifstream& is);

   // attribute primitives
   cVector3   Read_Vector(ifstream& is);
   float      Read_Scalar(ifstream& is);
   color3      Read_Color (ifstream& is);
   void       Read_Comma (ifstream& is);

    // render settings
    class Render_Settings
  {
       public:
             Render_Settings()
             {
                 use_diffuse_shading = false;
                 use_diffuse_reflection = false;
                 use_specular_reflection = false;
                 use_refraction = false;
                 use_specular_hightlights= false;
             }
   public:
              bool     use_diffuse_shading,
                       use_diffuse_reflection,
                       use_specular_reflection,
                       use_refraction,
                       use_specular_hightlights;
      };

   // lighting parameters from file
    bool       Read_Render_Settings(ifstream& is);

   // New code for Ray Tracing
   int      Find_Closest_Object(cRay ray, cVector3 * cHitPoint);

   void      BeginTracing();
   bool      Tray_Ray_In_Scene(cRay ray, int depth, color3 *color);
   color3     ShadePoint(cVector3 hit_point, cVector3 normal, int iObject);

public:
   cObject  * pObjectList;
   long       nObjectCount;
   cLight    * pLightList;
   int       nLightCount;

   color3    ColorBuffer[ RENDER_WIDTH * RENDER_HEIGHT ];
   cVector3  CameraPosition,
                Look,
                 Up,
     Viewing_Direction;

   float   Viewing_Distance;
   bool   isDataReady;

   long   triangle_count,
      sphere_count;

   Render_Settings RenderSetting;
   // New code for chapter 9
   int Render_Algorithm;
};

#endif

Initializing the Scene

You need to prompt the user to manually load the scene from file. The application will ask the user for the path and filename of the scene definition file. The path can be relative (MyFolder\File.txt) or absolute (X:\File.txt). After the user inputs the path and filename, the program begins to load each ele-ment of the scene definition file into the scene structures. All the lights and objects are loaded into their appropriate arrays. The rendering settings are defined and the camera is set up; the viewing direction and position are defined. The application is then notified that it will be using the ray-tracing algorithm. The information is stored in the Render_Algorithm variable. The scene is now ready for the rendering algorithm.

void cScene::Init_Scene()
{
   char buffer[256];
   cout << "Please Type in Path and Filename\n";
   cout << "Usage (ex. C:\\sample.txt)" << endl;
   cout << "Enter  Information: ";
   cin  >> buffer;

   Read_In_Scene(buffer);

   // Ray Tracing and Local Reflection
   Render_Algorithm = RAY_TRACING;

}

Finding the Closest Intersection

The first thing you need to do is develop a method that tests for ray to object intersections. This method will pass a ray structure and a vector pointer in as two parameters. It returns the index of the object that intersected with the ray. The ray is the input, and the outputs are the returned integer (index of object hit) and vector pointer (point of intersection). This method's sole purpose is to search through each object in the scene and determine whether the incoming ray has intersected a point on any one of the objects in the scene. This method also calculates which object is intersected first by the ray. It then updates the vector pointer to the new closest point of intersection and returns with the object index.

int cScene::Find_Closest_Object(cRay Ray, cVector3 * hit_point)
{
   int i, hit_object, TempHit;
   float Last_Shortest_Distance;

   Last_Shortest_Distance = 9999999.0f; // make very big
   hit_object = -1; // this is an invalid number

   // for n amount of objects we have
   for(i = 0; i < this->nObjectCount; i++)
   {
     if( pObjectList[ i ].Default_Primitive != OBJECT_NOTHING)
     {
        // Get temp hit
       TempHit = pObjectList[ i ].Find_Hitpoint( &Ray );

       // If temp hit is valid and this ray distance is less
       // than the previous distance let's save it.
       if( TempHit && ( Ray.distance < Last_Shortest_Distance ) )
       {
         // save
          Last_Shortest_Distance       = Ray.distance;
         *hit_point            = Ray.destination;
         hit_object            = i;
       }
     }
   }
   return hit_object;
}

Beginning the Tracing Process

All the rays being traced from the image plane into the scene are listed in one major function called BeginTracing(). You must trace each ray into the scene through each pixel on the view plane. See Figure 9.6.

Figure 9.6. The Begin Tracing method traces rays from the image plane into the scene.

This requires a ray structure that is used to tell the origin, direction, and destination of the ray. You must also create a color3 structure, which will hold the returned illumination information after the ray has intersected a point on a surface and has been shaded accordingly. In order to guide each ray in the correct direction for each pixel on the image plane, you need to set up the local X, Y, and Z axes of the camera. Again the camera is always relative to the world axes and must be kept orthogonal. You not only need to set up the local coordinate system for the camera but must also guide each ray for each pixel in accordance with the camera position and the distance to the view plane. As all these procedures occur, a progress report is printed to the console screen that reports the number of columns on the screen that have rendered to the image plane. This keeps the user informed of what's happening in the application.

In order to accommodate the infinite orientations and positions of the camera, you must calculate the camera coordinate system. Each ray origin is set to the sum of the Z axis vector, camera position, and distance to the view plane (viewing distance). This creates the field of view. Each pixel position on the image plane is set as the direction the ray is going to take. In order to guide the ray throughout the world and guarantee it travels orthogonally, the ray's direction is multiplied by the X, Y, and Z camera vectors.

If the returned information is valid from the Tray_Ray_In_Scene() method, the ray has been traced successfully. The ray has intersected a point on a surface and must be shaded accordingly. When the point on the surface has been shaded, you must then saturate the returned color so each red, green, and blue component falls in the unit range of (0.0 to 1.0). Finally, the application requests the path and filename you want to create on your local file system. After you have entered this information, the application will begin processing the color buffer. It plots each pixel into the color buffer and finally saves the whole RGB color buffer (image) to a *.PPM formatted file that you've selected on your local file system.

The logic behind BeginTracing() is that, for each pixel on the image plane, the program picks a ray from the eye through this pixel where the pixel color is assigned from the information that is called by Tray_Ray_In_Scene(). The number of rendered columns is then printed to the console screen. Here's the code:

void cScene::BeginTracing()
{
  // Error Checking
   if ( this->nLightCount == 0 && this->nObjectCount == 0 )
   {
      printf("Error, no objects and lights loaded!\n");
      return;
   }
 cRay           Ray;
 color3            color = color3::Black;

 cVector3       X_Axis, Y_Axis, Z_Axis;
 cVector3        OffsetX, OffsetY;
 int         Half_Width, Half_Height,
         X,Y;
 int            w,h;

  // get half of width and height
  Half_Width      = RENDER_WIDTH / 2;
  Half_Height     = RENDER_HEIGHT / 2;

    // setup ray based on camera vectors
    Setup_Camera_Coords( &X_Axis, &Y_Axis, &Z_Axis );

    // ray must begin some distance into the scene where
    // the image plane is located
    Ray.origin = CameraPosition + ( Z_Axis * Viewing_Distance) ;

     // for each pixel on the image plane, spawn a ray
      for(w = 0; w < RENDER_WIDTH; w++)
      {
       // report progress to console screen
       cout << "Columns:" << w << " of " << RENDER_WIDTH-1 << endl;

          for(h = 0; h <RENDER_HEIGHT; h++)
          {
              X = w - Half_Width;
              Y = h - Half_Height;
             // calculate offset for the ray on the X axis
             OffsetX = X_Axis * (float) X;

             // calculate offset for the ray on the Y axis
             OffsetY = Y_Axis * (float) Y;

             // calculate ray direction based on the
             // computed x,y and z
             Ray.direction = (( OffsetX + OffsetY )
             + this->CameraPosition );

             // now position it to where the
             // image plane is located
             Ray.direction = Ray.direction - Ray.origin;

             // normalize the ray (unit vector)
             Ray.direction.Normalize();

             // trace the ray into the scene
             if( Tray_Ray_In_Scene
                 ( Ray, ZERO_TRACE_DEPTH, &color) )
             {
                 // saturate return color if it falls outside
                 // the 0.0 to 1.0 unit range
                 color.Sat();

                 // save color to image buffer
                 PlotPixel(w,h, color);
             }
         }
     }

    // write ppm
    char buffer[255];
    cout << "Please Enter output Path and Filename:" << endl;
    cout << "Usage (c:\\example.ppm): ";
    cin >> buffer;

   WritePPM(buffer, ColorBuffer, RENDER_WIDTH, RENDER_HEIGHT );
}

Tracing the Ray Through the Scene

The second part of the process involves actually tracing the ray throughout the scene. See Figure 9.7. The journey the ray takes must be calculated and recorded. After the ray leaves the image plane, the next thing you must do is find the closest intersection. This is done by calling the Find_Closest_Object() method. The method will return with the closest object found via a pointer to the closest intersection point.

Figure 9.7. The movements of rays leaving the image plane that intersect objects in the scene.

The next step of the algorithm is to shade the intersection point. A color structure is declared to store and return the final pixel intensity. The shading is done in a separate method called ShadePoint(). This part of the application is the brain of the operation because all of the lighting logic as well as the reflection and refraction capabilities are performed here. The color of the object is needed to shade a point and is applied to the color structure. The ambient coefficient is also summed into the solution. The diffuse interaction and specular highlight contribution is returned from the shade point method and is summed into the color structure. The RGB components are then saturated so that each component falls within the unit range of 0.0 to 1.0. After all these coefficients are applied to the color structure, the information is returned.

The logic behind Tray_Ray_In_Scene()is that it finds the closest intersection in the scene, computes the intersection point and normal, and calls ShadePoint() to shade the desired point for the different natural lighting effects.

bool cScene::Tray_Ray_In_Scene(cRay ray, int depth, color3 *pColor)
{
   cVector3 Hit, Normal;
   int      iClosestObject;
   color3     color_Object    = color3::Black,
          intensity_Shade = color3::Black;
   color3     color         = color3::Black;

   // find closest object
   iClosestObject = Find_Closest_Object(ray, &Hit);

   // get object normal
   Normal        = pObjectList[iClosestObject].Normal(Hit);

   // if object is in the local array list
   if( iClosestObject > -1 )
   {

       // object color

       color_Object = pObjectList[iClosestObject].Color;
       // BEGIN RAY TRACING

       if ( this->Render_Algorithm & RAY_TRACING )
       {
              // Ray Tracing uses an Ambient shading
              // to ensure objects are lit
              color =
                 color * pObjectList[iClosestObject].fAmbientFactor;

         // diffuse shading & specular highlights
         intensity_Shade =
         ShadePoint( Hit, Normal, iClosestObject );

         // combine our information
         color.r += color_Object.r * intensity_Shade.r;
         color.g += color_Object.g * intensity_Shade.g;
         color.b += olor_Object.b    ntensity_Shade.b;

   }

   // Saturate colors out of range
   color.Sat();
   // Save Color
      *pColor = color;
   }
   else
   {
      *pColor = color3::Black;
      return false;
   }
   return true;

}

Shading the Point of Intersection

This final part of the process shades a point in 3D space and projects it on to the view plane. The shade point method calculates the radiance of a point. The function cycles through each light source and applies the diffuse interactions and specular highlights accordingly. The rays that leave the image plane and hit objects are called primary rays. As these rays intersect points on surfaces, they are shaded using the shade point method. A point is shaded as a result of rays being shot directly from the view plane and not from any other surface; this process is called direct illumination or local illumination. When rays hit objects from different surfaces other than the eye, this is called secondary illumination.

This information is relative to the type of ray tracing you use. For example, in forward ray tracing, direct illumination refers to rays being shot directly from the light source instead of the eye as in backward ray tracing. The shade point method calculates the shading for the diffuse surface as well as applying the specular highlights. It also includes the different light sources' contributions (wattage and color) to the shaded point. It is important to point out that, as of now, light comes directly from the eye and is shaded relative to the light source. The light doesn't get reflected/transmitted to other surfaces. For this reason, you don't need a recursive algorithm because reflection and refraction require the recursive functionality because of secondary rays being launched at the point of intersection. As the color structure is shaded to the different types of effects, it is then returned to the caller function with the new final shade. The shade method includes the specular highlights and the diffuse interaction contribution for the point.

The logic behind ShadePoint()is that it sets the initial color to black. It then cycles through each light source in the scene and applies the color of the object and the different natural light coefficients for diffuse shading, specular highlights, and the light source color and wattage for the intersection point.

color3 cScene::ShadePoint(cVector3 hit_point, cVector3 normal, int iObject)
{
    //Get normal
    cVector3 From_Light_To_Hitpoint, Shininess;
    int i;
    float fLen, fAngle;

    // color is black by default
    color3 color;

    // set color to black
    color = color3::Black;

    // for each light
    for( i = 0; i < this->nLightCount; i++ )
    {
     // is light okay to use?
     if( pLightList[i].bActive )
     {

        // from light to hit point
           From_Light_To_Hitpoint = pLightList[ i ].vPosition - hit_point;
        fLen = From_Light_To_Hitpoint.Mag();
        From_Light_To_Hitpoint.Normalize();

        // find angle between surface normal and light direction
        fAngle = ( From_Light_To_Hitpoint * normal );

        // if visible from lightsource to surface
        if( fAngle > 0.0f )
        {
           // do diffuse interaction on surface
          if( RenderSetting.use_diffuse_shading
             && pObjectList[iObject].fDiffuseFactor> 0.0f )
          {

             color.r +=
             ( fAngle * pLightList[ i ].Color.r ) *
pLightList[ i ].fWattage *
pObjectList[iObject].fDiffuseFactor ;
            color.g +=
(  fAngle * pLightList[ i ].Color.g ) *
   pLightList[ i ].fWattage *
  pObjectList[iObject].fDiffuseFactor ;

             color.b +=
            (  fAngle * pLightList[ i ].Color.b ) *
       pLightList[ i ].fWattage *
     pObjectList[iObject].fDiffuseFactor ;
          }

         // do specular highlight
         if( RenderSetting.use_specular_hightlights
              && pObjectList[iObject].fShininess > 0.0f )
         {
             Shininess = ( normal * ( 2.0f * fAngle ))
   From_Light_To_Hitpoint;

             float Dot = ( Shininess * this->Viewing_Direction);

            if( Dot > 0.0f )
             color += pLightList[i].Color *
              (    (float)pow( Dot,
     pObjectList[iObject].fSpecularExponent )
                   * pObjectList[iObject].fShininess );
         }
      } // end visible
    } // end light on
  } // end number of lights
  return color;
}


Using the Chapter's Source Code

In /Book Code/Chapter 09/ are three ray-tracing applications. The folder names are Ray Tracer 1, Ray Tracer 2, and Ray Tracer 3. The general design of the applications follows a set of classes in a /Common folder and one Ray Tracer.cpp file for the general logic of the programs. You are going to include all of these classes into the ray-tracing applications in this chapter. These classes are required to implement ray tracing as well as photon mapping, so they are extremely important.

The entry point for the programs is inside main(). This is how you dictate to the application what type of logic should be executed for ray tracing. The body and main logic of the program are located in /Program Name/Ray Tracer.cpp. This is the brain of the program, where it calls the different methods of the scene class. The cScene class is a global object that is called throughout the ray-tracing program. The required class files (as well as the flow of the program) are listed in /Program Name/Ray Tracer.cpp. Take a look at the code:

#include <stdio.h>
#include <iostream.h>
#include <fstream.h>
#include <windows.h>
#include <math.h>
#include <stdio.h>
#include <string.h>

#include "Common\MathTools.h"
#include "Common\Color3.h"
#include "Common\cVector3.h"
#include "Common\cRay.h"
#include "Common\cObject.h"
#include "Common\cLight.h"
#include "Common\PPM.h"
#include "Common\cScene.h"

// SETUP OBJECTS
cScene TheScene;

int main(int argc, char* argv[])
{
        // for timing
        unsigned long Start_Time    = 0;

        long                  Whole_Interval     = 0,
                              seconds             = 0;
        int                    G_Curr_Mins       = 0,
                               G_Curr_Secs       = 0,
                               Average           = 0;

        // grab begin time
        Start_Time = GetTickCount();
     // RAY TRACER
     printf(" TODO: Program Name Goes Here !\n");

     // Begin
     printf("Init Scene!\n");
     TheScene.Init_Scene();

     // Scene Stats
     printf("Scene Stats!\n");
     printf("Objects: %d Lights: %d\n",
         TheScene.nObjectCount, TheScene.nLightCount);

     // Trace
     printf("Tracing Scene!\n");
     TheScene.BeginTracing();

     // We're all done!!
     printf("Done Tracing Scene!\n");
     TheScene.Kill_Scene();

     // actual render time
     Whole_Interval  = 0;
     Whole_Interval  = (GetTickCount() - Start_Time);
     seconds          = (Whole_Interval / 1000);
     G_Curr_Mins      = (int)seconds / 60;
     G_Curr_Secs      = (int)seconds % 60;

     printf("Render Took %d Minute(s) and  %d Second(s)...",
        G_Curr_Mins, G_Curr_Secs);

     // halt and beep
     Beep(1000, 500);

     return 0;
}

As you can see, the general flow of the program is simple. At the beginning of the program the current tick count is saved in a variable. The program loads in the scene by calling TheScene.Init_Scene().The scene stats are then reported after all the elements from the scene definition file are read in. The process begins tracing the rays in the scene by calling TheScene.BeginTracing(). After the ray-tracing algorithm has rendered the scene, the program releases memory by calling TheScene.Kill_Scene(). The time it took to render the scene is calculated and the computer speakers beep.

Let's take a look at a program designed to perform diffuse interaction and specular highlights in the next section.


Program 1: Ray Tracer 1

In the path /Book Code/Chapter 09/ on the CD is a folder called Ray Tracer 1. The application in this folder shades a given point in the scene using the method just defined. That is, it will shade the diffuse interaction (using the cosine method) on the surface and will also apply the specular highlights (shininess).

The /Commonfolder contains all the underlying classes you've been developing throughout the book. These files are used in the ray-tracing application. Open this application in Microsoft Visual C++ 6.0. In order for the scene definition files to work they must be in the relative path with the executable. You must run the application using Microsoft Visual C++ or by pressing Control+F5.

Inside the folder /Ray Tracer 1 is another folder called /Scenes. The files included in this folder are the scene definition files for the program. The basic design of the programs in this book include the /Common folder. This folder contains the basic classes required to set up a 3D rendering environment. The folder that includes the required classes for this program is in /Book Code/Chapter 09/Ray Tracer 1/Common.

The files in this folder are:

12/21/2002  04:48p               1,381 cLight.h
12/21/2002  04:38p               5,127 cObject.cpp
12/21/2002  04:58p               4,667 cObject.h
12/21/2002  04:39p                 442 Color3.cpp
12/04/2002  12:33p               4,476 Color3.h
12/21/2002  04:39p                  23 ay.cpp
12/21/2002  04:59p               2,043 cRay.h
12/21/2002  04:39p              22,499 cScene.cpp
12/21/2002  04:59p               3,324 cScene.h
12/19/2002  05:29p               1,991 cVector3.cpp
12/21/2002  04:47p               5,171 cVector3.h
12/21/2002  04:59p               1,060 MathTools.h
12/21/2002  04:39p               1,108 PPM.cpp
12/21/2002  04:49p                 154 PPM.h

To compile this program, follow these steps:

  1. Open the project workspace (Ray Tracer 1.dsw or Ray Tracer 1.dsp).

  2. From the File menu, choose Build, Rebuild All.

  3. From the File menu, choose Build, !Execute Ray Tracer 1.exe.

To run the program directly instead, you can execute the following file:

/Book Code/Chapter 09/Ray Tracer 1/Ray Tracer 1.exe

The program begins running and will soon prompt you to enter the path of the scene definition file. When the program asks you to "Enter Information:", you enter:

/scenes/diffuse1.txt

or

/scenes/diffuse2.txt

or

/scenes/diffuse3.txt

The program starts the ray-tracing process and then will prompt you to enter the output path for the rendered file stored on the image plane. When it asks "Please Enter output Path and Filename:", you enter:

/scenes/myoutput1.ppm

or

/scenes/myoutput2.ppm

or

/scenes/myoutput3.ppm

You can then browse to that path and open the file in IrfanView. However, I've already created the exported files for you and placed them in the follow-ing folder:

/Book Code/Chapter 09/Ray Tracer 1/Output

The files in this folder are:

12/10/2002  10:38p           3,266,333 diffuse1.ppm
12/10/2002  10:42p           3,301,150 diffuse2.ppm
12/10/2002  10:47p           3,119,627 diffuse3.ppm

Using the Program

In this section, you begin

compiling the program and executing it using Microsoft Visual C++ 6.0. The application will begin running and you will see the following information on the screen.

NOTE

TIP

Ray tracing renders spheres very quick-ly.It may take a few seconds to render a sphere with mirror reflections.

The application begins in a DOS command prompt window. Both ray tracing and photon mapping can be implemented on any platform, so I've decided to keep things at a minimum and use a simple method to implement the algorithms. The command prompt concepts can run on any platform. Press Control+F5 to begin the program.

The program input is as follows:

Please Type in Path and File Name
Usage (ex. C:\sample.txt)
Enter Information:

This is where you enter the relevant information pertaining to the scene files. For example, if you wanted to render the diffuse1.txt file, you would type it into the line as: Scenes/diffuse1.txt. This procedure is going to load all data in the scene for rendering.

The program statistics are as follows:

Init Scene!
Scene Stats!
Objects: 2 Lights: 2
Tracing Scene!

The objects and light lists are updated and will report this information. The application then begins tracing the scene. After the scene is rendered to the image plane, all the data is copied over to the color buffer. The application then requests for an output path and filename.

The program output is as follows:

Please Enter output Path and Filename:
Usage (c:\example.ppm):

This is where you enter the output file. This is a file with a *.PPM extension. After you have entered this information, the application is complete.

The program time report is as follows: eDone Tracing Scene! Render Took 0 Minute(s) and 9 Second(s)...

The application reports the time it took to load in the scene and render it. As you can see, it took nine seconds to render Scenes/diffuse1.txt. This is the general format for all future ray-tracing applications, so be sure to commit this procedure to memory.

The Renders

The scene files are rendered using local lighting or direct illumination. The shading is composed of diffuse interaction and specular highlights, as shown in Figures 9.8 through 9.10. Figure 9.8 shows an example of two spheres that are rendered with diffuse interaction and specular highlights.

Figure 9.8. Scenes/diffuse1.txt contains two spheres that are rendered using diffuse interaction and specular highlights.

Figure 9.9 shows an example of a room model that is rendered with diffuse interaction.

Figure 9.9. Scenes/diffuse2.txt contains a room model that is rendered using diffuse interaction.

Figure 9.10 shows an example of a table that is rendered with diffuse interaction.

Figure 9.10. In Scenes/diffuse3.txt contains a table model that is rendered using diffuse interaction.

    [ LiB ] Using the Chapter's Source Code Revisiting Reflection and Refraction


Revisiting Reflection and Refraction

We haven't really talked about reflecting and transmitting light in ray tracing. After the ray hits a reflective material some of the energy is emitted back into space. As the energy travels it can then bounce (reflect) or refract (transmit) onto nearby surfaces. For this reason the light reflection/transmitting algorithm must be capable of handling multiple bounces of rays moving from object to object. For refraction we must consider the light partially passing through the object. As we said in Chapter 3, refraction occurs on transparent objects. This means that the opacity of the object must be less than 1.0. If the object is specularly reflective to some degree a new ray is traced from the point of intersection towards the reflection. See Figure 9.11.

Figure 9.11. An example of (mirror) reflection on a sphere.

The new ray can then bounce from object to object. The number of times a ray interacts with an object must be limited. Now if you think about it the multiple bounces of a ray from the image plane can be seen as a tree of light paths.

Let's take a look at the steps required to simulate reflection and refraction. We are going to alter the Tray_Ray_In_Scene() method to accommodate the new functionality required to reflect and refract rays. If you're a bit hesitant about the new changes in the method, don't worry because we're not going to conduct any major changes to the method.

To trace the ray in the scene, you follow these steps:

  1. Find the closest intersection with the scene.

  2. Compute the intersection point and the normal.

  3. Color = Shade Point( point, normal )

  4. Reflected Shade = Spawn Reflected Ray

  5. Color += Add Reflected Shade

  6. Refracted Shade = Spawn Refracted Ray

  7. Color += Add Refracted Shade

  8. Return the color.

As you can see, the changes are minimal with only a few more steps required for reflection and refraction. Let's take a look at a program designed to perform reflection and refraction in the next section.


Program 2: Ray Tracer 2

You already know how to shade a surface for diffuse interaction and specular highlights. Now you need to implement specular (mirror) reflections of light and the refraction of light on surfaces. The program that incorporates reflection and refraction resides in the folder /Ray Tracer 2. The /Common folder contains all the underlying classes you've developed in the book. These files are used for the ray-tracing application. The only thing that's changed from the previous application is the addition of the Tray_Ray_In_Scene() method, which incorporates the new features for reflection and refraction. After a ray has been shaded, new rays are launched from the point of intersection. These new rays can be reflected or refracted. The new rays are spawned and will move through space until they hit other surfaces. Depending on the type of surface they encounter, the rays will either diffusively shade the surface or specu-larly reflect onto other surfaces. The process continues until the maximum trace depth is encountered.

NOTE

NOTE

Ray tracing is extremely good at mirror reflection and refraction.

Take a look at the new method that follows. The reflected and refracted directions are computed by using the vector reflection and vector refraction functions. The application then calls the same function again with the new reflected or refracted directional vector as its parameter. The process begins again with the new vector as the input direction. Its goal is to find any objects nearby that were intersected. If a new intersected point is found it is summed into the point in the previous recursion. After the reflected or refracted contribution is found, it is summed into the current pixel. The type of reflection or refraction that occurs is based on the material properties. The new code is listed in bold:

bool cScene::Tray_Ray_In_Scene(cRay ray, int depth, color3 *pColor)
{
   cVector3         Hit, Normal;
   cVector3         Reflected_Dir, Refracted_Dir;
   int              iClosestObject;

   color3    color_Object     = color3::Black,
             intensity_Shade  = color3::Black;
   color3   color             = color3::Black;

   color3   reflected_Shade   = color3::Black;
   color3   refracted_Shade   = color3::Black;

   if( depth > MAX_TRACE_DEPTH )
   {
      *pColor = color;
      return false;
   }

   // find closest object
   iClosestObject = Find_Closest_Object(ray, &Hit);

   // get object normal
   Normal = pObjectList[iClosestObject].Normal(Hit);

    // if object is in the local array list
   if( iClosestObject > -1 )
   {
       // object color
      color_Object = pObjectList[iClosestObject].Color;

       // BEGIN RAY TRACING
       if ( this->Render_Algorithm & RAY_TRACING )
       {

       color = ( color * pObjectList[iClosestObject].fAmbientFactor );
        intensity_Shade = ShadePoint( Hit, Normal, iClosestObject );

    color.r += color_Object.r * intensity_Shade.r;
    color.g += color_Object.g * intensity_Shade.g;
    color.b += color_Object.b * intensity_Shade.b;

        // Calculate Reflection
    if( RenderSetting.use_specular_reflection &&
              pObjectList[iClosestObject].fSpecularFactor > 0.0f )
     {

      Reflected_Dir = ray.direction.Vector_Reflection( Normal ) ;

          if(!Tray_Ray_In_Scene(cRay(Hit, Reflected_Dir),
                   depth + 1, &reflected_Shade ))
      reflected_Shade = color3::Black;
     }

         color += (reflected_Shade *
            pObjectList[iClosestObject].fSpecularFactor);

      // Calculate Refraction
      // Mediums are currently : From Air (1.003)
      // to the Surface Refraction Index
   if( RenderSetting.use_refraction &&
   pObjectList[iClosestObject].fOpacity < 1.0f )
   {
         // surface - into air
    if(ray.direction.Vector_Refraction(Normal,
            1.003f,pObjectList[iClosestObject].fIndex_Of_Refraction,
            &Refracted_Dir ))
           {
         if( !Tray_Ray_In_Scene(cRay(Hit, Refracted_Dir),
                   depth + 1, &refracted_Shade ))
      refracted_Shade = color3::Black;
       }
     }

color += ( refracted_Shade  * pObjectList[iClosestObject].fOpacity );

   } // end ray tracing

      color.Sat();
   *pColor = color;
}
   else
   {
      *pColor =      color3::Black;
      return  false;
   }

   return  true;
}

Using the Program

To use this program, you need to open the application in Microsoft Visual C++ 6.0, in debug mode. In order for the scene files to work in a relative path with the executable, you must run the application using Microsoft Visual C++. Recall that the /Common folder contains all the underlying classes used for the ray-tracing application. In the folder called /Ray Tracer 2 is another folder called /Scenes. The files included in the /Scenes folder are the input scene files you will type into the program.

The /Common folder is in /Book Code/Chapter 09/Ray Tracer 2/Common. The files in this folder are:

12/21/2002  04:48p               1,381 cLight.h
12/21/2002  04:38p               5,127 cObject.cpp
12/21/2002  04:58p               4,667 cObject.h
12/21/2002  04:39p                 442 Color3.cpp
12/04/2002  12:33p               4,476 Color3.h
12/21/2002  04:39p                  23 cRay.cpp
12/21/2002  04:59p               2,043 cRay.h
12/21/2002  04:39p              22,499 cScene.cpp
12/21/2002  04:59p               3,324 cScene.h
12/19/2002  05:29p               1,991 cVector3.cpp
12/21/2002  04:47p               5,171 cVector3.h
12/21/2002  04:59p               1,060 MathTools.h
12/21/2002  04:39p               1,108 PPM.cpp
12/21/2002  04:49p                 154 PPM.h

To compile this program, follow these steps:

  1. Open the project workspace (Ray Tracer 2.dsw or Ray Tracer 2.dsp).

  2. From the File menu, choose Build, Rebuild All.

  3. From the File menu, choose Build, !Execute Ray Tracer 2.exe.

To run the program directly instead, execute the following file:

/Book Code/Chapter 09/Ray Tracer 2/Ray Tracer 2.exe

The program begins running and will soon prompt you to enter the path for the scene definition file. When it asks "Enter Information:", you enter:

/scenes/reflect1.txt

or

/scenes/reflect2.txt

or

/scenes/refract1.txt

The program starts the ray-tracing process and then again will prompt you to enter the output path for the rendered file stored on the image plane. When it asks " Please Enter output Path and Filename:", you enter:

/scenes/myoutput1.ppm

or

/scenes/myoutput2.ppm

or

/scenes/myoutput3.ppm

You can then browse to that path and open the file in IrfanView. However, I've already created the exported files for you and placed them in the follow-ing folder:

/Book Code/Chapter 09/Ray Tracer 2/Output

The files in this folder are:

12/10/2002  11:22p           3,272,665 reflect1.ppm
12/10/2002  11:26p           3,153,601 reflect2.ppm
12/10/2002  11:34p           3,129,944 refract1.ppm

The Renders

The files shown in Figures 9.12 and 9.13 are rendered using reflection and refraction lighting effects. Figure 9.12 shows two spheres rendered with diffuse interaction and mirror reflection.

Figure 9.12. Scenes/reflect1.txt contains two spheres rendered with diffuse interaction and mirror reflection.

Figure 9.13. Scenes/reflect2.txt contains a table with a sphere rendered with diffuse interaction and mirror reflection.

Figure 9.13 shows an example of a table with a mirror and a sphere that is rendered with mirror reflection. If you look closely, you can see the table and sphere reflected in the mirror.


Simulating Shadow Rays

Wow, you've come a long way and now you can do many wonderful natural lighting effects such as diffuse interaction on surfaces, specular reflection, specular highlights, and refraction. But you're still missing one very important natural lighting effect. Shadows are very important to simulating real-world examples because shadows are the most common lighting effect you see. You need to simulate shadows in your rendering environment using ray tracing. Shadows are very simple to simulate using ray tracing. In order to do diffuse shading on a surface you need the point of intersection. This same point of intersection is needed to do shadows.

This point can be used as the origin of the shadow ray. The direction of the shadow ray is the point of intersection towards the light source. If any other object in the scene is blocking this line of sight between the point of intersection and the light source, this is considered a shadow and must be shaded accordingly. As you can see from the previous figure, the sphere casts the shadow on the flat polygon because it obstructs the line of sight between portions of the polygon and the light source. In order to simulate shadows, you need a function that determines whether the line of sight between two vec-tors is obstructed by objects in the scene. This effect can be easily applied to the shade point method.


Program 3: Ray Tracer 3

You can now upgrade your application so it can handle shadow rays. The application is going to trace a ray backwards from the point of intersection to the light source to determine whether anything is obstructing the line of sight. If the line of sight is obstructed, the color black RGB(0,0,0) will be assigned to the pixel and returned as the shade. This in essence creates the shadow. If the line of sight isn't obstructed, the point is shaded normally.

Determining if the Light Is Blocked

The first thing you need in order to simulate shadows is a method similar to Find Closest Object(), but one that determines whether the line of sight between a point to the light source (two vectors) is blocked. The method cycles through each object in the scene, testing for points of interaction. If the ray intersects a point on an object, it returns true. If not, it signals that the ray isn't obstructed and should be shaded normally.

Here is the method:

bool cScene::Is_It_Blocked( cRay Ray, float LightDistance )
{
  int iFoundObject;

   for(int i = 0; i < this->nObjectCount; i++)
   {
     if( pObjectList[ i ].Default_Primitive != OBJECT_NOTHING)
     {

       iFoundObject = pObjectList[i].Find_Hitpoint( &Ray );
       if( iFoundObject && ( Ray.distance <= LightDistance ) )
           return true;
      }
   }

  return false;
}
Here is the logic:
Shade Point( point, normal )
      Color = black
      For each light source
      If not Shadow Ray
      Color = Color + direct illumination
Return Color

You can now update the ShadePoint() method to accommodate the shadow ray functionality. If the line of sight is blocked from the point of intersection to the light source, the value for black is returned. The new code is listed in bold:

color3 cScene::ShadePoint(cVector3 hit_point, cVector3 normal, int iObject)
{
   //Get normal
   cVector3 From_Light_To_Hitpoint, Shininess;
   int i;
   float fLen, fAngle;

   // color is black by default
   color3 color;

   // set color to black
   color = color3::Black;

   // for each light
   for( i = 0; i < this->nLightCount; i++ )
   {
    // is light okay to use?
    if( pLightList[i].bActive )
    {

       // from light to hit point
        From_Light_To_Hitpoint = pLightList[ i ].vPosition - hit_point;
       fLen = From_Light_To_Hitpoint.Mag();
       From_Light_To_Hitpoint.Normalize();

       // find angle between surface normal and light direction
       fAngle = ( From_Light_To_Hitpoint * normal );

       // if visible from lightsource to surface
       if( fAngle > 0.0f )
       {
         // Check to see if anything is blocking this ray to the object
         if( !Is_It_Blocked(
                cRay( hit_point, From_Light_To_Hitpoint ),
                           fLen ) )
       {
       // do diffuse interaction
       if(RenderSetting.use_diffuse_shading &&
               pObjectList[iObject].fDiffuseFactor> 0.0f )
       {
           color.r +=
           ( fAngle * pLightList[ i ].Color.r ) *
             pLightList[ i ].fWattage *
             pObjectList[iObject].fDiffuseFactor ;

          color.g +=
           ( fAngle * pLightList[ i ].Color.g ) *
             pLightList[ i ].fWattage *
             pObjectList[iObject].fDiffuseFactor ;

         color.b +=
          ( fAngle * pLightList[ i ].Color.b ) *
            pLightList[ i ].fWattage *
            pObjectList[iObject].fDiffuseFactor ;

       }
       // do specular highlight
       if(RenderSetting.use_specular_hightlights
            && pObjectList[iObject].fShininess > 0.0f )
       {
          Shininess =
              ( normal * ( 2.0f * fAngle ))
                  From_Light_To_Hitpoint;

          float Dot = ( Shininess * this->Viewing_Direction);

         if( Dot > 0.0f )
          color += pLightList[i].Color
                * ( (float)pow( Dot,
                   pObjectList[iObject].fSpecularExponent )
                * pObjectList[iObject].fShininess );
       }
     } // is light blocked
   } // end visible
 } // end light on
} // end number of lights
    return color;
}

Using the Program

To use this program, follow the same instructions as for using the previous two examples. The folder /Ray Tracer 3 contains another folder called /Scenes. The files included in this folder are the input scene files you will type into the program.

This /Common folder contains the basic classes required to set up a 3D rendering environment. The folder that includes the required classes for this program is in /Book Code/Chapter 09/Ray Tracer 3/Common.

The files in this folder are:

12/21/2002  04:48p               1,381 cLight.h
12/21/2002  04:38p               5,127 cObject.cpp
12/21/2002  04:58p               4,667 cObject.h
12/21/2002  04:39p                 442 Color3.cpp
12/04/2002  12:33p               4,476 Color3.h
12/21/2002  04:39p                  23 cRay.cpp
12/21/2002  04:59p               2,043 cRay.h
12/21/2002  04:39p              22,499 cScene.cpp
12/21/2002  04:59p               3,324 cScene.h
12/19/2002  05:29p               1,991 cVector3.cpp
12/21/2002  04:47p               5,171 cVector3.h
12/21/2002  04:59p               1,060 MathTools.h
12/21/2002  04:39p               1,108 PPM.cpp
12/21/2002  04:49p                 154 PPM.h

When you run it using the procedure described earlier, the program will prompt you to enter the path for the scene definition file. When it asks "Enter Information:", you enter one of the following:

/scenes/shadow1.txt
/scenes/shadow2.txt

The program starts the ray-tracing process and then again will prompt you to enter the output path for the rendered file stored on the image plane. When it asks "Please Enter output Path and Filename:", enter one of the following:

/scenes/myoutput1.ppm
/scenes/myoutput2.ppm

You can then browse to that path and open the file in IrfanView. I have created the exported files for you and placed them in the following folder:

/Book Code/Chapter 09/Ray Tracer 3/Output

The files in this folder are:

10/13/2002  03:30p           3,200,805 shadow1.ppm
10/13/2002  03:43p           3,131,109 shadow2.ppm


Summary

This chapter covered many of the most common types of lighting effects available. You developed your own ray-tracing application that simulates numerous types of effects. You also learned how to write a method that searches through objects in the world and tests for point intersections.

You can see the coloring of light absorbed into the material that simulates diffuse reflection. You wrote a function that shades a point based on the cosine method. You can now cast shadows on walls using the shadow ray method. You also wrote a function that determines whether the line of sight is obstructed between two vectors. You can see the bright shiny spots on smooth surfaces such as apples and billiard balls with specular highlights. You have come a long way into rendering synthetic images. With photon mapping, you'll learn to improve these important techniques that you've just learned.


Chapter 10. Photon Mapping in a Nutshell

Photon mapping has been around for some time now and is proving to be better at computing global illumination than radiosity. This chapter explains photon mapping from a theoretical point of view. Later chapters will then delve deeper into explaining its practical applications. The book will, in essence, implement the method in the following chapters. This chapter is an overview of what photon mapping is and why it's far better than radiosity and ray tracing. So let's begin.


The Goal

The primary goal here is to achieve global illumination with an algorithm that is capable of handling various types of geometry as well as different types of materials. As noted in Chapter 5, ray tracing and radiosity suffer from specific drawbacks. You need a new algorithm that overcomes these drawbacks. Each new algorithm promises to achieve a specific advantage over the previous ones, which should be your goal.

You need an algorithm that is efficient and robust for rendering synthetic images. You want to be able to take the positive aspects from the previous algorithms and derive an algorithm with fantastic rendering results and minimal problems. But it's impossible to develop a perfect solutionsimulating light in all of its complexity is a very daunting task and even the most advanced rendering algorithms today are far from being perfect.

One reason for this is simply that many aspects of light are still not yet fully understood. For now, let's work with what you have. The primary objective is to implement an algorithm that tracks the flow of light in a global manner throughout the scene. Light is everywhere and you must track the overall energy that moves around in space. If you think about it for a moment, the energy that arrives at a surface can do two things. The energy can be absorbed into the object as heat or it can be transmitted through the object (as seen in refraction) or reflected (either diffusively or specularly) to another object. This simple concept can create the most realistic images if simulated properly. This is what you intend to simulate when you implement photon mapping.


Problems with Ray Tracing and Radiosity

In the past years, ray tracing and radiosity have been extensively used to simulate light. Ray tracing and radiosity suffer from their own individual problems. Let's quickly cover some of these problems in each algorithm.

Problems with ray tracing include the following:

  • Diffuse reflection

  • Indirect illumination

  • Errors in calculating the right pixel intensity

  • Multiple reflection problems

  • Not a global illumination algorithm

Problems with radiosity include the following:

  • Excessive memory consumption

  • Specular reflections

  • Inaccuracy between materials

  • Meshing constraints

  • Poor realism with curved surfaces

You need an algorithm that can correctly simulate diffuse reflections or basically scatter light in many random directions in a correct fashion. You need an algorithm that correctly computes the penumbra in shadows and calculates the right pixel intensity based on the flow of light throughout the whole scene, or in short, global illumination. You need an algorithm that can do specular reflections but doesn't require a large amount of memory to render them, and can accurately tell the correct flow of light between different types of materials. You don't want a subdivided mesh of the original geometry because it creates a whole array of problems. You want to simulate lighting accurately on any type of curved surface and complex object. You want to eliminate all of these problems and develop an algorithm to fix these mishaps. No small feat!


The Concept Behind Photon Mapping

The model for photon mapping utilizes the basic form of light transport using the old fundamental concepts of ray tracing. This way, you can keep the simplicity of distributing light because it follows the basic concept of how light really works in nature. Because the application you are developing isn't real-time, you want to render the scene after all the light has already propagated into the scene. All the dark areas will be lit and all the reflections and refrac-tions have already occurred.

The observer and the light sources are the two major elements you must take into account when rendering. If you were to render the scene from the observer's point of view, you would run into the same problems as seen in backward ray tracing. This was why bi-directional ray tracing was developed, which means you render the scene in two passes. You need to take the observer and the light sources into consideration when rendering because they both reveal very important information about the flow of light. The scene is rendered using the eyes of the observer and the lighting comes from the light sources. You must utilize both of these aspects to calculate the light-ing correctly. The light must be scattered (find the irradiance) and then observed (find the radiance).

Here is the fundamental process when rendering:

  1. As light is emitted from a light source, it is scattered by nearby objects. The light is then either absorbed into the object or reflected/transmit-ted to another object.

  2. Sample the scene from the observer's point of view after all the light has propagated into the scene. Then store the visual image of what the observer perceives on the image plane.

In the scene, surfaces are often rendered in a smooth shading manner because the intensity of light leaving each point (or reflected radiance) does-n't change much on large surfaces. For this reason, it would make sense to use and re-use the illuminative information stored at the surfaces in the scene. This would free up memory in the long run when rendering vast and complex models. This means that the illuminative information must be preserved and stored at the surfaces in the scene in order to re-use lighting information. This is what radiosity does by storing the illuminative values in each element of the subdivided mesh. But as you recall from Chapter 5, creating a subdivided mesh of the original geometry can pose a series of new problems. You certainly don't want to deal with meshing constraints and artifacts.

The first thing you need to do is to break apart the lighting information from the geometry, which means that you need to store the illuminative information in a separate entity without actually subdividing the original geometry. The illuminative information is not actually radiance; it is a delta that tracks the overall energy or photons arriving at each surface in the scene (or the irradiance).

In an unlit environment, everything looks pitch black and even the smallest amount of light contributes to lighting an environment. For example, a can-dle in a big room can send light waves in the most unusual places. The more photons that propagate into a given region, the higher the amount of energy or the higher the density of energy.

Because you are trying to simulate global illumination, you need to track energy everywhere in the scene and not only from the light source to a point on a surface. This is one way ray tracing is weak; it cannot calculate indirect illumination properly. It simply isn't a global illumination algorithm. Shadows look pitch black, thus creating very ugly results in renders.

One fix to this problem is using an ambient component for each surface so that every object in the scene is lit to some small degree. This helps in making shadows brighter but it is a quick and cheap fix. Another fix is to cast several rays from the light sources to guarantee a shadow is cast properly. The penumbra in some shadows just can't be simulated at times. These problems are mainly because ray tracing is a point-sampling algorithm. Radiosity is better than ray tracing at computing diffusive reflections. You want to be able to correctly simulate diffusive reflections across a given surface. You also want to copy the specular reflection functionality as seen in ray tracing because it's wonderful at computing this phenomenon.


The Solution

The first part of the solution is to solve the meshing problem that radiosity poses. You want to store the illuminative information as a primitive in a global list that can be archived and searched for later. This list would be used anytime in the application to retrieve incoming lighting information relative to each point on a surface. You need a primitive that is much smaller than a patch and that requires less memory consumption. It needs to be resilient and nominal in size; it must have the capability to handle any type of geometry.

The requirement is to simulate energy particles moving around in space. The simple solution is to use a very small structure similar to a 3D point. Points can determine position, which is one important element of lighting. You also need an intensity component that carries a fraction of the light source's power. But it doesn't stop here. Light waves travel in an arbitrary direction, so you need a direction component. Combining these three components yields the photon.

The Photon

The following code shows the photon structure that includes the three attributes of energy, direction, and position:

typedef struct
{
   color3   tPower;
   cVector3 tDirection;
   cVector3 tPosition;
} photon_t;

Photons are compact and use a lot less memory than low-resolution patches, used in radiosity. The list of photons sitting across each surface in the scene is called the photon map. Using photons not only serves the needs for simple surfaces, but can also enable you to represent very irregular and complex geometry. Curved surfaces are a nightmare to compute in radiosity. Using simple photons creates a resilient representation because photons are very small and compact. You don't have to worry about the meshing constraints, as you do in radiosity. Ray tracing uses point calculations constantly and that's why it is very good at computing specular reflections, typically mirrors. You can use photons to calculate curved surfaces such as clouds, liquids, smoke, and many other irregular objects such as rocks, trees, and concrete.

In ray tracing, we traced rays moving from the eye into the scene that reflected a few objects. This movement the ray takes is known as the eye-to-object path. Similarly, the photon map is like a list of light paths taken from the light source to the surfaces and from the camera to the surfaces. These different path movements are recorded in a map. Begin by taking a look at Figure 10.1.

Figure 10.1. The photon map preserves the light paths taken from the light sources to objects as well as the path from the observer to objects.

The photon map in Figure 10.1 is simply a list of photons in a simple array. To visualize its purpose, think of a set of light bulbs positioned in a box of wood. See Figure 10.2 for an example.

Figure 10.2. These light bulbs illustrate the idea of photons contributing to the appearance of a surface in the scene.

As you know, each light bulb carries a different wattage. Light bulb one is 60 watts, light bulb two is 80 watts, and light bulb three is 100 watts. Light bulb three emits the highest amount of energy. Now if you place a translucent glass cover over the top of this wooden box, what would happen? The glass cover would transmit the light and the three bulbs will appear as one single surface emitting light where each light bulb may cause a hot spot on the surface of the cover. See Figure 10.3 for an example.

Figure 10.3. The glass cover simulates light leaving an area.The three light bulbs contribute to the overall intensity where a few hot spots appear on the surface.The photons stored on the surface contribute to its intensity shading across the surface.

Now, if you're really cunning, you might realize that this simple example is analogous to how the photons are stored on the surfaces in the scene. The photons are used to shade the overall appearance of the surface. Each light bulb can be seen as a photon that contributes to the overall intensity of the surface. The lighting on the cover is brighter where the wattage is higher. Figure 10.4 shows what would happen if you added more light bulbs.

Figure 10.4. More lights in a box show the overall intensity of light with more detail and fewer hot spots on the glass cover.Similar to adding more photons to the surface,this technique helps to generate a better render of diverse lighting across the surface.

The primary idea is to separate the lighting information from the geometry in the scene so the lighting information can be used at anytime during the course of rendering. This lighting information is primarily a list of paths preserved in a list of paths taken from both the eye and the light sources. The photon map can be seen as a cache of light paths that contributes to the radiance of each point on a surface. The more photons there are at a surface, the more diverse the lighting across the surface.

The first step is to trace all photons from each light source into the scene, and then save the illuminative values at the surfaces in a global list for later retrieval. These illuminative values calculate the energy that is arriving at each surface or the incoming flux in the model.

The next logical step is to render the scene by tracing rays from the view plane to each surface into the scene. By calculating the average sum of a set of photons in a given area, you can find the radiance of each point on a surface. Don't worry if you're a bit lost here; more on this later. Each photon cannot determine the radiance of a point by itself because photons determine the irradiance and must be averaged with other photons to find the radiance.

If you were to trace the correct lighting on a surface, the photon map requires a higher density of photons to compute the radiance for a point. This means that if you want to shade an arbitrary surface in a scene, you must use a good number of photons to retrieve reasonable results. This is one minor problem with photon mapping. It is, however, the only way to alleviate the major problem of noise seen when rendering in ray tracing. The photon mapping radiance estimate for a point on a surface may not be the correct value because it suffers from some low-frequency noise. This isn't really a problem because the correct result can be found by using more photons. The result is a slow-changing value that is consistent in intensities. This is as opposed to ray tracing, which suffers from high-frequency estimates seen as noise.

The two passes of photon mapping are as follows:

  • Photon tracing: Build the photon map structure by tracing rays from the light sources to each surface in the scene. This pass captures the light as it arrives at surfaces in the scene.

  • Photon rendering: Render the scene using the information stored in the photon map to generate the final image. This pass basically captures the light as it leaves each surface in the scene.


Summary

By now you should be able to write your own photon mapping application (just kidding). But yes, this chapter has covered the general concept of pho-ton mapping. It's not a hard idea to grasp and I'm sure most of you have an idea what it is by now. The photon map can be visualized as a list of predetermined paths calculated and saved from the light sources to the surfaces in the scene as well as from the observer to the objects in the scene. Meaning the photon map is used twiceonce to scatter the light and once to find the reflected lighting on surfaces. The photon map represents the incoming flux of each surface in the scene. The photon mapping process is conducted in two major stages. The first stage to build the photon map is by tracing rays from the light sources to the surfaces in the scene. The next stage is to perceive the scene as the observer by tracing rays from the view plane into the scene to find the radiance of each pixel on the view plane. The next chapters take a detailed look at photon tracing and photon rendering.


Chapter 11. Photon Tracing

This chapter shows you how to implement photon tracing and discusses in further detail each section of the algorithm. The algorithm can be broken into two simple procedures, each of which is covered in detail in this chap-ter. You'll develop both of these procedures and learn about a more advanced scattering technique. Then, when you've completed building the photon-map-ping components, you'll incorporate them into the cSceneclass. You're going to build the photon map and then process the intersections based on a conditional state. By the time this chapter is over, you'll understand better how to scatter and store photons in the photon map using photon tracing.


Understanding the Structure of a Photon

In order to save the illumination separated from the geometry in the scene, the photon structure is required. The photon structure is stored on surfaces in the scene. Remember that photons represent incoming illumination from your light sources. Each photon tells you where the energy is flowing and carries a portion of the light source wattage.

Here is the structure of this simple photon:

typedef struct
{
   cVector3 tPosition;
   color3 tPower;
   cVector3 tDirection;

} photon_t;


The Photon-Mapping Algorithm's Two-Pass Method

The photon-mapping algorithm is conducted in two passes. The two passes are as follows:

  • Photon tracing: You build the photon map structure that captures the light as it arrives at surfaces in the scene. This is done by tracing rays from the light sources to each surface.

  • Photon rendering: You render the scene using the information stored in the photon map to generate the final image. In other words, you tell the program to capture light as it leaves each surface in the scene.

The first pass traces photons from the light sources as they intersect objects in the scene. Depending on the type of object encountered, the photon might not be stored. The camera isn't taken into account when scattering photons from the light sources. The second pass uses backward ray tracing with the photon map to render the reflected radiance of each point in the field of view of the camera projected on the image plane. The camera is used in the second pass to record the reflected light from each surface in the scene. This way, the process is a simple and efficient way to mix ray tracing (the old fundamental concepts) with photon mapping. Ideally, photon mapping is a much closer relative to ray tracing as apposed to radiosity. Figure 11.1 shows an example of the two-pass method.

Figure 11.1. The two-pass method renders images by scattering photons in the scene and then rendering the scene using the scattered photons.

The major step in implementing photon mapping is to break each section into its component form. Let's take a look at what you need to do in order to implement the algorithm:

  1. Release the photons from the light sources in the scene.

  2. Process the photons at the points of intersection.

  3. Store the photons in the photon map based on a conditional state.

  4. Render the scene using the photon map.

The sections that follow describe these steps in further detail.

Tracing the Photon: The First Pass of the Algorithm

Photon tracing involves tracing individual photons from each light source as they intersect with objects in the scene. As intersections are encountered, the photon map is constructed. Photons are created from light sources, as they are in the real world. A variety of light sources can be utilized for scattering photons, including point lights, directional lights, and area lights. But for the sake of simplicity, this example uses only diffuse point lights because they are very easy to implement.

When energy is released from a light source, trillions of small energy particles (photons) are emitted to nearby objects. As the light source emits pho-tons, each photon carries a small amount of energy from the light source. The wattage (or power) of the light source is divided among the number of emitted photons. The old concept of giving a light source a wattage value isn't used anymore as previously seen in the ray-tracing applications. As pho-tons move into different regions in space they transfer energy into these areas, making the areas brighter. See Figure 11.2.

Figure 11.2. Photon scattering is a more realistic simulation of light because it closely imitates nature as it distributes light from its source.

You must simulate this concept by shooting photons from the light sources, scattering them, and tracking their movements. The simulation method is almost the same as forward ray tracing or light tracing. Note that photons accumulate energy, whereas rays just calculate the radiance. By calculating the amount of flux in the scene, you can simulate a more global effect. Photons that leave the light sources and intersect objects in the scene are called global photons. Because of photons being stored globally it would make sense to call the photon map the global photon map. Ray tracing is incomplete in the way it calculates lighting because it doesn't take other lighting effects such as indirect illumination into account when calculating the radiance. Because you are simulating natural light, you must tell the application how many photons to scatter in the scene. You must also scale down each photon's power emitted from each light source accordingly.


Designing the Photon Map Class

In order to scatter energy and manipulate photon movements, you must create a class where all photon mapping structures and functions reside. This class will be inherited into the cScene class after you are done constructing it. This is done to keep the internal photon mapping procedures as independent from the cScene class as possible. As you'll see, you'll need a few methods and objects from the cScene. Let's begin by designing the class.

The data members of the photon mapping class are composed of the following:

  • A pointer to the global photon map array called pGlobalPhoton.

  • A variable to count the number of current photons in the global pho-ton map called lNumGlobalPhotons.

The member methods of the photon mapping class are composed of the fol-lowing:

  • The photon mapping constructor is used to reset variables and null pointers.

  • The photon mapping destructor is used to unload memory.

  • The AddToPhotonList() method is used to add photons to the photon map.

Let's take a look at the class:

class PhotonMap
{
public:
   long       lNumGlobalPhotons;
   photon_t * pGlobalPhoton;

public:
   PhotonMap();
   ~PhotonMap();

   void   AddToPhotonList(photon_t **, long *, photon_t);
};

Now you can inherit the PhotonMap class into the cScene class.

class cScene: public PhotonMap
{

  // .. rest of code assumed
}

When the cScene constructor is executed, it will call the photon-mapping constructor.

cScene::cScene()
{
   // Rest of code is assumed…
   // photon mapping constructor
   PhotonMap::PhotonMap();
}

You now need to define the inner workings of the constructor and destructor, as well as create a method that will add photons to the photon map.

Defining the Constructor

The class constructor initializes the number of photons to zero and sets the photon map array to NULL. No photons are currently saved in the photon map array.

PhotonMap::PhotonMap()
{
   lNumGlobalPhotons = 0;
   pGlobalPhoton        = NULL;
}

Defining the Destructor

The scene destructor releases any allocated memory and sets the array to NULL.

PhotonMap::~PhotonMap()
{
    if (pGlobalPhoton != NULL)
       delete [] pGlobalPhoton;

    pGlobalPhoton = NULL;
}

NOTE

NOTE

The AddToPhotonList() method reallocates an additional 1000 pho-tons at a time whenever the photon list is full.

Adding Photons to the Photon Map

The AddToPhotonList() method will add photons to the photon map (which is just an array). It will allocate and reallocate data if required. The number of photons that are allocated at each interval is 1000.

// 1000 photons at a time to allocate
#define PHOTON_REALLOC_JUMP             1000

void PhotonMap::AddToPhotonList(
              photon_t ** pList,
              long *lCount,
              photon_t tPhoton)
{
   int cnt = 0;
   if ((*lCount % PHOTON_REALLOC_JUMP) == 0)
   {
      cnt = ((*lCount/PHOTON_REALLOC_JUMP)+1) * PHOTON_REALLOC_JUMP;

      *pList = (photon_t *) realloc(*pList,sizeof(photon_t) * cnt);
   }
   (*pList)[(*lCount)++] = tPhoton;
}

You now have a class that includes a photon map that can dynamically allocate photons by using the AddToPhotonList() method. The next step on your way to implementing the algorithm is to scatter photons from the light sources. Because you've been using a ray structure to track the movements of light from one point to another, it would be wise to use it for photon movements as well. This way, you don't have to rewrite any new code for casting photons into the scene and testing for object intersections. You will upgrade the PhotonMap class throughout this chapter as you learn more about the algorithm.


Incorporating Simple Scattering

Developing an algorithm that scatters photons from a diffuse point light is your primary goal, and there are many ways to do it. One simple algorithm for scattering photons from a light source uses a center point with a unit radius and randomly generates outgoing vectors around the unit sphere. See Figure 11.3 for an example of simple scattering. The center point is the light source position.

Figure 11.3. Simple scattering uses a center point that generates random vectors in outgoing directions around a unit sphere.

NOTE

NOTE

There are a variety of light-scatter-ing algorithms available,and you can even implement your own.

This causes photons to be equally distributed outward from the light source. The power of each photon is randomly generated and should be scaled by the amount of emitted photons. This concept is very similar to how energy is released from a light bulb. Let's take a look at the logic required to scatter photons equally from a diffuse point of light.

Implementing a method to scatter photons from a diffuse point light is actually very simple. The first things you need to do are go through each light source and then emit n number of photons into the scene in random directions. This is done by emitting a photon using a random number for the individual X, Y, and Z components to find a random direction for the ray to travel. If an intersection has occurred in the scene, you process the photon and begin the whole procedure again until n photons are emitted from the light source. When you have completed emitting the desired photons from the light source, you scale each photon's power to a fraction of the light source total wattage. Now, let's take a look at the algorithmic description of simple scattering:

  1. For each light source in the scene.

  2. For the number of emitted photons from the light source.

  3. Pick a photon in a random direction and shoot it into the scene.

  4. Find the closest intersection.

  5. Process the photon at the point of intersection. This logic for the point of intersection is done by calling a special method called ProcessPhotonHit().

  6. After releasing all photons for the light source, scale each photon to (1 / emitted photons).

As you can see, the algorithm is straightforward. Let's implement the method to scatter photons in a uniform fashion.

Let's take a detailed look at the code required to scatter photons in a uniform fashion. Two variables are created; one to count the number of emitted photons and the other to count the number of intersected photons. The maximum number of photons that are released from a light source is set up by the application. The random x, y, and z components of the direction vec-tor are in a range of 1 and 1. Remember that the unit vector randomly points in some direction in the unit sphere.

void Create_Photons_For_Light (cLight light)
{
   int nShot = 0,
       nHit  = 0;

   float   x,y,z;
   cRay    ray;
cVector3 vHit, vNormal;
int iClosestObject;

// shoot random directions
while(nShot < max_photons){

do {
  x = (-1 + 2*(float)rand() / (float)RAND_MAX);
  y = (-1 + 2*(float)rand() / (float)RAND_MAX);
  z = (-1 + 2*(float)rand() / (float)RAND_MAX);

} while ( x * x + y * y + z * z > 1);

Setting the Direction

The next step after you generate a random vector is to set up a photon and cast it into the scene. You do this using the ray structure. The ray's origin is the light source position and the ray's direction is the random vector. The ray is normalized and is ready to be launched into the scene.

// set up ray ( light -> random dir)
ray.origin = light.vPosition;
    ray.direction = cVector3(x, y, z);
ray.direction.Normalize();

Finding the Closest Intersection

The ray is cast into the scene by calling the Find_Closest_Object() method. If the ray has intersected a point on an object in the scene, the intersection point and the object index are returned.

// find closest object
 iClosestObject = Find_Closest_Object(ray, &vHit);

Getting the Point Normal

The object normal is referenced for later use when processing photon intersections.

// get object normal
vNormal          = pObjectList[iClosestObject].Normal(vHit);

If an intersection is found between the light source and an object in the scene, the photon is set to the total wattage of the light source. The hit counter is updated and the ProcessPhotonHit() method should be called to compute the photon's behavior. The emitted counter is updated and the process begins again.

 // if object is in the local array list
 if( iClosestObject > -1 )
 {
  cout << "A photon has hit object:" << iClosestObject << endl;

 // local photon
  cPhoton p;

 // strength of light source
 float e = light.fWattage;
 e  *= max_photons;

 // attach it to photon
 p.Power = color3(e, e, e);

 // process photon hit

    TODO: Call Process Photon Hit Function
    // ProcessPhotonHit();
    nHit++;
 }
  nShot++;
}

NOTE

TIP

Remember each photon released from a light source must be scaled to carry a fraction of the emitted energy.

Scaling Each Photon's Power

The final step after all photons have been emitted from the light source is to give each photon a portion of the light source wattage. If you recall, you previously gave each photon the full wattage of the light source. In order to scale each photon appropriately, you use the following formula.

Photon Power = Light Source Wattage (1.0 full) / emitted photons

The following code shows how to do this:

  // scale power of stored photons with 1/emitted photons
  float scale = 1.0f/(float)(nShot);

  for(int i=0; i < lNumGlobalPhotons; i++)
          pGlobalPhoton[i].Power *= scale ;
}

NOTE

The Problem with Simple Scattering

The problem with this simple scattering algorithm is that it randomly shoots photons in many directions.Scenes that have objects that are spaced apart may not receive enough photons using this technique.This algorithm works great using the Cornell box (the inside of a cube with one light and one face missing),but suffers if the objects are separated.Earlier tests involved shooting 1000 pho-tons towards two spheres spaced apart.They resulted in each sphere only receiving 30 hits.Too many wasted photons are created that do nothing but consume processor cycles. Rendered images look blotchy and ugly because there are too few photons intersecting each object.I had to increase the number of photons emitted for my light sources from 1000 to 100,000 just to accomplish reasonable results which resulted in a lot of wasted pho-tons,processing cycles,and time.This was unacceptable so I had to find another way of distributing photons towards objects in the scene with less work for the processor.I've decided to move away from this method and implement a smarter method of scattering photons.


Incorporating Proportional Light Scattering

A better technique for light scattering utilizes a smarter method of distributing photons from a diffuse point light. The algorithm shoots a fixed number of photons in a spiral fashion from a light source towards each object in the scene. This means that instead of releasing 1000 photons from a light source, 1000 photons would be shot towards each object in the scene.

The light source wattage is found by the following formula:

Emitted Photons = Targeted Photons Number of objects in Scene

As you can see, you are going to be selective about how you release photons from the light sources. I choose not to calculate random photons that go into space and do nothing. This way each object is targeted a baseline number of photons. Targeting photons towards objects is a better method of distributing photonsit saves time and memory.

Upgrading the PhotonMap Class

A variable is required to count the number of photons that are targeted towards each object in the scene. You need to add this variable to the PhotonMap class.

long      lMaxPhotonPerObject;

You also need to implement an algorithm to scatter photons in a proportional manner. The notion of releasing individual photons from a light source is the same as in the simple scattering technique but changes because these photons are unequally distributed and proportionally targeted towards objects in the scene. See Figure 11.4 for an example.

Figure 11.4. An example of shooting photons proportionally at the objects in the scene from a light source.

One problem with this is that the equal emission concept of energy being released from a light source just went out the window. This is because the light source doesn't give off equal energy (1/emitted) for each emitted pho-ton anymore. This means that wasted photons that go into space and do nothing are not calculated anymore. You calculate only the energy that is proportional to the surface. This requires a new method of giving each pho-ton a portion of the light source wattage.

In order to simulate photons being released from a light source and proportionally target them towards objects in the scene, you need a local coordinate system. The local coordinate system guides the photons to their destinations from the light sources in the scene. The local coordinate system is very similar to the camera coordinate system developed earlier in the book. The process of shooting rays from the camera towards objects in the scene can be adapted to shootings rays from a light source to the objects in the scene. This means that same local X, Y, and Z axes must be evaluated. Consider a simple example first.

Pretend a polygon is sitting 30 units into the screen and is lying flat on the X and Y world axes. The light is located at (0,0,-5) and the observer is at (0,0,-10). See Figure 11.5 for an example.

Figure 11.5. This polygon example involves a polygon sitting in front of the light and the observer.

You now need to create a local coordinate system to scatter photons from the light source to the polygon. You need to find the LOOK and UP vectors. You don't have to search for the RIGHT vector because you can easily find it using the LOOK and UP vectors.

To find the LOOKvector between the light and the polygon, you simply subtract the center point of the polygon from the light source position. This creates the local Z axis for the local coordinate system. See Figure 11.6 for an example.

Figure 11.6. The Z axis of the local coordinate system is computed by subtracting the center point of the object from the light source position.

Adding the UP vector to the local coordinate system is not as easy as aligning it to the world Y axis. Remember this is a local coordinate system relative to the world coordinate system and can change dynamically if required. For example, the X and Y axes of the local coordinate system can be aligned with the world X and Z axes. Sometimes the UP vector can be rotated in a way where it's parallel to the world's X axes, like you learned in Chapter 8.

By looking at the individual X, Y, and Z components of the LOOK vector, you can determine where the UP vector is pointing.

If the absolute value of the X component is more or equal to 1.0 (which dictates the LOOK vector is parallel to the world X axis), the UP vector is parallel to the world's Y axis. See Figure 11.7 for an example.

Figure 11.7. The UP vector is set to the world Y axis (0,1,0) because the LOOK vec-tor's X component is more than or equal to 1.0.The LOOK vector is therefore parallel to the world X axis (1,0,0).

If the absolute value of the Z component is more than or equal to 1.0 (which dictates that the LOOK vector is parallel with the world Z axis), the UP vector is still parallel to the world's Y axis. If the X and Z components are not 1.0, on the other hand, the UP vector is parallel to the world X axis.

By having the LOOK and UP vectors, you can easily find the local X, Y, and Z axes of the local coordinate system, as follows:

  • You can find the Z axis (or LOOK vector) of the local coordinate system by subtracting the center point of an object from light source position.

  • You can find the X axis (or RIGHT vector) by crossing the LOOK vector with the UP vector.

  • To find the Y axis, you simply cross the X axis with the LOOK vector.

This creates a local coordinate system that is orthogonal; these local X, Y, and Z axes vectors are 90 degrees apart. Let's write a method that creates the local coordinate system between the light source and the center point of an object. See Figure 11.8 for an example of the local coordinate system.

Figure 11.8. The local coordinate system is used to shoot pho-tons from the light source to an object in the scene.

NOTE

NOTE

The MakeXY() method uses the LOOK and UP vectors to create a local coordinate system.The ray travels from a light source to a surface in the scene.

Calculating the X and Y Axes

The MakeXY() method is used to calculate the remaining two axes of the local coordinate system. The Z axis (LOOK vector) is already calculated by subtracting the center point of the object from the light source position. The LOOK vector is passed as V. The other two pointers (vX and vY) will be the local X and Y axes after the method calculates them.

#define TINY (1.0e-10) // 1.0000000000

void PhotonMap::MakeXY( cVector3 *vX, cVector3 *vY, cVector3 V )
{
 cVector3 U; // UP Vector
 V.Normalize();

 if( fabs(V.x) > TINY || fabs(V.z) > TINY )
 {
   U.x = 0.0;
    U.y = 1.0;
    U.z = 0.0;
  }
  else
  {
    U.x = 1.0;
    U.y = 0.0;
    U.z = 0.0;
  }

  *vX = (V   ^ U); // local X Axis
  vX->Normalize();

  *vY = (*vX ^ V); // local Y Axis
  vY->Normalize();
}

The previous method enables you to begin spawning photons from the light source towards objects in the scene. The origin of each ray travels along the local Z axis, intersecting points on the local X and Y axes where the object sits. This is wonderful because you need to randomly shoot photons and calculate intersections on the object of interest along the local X and Y axes.

The scattering algorithm utilizes the center point and the bounding sphere of an object to create random photons along a disk. The photons will leave the light source, targeting random points around the center point of the object. Consider an example of spiraling photons towards a disk. The pho-tons will originate from the local Z axis (light source to center point), targeting random (x, y) locations on the X and Y axes of the local coordinate system. The spiral fashion of photons being targeted towards objects results in a higher number of intersections. This in turn limits the work to only where photons intersect objects.

This process is like shining a flashlight on an object. The greater the distance between you and the object, the dimmer the illumination on the object. If you shine the flashlight very close to the object, you can see the object lit up more brightly with greater detail. More photons are gathered on a surface, thus generating a more detailed image. Because a scene can have n amount of lights that emit n amount of photons per object, you need to somehow scale each photon down to the light source wattage.

The incoming illumination towards an object is what you'll use to scale a photon. Each photon will carry a portion of the total incoming illumination for the object. The formula for doing this is straightforward:

Photon Power = Area of the Object / Incoming Photons per Object

NOTE

TIP

Scattering photons proportionally towards objects in the scene increases efficiency for photon mapping because it guarantees that each object receives a minimum amount of incoming illumination.

The logic of proportionally shooting photons towards objects involves choosing each light source and then cycling through each object in the scene. The next step is to shoot n amount of photons towards each object from the light source. The photons will be shot in a spiral fashion towards each object's center point. This is where the local coordinate system will be utilized. For each targeted photon, its power is scaled by the area of the object divided by the number of shot photons to the object. Now if an intersection has occurred the behavior of the photon must be handled. Of course, this will be handled in ProcessPhotonHit(). Now, let's take a look at the algorithmic description of proportional scattering:

  1. For each light source in the scene.

  2. For each object in the scene.

  3. For n amount of photons to shoot at object.

  4. Shoot Spiral Photon towards Object Center.

  5. Scale Photon Power by (Object Area / n amount of photons shot).

  6. Shoot Photon in Scene.

  7. Find the closest intersection.

  8. Process the photon at the point of intersection. This logic for the point of intersection is done by calling a special method called ProcessPhotonHit().

As you can see the algorithm is not very complicated. However, the algorithm is a bit cumbersome, which means it might be a bit slow. The method you'll use to create photons that proportionally scatter towards objects in the scene will be located in the cScene class. You need to utilize the intersection method in the cScene class. The method's purpose is to distribute photons in a proportional manner towards objects in the scene. It will use less time and memory as compared to the simple scattering algorithm. The method will cycle through each light object in the cScene array and shoot n amount of photons towards each object in the scene in a spiral fashion. The result of using a smarter way to scatter photons is more intersections encountered with less processing time.

void cScene::CreatePhotons() {

Using the Many Local Variables

The following function utilizes a variety of local variables. The local variables are going to be used for the scattering algorithm. A few counter variables are created for array loops. Variables are created to hold a photon's power, its direction, and its destination points. A series of small variables construct a disk that will be created on the surface of the object of interest. A few statistic variables are used to inform the user of what's going on in the scattering process. These are variables used to fire, count, translate, and scale photons. A for loop begins looping through each light in the scene; another loop then cycles through each object in the scene for each light.

int      i,j,k;
cLight   tLight;
cVector3 tObjCenter;
float    dObjRadius;

cVector3 tToCenter;
cVector3 tX, tY;

float    Power, x, y, dx, dy;
int       ix, iy, nx, ny;

cVector3 tDisk;
cVector3 tPhotonDest, tPhotonDir;
cVector3 tInter;
int iHitIndex;
long lFired, lHits;
photon_t tPhoton;

for (i = 0; i <  this->nLightCount;  i++)
{
   tLight = pLightList [i];
   for (j = 0; j < this->nObjectCount; j++)
   {

Referencing the Object Radius

A few variables are used to count the amount of shot photons from the amount of intersected photons. The object center and radius are referenced as well as the distance from the object to the light source.

lFired = lHits = 0;

tObjCenter = pObjectList[j].ObjectCenter();
dObjRadius = pObjectList[j].ObjectRadius(tObjCenter) * 1.1f;
tToCenter  = tObjCenter - tLight.vPosition;

Calculating the X and Y Axes

The LOOKvector is passed into the MakeXY()method. The method will now calculate the orthogonal X and Y axes vectors and save them to the passed pointers.

MakeXY(&tX,&tY,  (tLight.vPosition - tObjCenter)   );

Scaling Each Photon's Power

The power of a photon has changed from 1/number of emitted photons to the Area of the Object / Incoming Photons per Object. The area of the object is found by PI * Radius * Radius. The power of each photon (or the irradiance) is the area of the object divided by the emitted photons from the light source towards the object. This is the amount of incoming flux targeted towards the object where each photon carries a portion.

Power = (3.1415926f * dObjRadius * dObjRadius)
                                  / lMaxPhotonPerObject;

The Spiral Distribution Algorithm

The spiral distribution algorithm is an elegant method used to shoot photons in a spiral pattern around the center point of the object. See Figure 11.9 for an example of targeting photons. A simple geometric disk is used. After an intersection is found on the disk, the ray's destination subtracted from the ray origin (light source) yields the ray's direction.

Figure 11.9. The generated random photons are stored around a center point by using the spiral distribution algorithm.

nx = ny = (int) sqrt(lMaxPhotonPerObject);
      ix = iy = 0;
dx = (2.0f * dObjRadius) / (float) nx;
dy = (2.0f * dObjRadius) / (float) ny;

x  = - dObjRadius;
y  = - dObjRadius;

 for (k = 0; k < lMaxPhotonPerObject; k++)
 {
     tDisk = ( (tX* (float) x) + (tY* (float) y) );

   x += dx;
   ix ++;
   if (ix > nx)
   {
      ix = 0;
      x = - dObjRadius;
      y += dy;
      iy ++;
      if (iy >= ny)
         break;
   }

   tPhotonDest = tObjCenter + tDisk;
   tPhotonDir = tPhotonDest - tLight.vPosition;
   tPhotonDir.Normalize();

Setting the Photon's Direction

The photon is set up and is ready to be cast into the scene. The photon's origin begins from the light source and its direction is targeted towards the object.

cRay Bullet;
Bullet.origin   = tLight.vPosition;
Bullet.direction = tPhotonDir;

Finding the Closest Intersection

If the photon intersected an object in the scene, the intersection point and the object index are returned. If an object was found during the intersection process, the power of the photon is set to scaled power using the previous formula. The scaled power is not the only component applied to the power of the photon because some light sources emit a specific color (ever been to a dance club?). Surely you don't think all light sources emit only white light, so coloring each emitted photon based on the light source color is a great idea which I also incorporated into the photon power. The photon is now ready to be processed. (The ProcessPhotonHit() method hasn't been explained yet so don't worry about it just yet.)

         iHitIndex = Find_Closest_Object(Bullet, &tInter );

         lFired++;
         if (iHitIndex > -1 && (iHitIndex == j))

         {
              lHits++;

             // scale photon by light source times power
             tPhoton.tPower = tLight.Color
                        *   (float) Power;
            // process photon
            ProcessPhotonHit (
                       iHitIndex,
                       tPhotonDir,
                       tInter,
                       tPhoton,
                       MAX_TRACE_DEPTH,
                       false);

         } // end if hit
      } // end n photons per object

         fprintf(stderr,"To Object %d: %d fired,
                  %d hit\n",j,lFired,lHits);
      } // end for each object
     } // end for ea
} // end of  method

Finally, you can scatter photons and test for point intersections on objects using your local coordinate system. The next step is to store the photon in the photon map based on the object's material properties. Determining a photon's behavior is the next subject at hand.


Storing Your Photons

You should now understand the nature of how photons move, but you haven't learned what the effect is after the photons interact with a surface. The nature of a photon after it has intersected an object must be simulated. Simulating the flow of light as it intersects objects using a series of photons is based on the object's material properties. The whole process can be broken down into two simple categories. One of two things can happen:

  • The photon can be absorbed into the object as heat or kinetic energy.

  • The photon can be reflected (or transmitted) from the object.

The absorption of photons is a very simple notion and can be easily simulated. But the types of reflection can be broken into a few functions for diffuse and specular surfaces. The behavior of the photon is determined by the type of material the photon encounters. Depending on the type of surface encountered, the specific reflection/transmission code is executed. It is very important to remember that a photon interaction with a material is very different from a ray interaction (as in ray tracing). Photons represent incoming illumination (flux) in the scene. Let's cover the procedures and rules necessary to construct the photon map.

First, photons are stored only when they hit diffuse surfaces. The reason why photons are not stored when they hit specular surfaces is because the storing of photons on specular surfaces is really a waste of memory and doesn't provide any important information. For example, a mirror would pose a problem for specular reflections using photon mapping because perfect specular reflections require a tremendous amount of photons to reflect the image in the opposite direction. Ray tracing does a better job at specular reflections in the mirror direction and it should be used for this purpose. When a photon originates from a light source, it can bounce from object to object diffusively and specularly. This means that the same photon can be stored in the photon map several times along its path if it encounters diffuse surfaces. This process is what builds the photon map.

The illumination information is stored separately from the geometry using the photon map, which is much better than radiosity meshing and its inherent memory constraints.

Note that the energy coming from the light sources is recorded by each pho-ton in the photon map. The photon map represents the direct and indirect illumination. It also represents a special lighting effect called caustics, covered later in this chapter. The more photons that propagate into a given region, the higher amount of strong incoming energy. The nature of a photon that interacts with a point on a surface in the scene can be one of four possibilities, described as follows:

  • When a photon hits a diffuse surface, you store the photon's incident direction and energy in the global photon map. The photon map is constructed only by diffuse interactions.

  • When a photon hits a specular surface, you do nothing to the global photon map but reflect the photon either in two ways. Depending on the opacity of the object, either mirror reflection or refraction (transmission) is simulated.

  • If the surface is partially diffuse and specular, you must use a method to randomly determine which component of the material carries higher weight. This is a simple method of proportionally and randomly choosing a probable outcome.

  • If the photon hits the object and somehow gets absorbed into the object, you store the photon in the photon map and end the photon's life.

You have to carefully plan and simulate these absorption and reflection/transmission factors of light. The next section takes a closer look at each effect that should be simulated when a photon interacts with a point on an object in the scene.

NOTE

TIP

It's very important to remember that when a photon intersects a point on an object,one of two things generally happens.The photon can be absorbed or reflected/transmitted.


Absorbing Light

If an object is opaque, it absorbs most of the incident light. This means that you must simulate this concept using photon mapping of light being absorbed. Light is absorbed mostly on diffuse surfaces. Sometimes when light hits an object, it is randomly absorbed. The random absorption of light in an object can become a very complex task because this is a factor of an object's molecular structure. In nature, light scatters in an object and reflects many times before it comes back out from some arbitrary direction. See Figure 11.10, which shows two photons. P1 depicts a photon released from the light source and sitting at the point of intersection. P2 shows the same photon randomly positioned because of the object's molecular structure.

Figure 11.10. Absorption of a photon released from a light source interacting with an object.

The light that is reflected outside the object can end up in some mysterious places you would never expect, and deriving an efficient rendering algorithm poses a series of problems. For this reason light is absorbed at the point of intersection. Recall that when a photon hits an opaque object and somehow gets randomly absorbed, the photon is recorded at the point of intersection and doesn't travel any longer. The photon's life is ended. How opaque the object appears must be taken into consideration when absorbing a photon. This means that when storing photons for absorption in the photon map, an object's opacity must be incorporated.


Transmitting and Reflecting Light

The transmission and reflection of light to other surfaces can be broken into two simple categories, specular and diffuse. The movement of light to other surfaces in a scene from the point of intersection can be random, bent, or straight. In the case of photons, the power of a mirror-reflected photon is scaled by the material's specular coefficient. The transmission of light through the object is scaled by the object's opacity. The diffuse interaction and reflection of light are based on the material's diffuse coefficient. The next section takes a closer look at the reflection/transmission effects that must be simulated using photon tracing.

Diffuse Interaction and Reflection

Diffuse interaction and reflection are induced by a material's diffuse coefficient. Diffuse interaction and reflection is responsible for most of the light you observe with your eyes. When a photon interacts with a diffuse surface, it is stored in the photon map at the point of intersection and is reflected as well. Because diffuse light can randomly scatter to nearby objects in the scene, as in radiosity, you have to randomly reflect diffuse photons. Because it's a reflection effect, a new photon is yielded to randomly reflect the light in some particular direction. The direction of the reflected photon can be arbitrary from the point of intersection. You can determine the power of the new reflected pho-ton by scaling the incoming photon with the diffuse reflectance coefficient of the material. The code for performing diffuse reflection is from Chapter 3.

Specular Reflection

Specular reflections are induced by a material's specular coefficient. If a pho- ton interacts with a specular surface, one of two things can happen:

  • The photon can be reflected in the mirror direction.

  • The photon can refract in or out of the object.

Mirror Reflections

When light is incident on a perfect specular surface such as a mirror it is reflected in the opposite direction. This means that a new photon should be reflected the same way. See Figure 11.11 for an example mirror reflections.

Figure 11.11. An example of specular (or mirror) reflection of a photon interacting a surface.

The formula for perfect specular reflection is used as in ray tracing. But instead of reflecting a ray in the opposite direction, a reflected photon is traced. The specular coefficient must also be taken into account when specu-larly reflecting a photon. If the surface were a mirror, the outgoing photon is equal to the same power of the incoming photon because the surface has very few imperfections. Recall that photons are not stored when they encounter specular surfaces; photons are only stored on diffuse surfaces. A photon can be specularly reflected multiple times by specular surfaces but is only recorded in the photon map when it finally intersects a diffuse surface.

Refraction

Recall that objects that are transparent allow light to pass (or transmit) through them. When light changes between two mediums, the speed of the light normally slows. It may also bend in order to travel to its destination. Figure 11.12 shows an example of refraction. In the case of light (a series of photons) changing mediums (such as from air into the object), the photons will travel through the object and will bend to an angle specific to the object. The new location of a refracted photon is based on the index of refraction.

Figure 11.12. Refraction of a photon changing mediums as it leaves air and travels into an object and then back out from some arbitrary direction.

NOTE

NOTE

All the effects such as diffuse interaction,diffuse reflection,specular mirror reflection,and refraction are available in photon mapping.


Playing Russian Roulette

You must now create the logic behind how a photon reacts to a surface. If the surface is mostly diffuse, diffuse reflection can be easily simulated. If the surface is mostly specular, specular reflections can be utilized. But what if the surface is partially diffuse and specular? How do you determine the nature of the photon in this particular instance?

Determining whether the surface is specular or diffuse is called Russian roulette. The term is exactly what it states. Putting a gun to your head and playing with the probability of it shooting or not is a very dangerous game. If a photon interacts with a surface in a particular instance, you must randomly yet proportionally select whether the photons get reflected/transmitted or absorbed. This is a factor of the diffuse and specular coefficients of the material and the chance of absorption. Deriving an algorithm to randomly yet proportionally select whether a photon is absorbed or reflected/transmitted (diffuse or specular) is required.

Let's take a look at how you randomly yet proportionally select diffuse, specu-lar, and absorbed photons. The maximum range of the reflection/transmis-sion (diffuse and specular) and absorption components must be more than 0.0 and less than or equal to 1.0. The Russian roulette formula is as follows:

(Diffuse + Specular + Absorption) > 0 and < = 1.0

Let's take a look at some instances of the behavior a photon takes when it interacts with a point in the scene.

  • If the specular component is 1.0 (or 100%), the diffuse component must be 0 and there is no chance of random absorption into the material.

  • If the diffuse component is 1.0 (or 100%), the specular component must be 0 and there is no chance of random absorption into the material.

  • If the diffuse component is 0.4 (or 40%), the specular and absorption components can be the remaining 0.6 (or 60%) and there is a fairly good chance of random absorption into the material.

  • If the specular component is 0.2 (or 20%), the diffuse and absorption components can be the remaining 0.8 (or 80%) and there is a very good chance of random absorption into the material.

  • If the diffuse component is 0.25 (or 25%) and the specular component is 0.25 (or 25%), this means that the remaining 0.5 (or 50%) is the absorption component. There is an excellent chance of absorption into the material.

The application will determine the probability in each instance that a photon has intersected a point on an object (traced from the image plane). Let's cover the pseudocode on how to achieve this concept.

Interaction Logic

Implementing an algorithm to simulate Russian roulette on a photon that has intersected an object and is waiting to be handled can be a tricky task. The first thing you need to do in order to randomly yet proportionally select diffuse, specular, and absorbed photons is to use a random number generator. The random number generator generates a random number in a domain (0 to 1.0) that includes these three components. Now when a state is set for a specific photon from the random number generator, the specific logic for the type of lighting effect is executed. For example if the random state is diffuse, the diffuse lighting logic and code are executed.

The process to actually do this using C/C++ is very simple. All you need to do is randomly generate a number greater than 0.0 and less than or equal to 1.0.

The following code shows how to do this:

// get number between 0 and 1

float random = -1 + 2*(float)rand() / (float)RAND_MAX;
if(   random < 0  ) random          = -random;

The Diffuse, Specular, and Absorption Ranges

The three components share a domain. The diffuse component begins from zero to the diffuse factor of the material. The specular component begins from the diffuse factor and ends at the absorption factor of the material. The absorption factor is the remaining factor that begins from the specular factor and ends at one. The listing order of each component is listed as:

Diffuse Range   =
Begin: 0.0
             End: Diffuse Factor of Material
Specular Range  =
Begin: Diffuse Factor of Material
             End: Diffuse Factor of Material + Specular Factor of
Material
Absorption Range   =
Begin: Diffuse Factor of Material + Specular Factor of Material
              End: 1.0
The following code shows how to branch the logic:
   // if diffuse reflection
   if( random < pObject->fDiffuseFactor )
   {
        // TODO: Store photon in photon map
        // TODO: Add Diffuse Reflection Code
   }
   // if specular (mirror) reflection
   else if ( random >= pObject->fDiffuseFactor &&
      random < pObject->fDiffuseFactor + pObject->fSpecularFactor )
   {
       // NOTE: Do nothing to photon map
       // TODO: Add specular (mirror reflection or refraction) code
   }
   else
   {
     // TODO: Store photon in photon map, photon's life has ended
   }

Reporting Effects

Because the nature of a photon is based on a series of situations, it would be wise to track the lighting effects that are simulated in the photon-tracing process. If the light is absorbed, mirror reflected, refracted, and diffuse reflected then it is incremented by the application. In order to do this, four variables are required for each effect.

These variables are as follows:

long      nAbsorbed;
long      nRefracted;
long      nSpecularReflected;
long      nDiffuseReflected;

To utilize these counter variables, they must be placed at specific points in the tracing code and finally reported after the tracing process has commenced. These statistics are printed at the bottom of CreatePhotons() method.

void cScene::CreatePhotons()
{
   // Previous Code is Assumed…

   // Report Information
   fprintf(stderr,"\n[STATS]\nActual Stored Photons =
                    %d\n",lNumGlobalPhotons);
   fprintf(stderr,"Absorbed Hits: %d\n",            nAbsorbed);
   fprintf(stderr,"Specular Reflected hits: %d\n",  nSpecularReflected);
   fprintf(stderr,Diffuse Reflected hits: %d\n",   nDiffuseReflected);
   fprintf(stderr,"Refracted Hits: %d\n",           nRefracted);
   }

A Simple Example

Pretend you have an object that has a specular component of 0.2 (or 20%) and diffuse component of 0.5 (or 50%). This means the remaining absorption component is 0.3 (or 30%). Now you can shoot a few photons towards the object and see how the nature of the photon will behave based on the material's properties and the probabilistic solution. Let's see how the algorithm would perform on a few materials with specific types of factors assigned for the diffuse, specular, and absorbed components.

In the first instance, 0.3 is generated. This means that the photon is diffu-sively reflected and stored when the random number (0.3) is less than the diffuse component (0.5) or falls inside the diffuse range. This means that the photon should be stored in the global photon map and a new diffuse reflected photon should be launched. The logic is as follows:

Random Number is 0.3
// Meaning the photon is diffusively reflected and stored
If 0.3 is less than 0.5
   Store Photon in Global Photon Map
   Randomly Reflect Photon

In the second instance, 0.6 is generated. This means that the photon is spec-ularly reflected but not stored. In this case, the random number (0.6) is more than the diffuse component (0.5) and is less than the absorption (0.7) component, meaning it falls inside the specular range. This means that a new photon is to be launched in the mirror direction but the photon is not stored in the global photon map. The logic is as follows:

Random Number is 0.6
// Meaning the photon is specularly reflected but not stored
If 0.6 is less than ((D<0.5> + S<0.2>) = 0.7) and more than or equal to
D<0.5>
   Do Nothing to Global Photon  Map
   If Object is Transparent
Refract Photon between Two Mediums (air to water)
   Else
Perfectly reflect photon in mirror direction
   End If

In the third instance, 0.45 is generated. This means that the photon is diffu-sively reflected and stored. In this case, the random number (0.45) is less than the diffuse component (0.5) or falls inside the diffuse range. This means that the photon should be stored in the global photon map and a new diffuse reflected photon should be launched. The logic is as follows:

Random Number is 0.45
// Meaning the photon is diffusively reflected and stored
If 0.45 is less than 0.5
   Store Photon in Global Photon Map
   Randomly Reflect Photon

In the fourth instance, 0.9 is generated. In this case, the random number (0.9) is less than 1.0 and is more than the diffuse and specular ranges, which means it falls in the absorbed range. This means that the photon is absorbed and its life has ended. The logic is as follows:

Random Number is 0.9
// Meaning the photon is absorbed and it's life has ended
If 0.9 is more than or equal ((D<0.5> + S<0.2>) = 0.7) and less than or
equal 1.0
   Store Photon in Global Photon Map

As you can see, the algorithm focuses on the component that carries the highest weight of the three. The range of each component is as follows:

  • From 0 to 50% is the diffuse component.

  • From (50% to 70%) or 20% is the specular component.

  • The remaining 30 % is the absorption component.

Using this method, how photons scatter is determined by the material type. The numbers that are randomly generated mean that 50% of them will be targeted towards diffuse reflections. This is a probabilistic method of efficiently choosing photons on different types of materials. Russian roulette is a very important way of distributing photons. If 50 photons were shot at the surface, 70% of them will be reflected either as diffuse or specular reflections. The remaining 30% will be terminated and absorbed into the object. This way, fewer photons are reflected, which simulates the concept of energy being turned into heat.

Using the Chapter's Source Code

You can now write a method that processes photons when object intersections occur. The method passes the object index, photon direction, intersection point, maximum trace depth, and a Boolean variable to scatter the light in or out of the object. The method's primary purpose is to scatter photons based on the probabilistic solution. Photons will either be absorbed or reflected/transmitted. You also want to add the rendering settings parameters so you can be selective about how you construct the photon map. As you build the ProcessPhotonHit() method, you therefore will also apply the rendering settings parameters to each section of the code.

void cScene::ProcessPhotonHit(
                 int iObjIndex,
                 cVector3 tPhotonDir,
                 cVector3
                 tInter,
                 photon_t tPhoton,
                 int depth,
                 BOOL insideobj)
{
   cVector3 tNormal;
   cVector3 tReflected,tRefracted;
   color3 tColor;
   int iRecurseIndex;
   cVector3 tRecurseInter;
   photon_t tRecursePhoton;
cObject *pObject;
photon_t tPhotonCopy;
cVector3 Dummy;

tPhotonCopy = tPhoton;
pObject     = &pObjectList[iObjIndex];
tNormal     = pObject->Normal( tInter );
tColor      = pObject->Color;

// MAX DEPTH
if ( depth <=0) return;
else depth;

// Do probability of how much energy should
// either be Reflecteed/Transmitted or Absorbed

// get number between 0 and 1
float random = -1 + 2*(float)rand() / (float)RAND_MAX;
if( random < 0 ) random = -random;

// if diffuse
if(  random < pObject->fDiffuseFactor )
{
   // absorbing photon ( diffuse interaction )
   if ( RenderSetting.use_diffuse_shading &&
       (pObject->fDiffuseFactor >  0.0) &&
       (pObject->fOpacity > 0.0))
   {
      this->nAbsorbed++;
      //fprintf(stderr,"[a]");

      tPhoton.tPosition = tInter;
      // photons are stored pointing opposite on the surface
      Dummy = tPhotonDir;
      Dummy.Invert();
      tPhoton.tDirection = Dummy;
      tPhoton.tPower = tPhoton.tPower * pObject->fOpacity;
      AddToPhotonList(&pGlobalPhoton,&lNumGlobalPhotons,tPhoton);
   }

   // diffuse reflection for photon
   if ( RenderSetting.use_diffuse_reflection &&
       (pObject->fDiffuseFactor > 0.0) &&(depth != 0))
   {
      tReflected = tPhotonDir.Diffuse_Reflection(tNormal);
            cRay Bullet;
            Bullet.origin      = tInter;
            Bullet.direction   = tReflected;

            iRecurseIndex = Find_Closest_Object
                     (Bullet, &tRecurseInter );

            if ( iRecurseIndex > -1 )
            {
               this->nDiffuseReflected++;
               //fprintf(stderr,"[d]");

               tRecursePhoton.tPower.r = tPhotonCopy.tPower.r *
                    pObject->Color.r * pObject->fDiffuseFactor ;
              tRecursePhoton.tPower.g = tPhotonCopy.tPower.g *
                 pObject->Color.g * pObject->fDiffuseFactor ;
            tRecursePhoton.tPower.b = tPhotonCopy.tPower.b *
                 pObject->Color.b * pObject->fDiffuseFactor ;
            ProcessPhotonHit( iRecurseIndex,
                                   tReflected,
                               tRecurseInter,
                               tRecursePhoton,
                                         depth,
                                    insideobj);
         }
      }
   }

   // if specular
   else if ( ( random >= pObject->fDiffuseFactor)
       && ( random < pObject->fDiffuseFactor+pObject->fSpecularFactor) )
   {
      // if object is transparent to some degree
      if ( RenderSetting.use_refraction &&
           (pObject->fOpacity < 1.0f) && (depth != 0))
      {
           // refracted hits
        if(insideobj == FALSE)
           tPhotonDir.Vector_Refraction(
                      tNormal, 1.003f,
pObjectList[iObjIndex].fIndex_Of_Refraction,
                      &tRefracted );
           else
           tPhotonDir.Vector_Refraction( tNormal,
                       pObjectList[iObjIndex].fIndex_Of_Refraction,
                      1.003f, &tRefracted );
            cRay Bullet;

            Bullet.origin    = tInter;
            Bullet.direction = tRefracted;

            iRecurseIndex = Find_Closest_Object
                         (Bullet, &tRecurseInter );

            if ( iRecurseIndex > -1 )
            {
               this->nRefracted++;
               //fprintf(stderr,"[r]");

               tRecursePhoton.tPower.r =
                    tPhotonCopy.tPower.r *
                    pObjectList[iObjIndex].Color.r *
                    1.0f - pObject->fOpacity;

               tRecursePhoton.tPower.g =
                   tPhotonCopy.tPower.g *
                   pObjectList[iObjIndex].Color.g *
                   1.0f - pObject->fOpacity;

               tRecursePhoton.tPower.b =
                   tPhotonCopy.tPower.b *
                   pObjectList[iObjIndex].Color.b *
                   1.0f - pObject->fOpacity;

               ProcessPhotonHit(
                             iRecurseIndex,
                                tRefracted,
                             tRecurseInter,
                             tRecursePhoton,
                                       depth,
                     (insideobj == TRUE) ? FALSE : TRUE);
             }
         }
         else
         {   Not You don't have to use photon mapping
          //or  specular mirror reflections. You should use
          //ray  tracing instead.
         // specular mirror  reflected
         if ( RenderSetting.use_specular_reflection &&
           (pObject->fSpecularFactor > 0.0) && (depth != 0))
         {
            tReflected = tPhotonDir.Vector_Reflection(tNormal);

            cRay Bullet;
            Bullet.origin = tInter;
            Bullet.direction = tReflected;

            iRecurseIndex =  Find_Closest_Object
                      (Bullet, &tRecurseInter );

            if ( iRecurseIndex > -1 )
            {
               this->nSpecularReflected++;
               //fprintf(stderr,"[s]");

               tRecursePhoton.tPower.r =
                      tPhotonCopy.tPower.r *
                      pObject->Color.r *
                      pObject->fSpecularFactor;

               tRecursePhoton.tPower.g =
                      tPhotonCopy.tPower.g *
                      pObject->Color.g *
                      pObject->fSpecularFactor;

               tRecursePhoton.tPower.b =
                    tPhotonCopy.tPower.b *
                   pObject->Color.b *
                   pObject->fSpecularFactor;

              ProcessPhotonHit(
                        iRecurseIndex,
                           tReflected,
                       tRecurseInter,
                       tRecursePhoton,
                      depth,insideobj);
             }
          }
       }
    }
    else
    {
    // absorbing photon and kill it
    if (  RenderSetting.use_diffuse_shading &&
        (pObject->fDiffuseFactor > 0.0) && (pObject->fOpacity > 0.0))
    {
       this->nAbsorbed++;
       //fprintf(stderr,"[a]");

       tPhoton.tPosition = tInter;

       // photons are stored pointing opposite on the surface
       Dummy = tPhotonDir;
       Dummy.Invert();

       tPhoton.tDirection = Dummy;
       tPhoton.tPower = tPhoton.tPower * pObject->fOpacity;
       AddToPhotonList(&pGlobalPhoton,&lNumGlobalPhotons,tPhoton);
    }
  }
}

Figure 11.13 attempts to convey the concept of photons being distributed and stored in the global photon map.

Figure 11.13. A left view of the room of photons stored in the global photon map.

By now you should have an idea of how photons are stored in the global pho-ton map. The behavior of photons that intersect objects in the scene is the result of a probabilistic solution. The photons that are stored at surfaces in the photon map represent the incoming energy from the light sources. The next process is to add the observer into the algorithm. The observer is going to perceive the reflected radiance of each point on a surface. Calculating how much light is leaving in conjunction with how much light is arriving at each point on every surface in the observer's field of view is the final part of the photon-mapping two-pass process.


Summary

This chapter provided a detailed overview about photon tracing. Photon tracing is the process of constructing a photon map of light paths taken from the light source to objects in the scene. The chapter explained the overall process required to implement photon tracing. You started learning about simple scattering and then moved over to proportional scattering because it's a better approach to getting quicker results with less processing time. You also learned about the nature of a photon as it intersects different types of materials in the scene. You now understand what happens to the photon when it hits specular and diffuse surfaces and now know that photons can be absorbed into objects. You've learned that the physical outcome of a photon on a partial diffuse and specular surface is based on a probabilistic solution. In the next chapter, you'll complete the second part of the photon-mapping algorithm.


Chapter 12. Photon Rendering

This chapter will guide you to complete the implementation of the pho-ton-mapping algorithm. The photon-rendering process is the second pass of the two-pass technique. The last chapter showed you how to trace photons and determine the behavior of photons based on a probabilistic solution. This chapter focuses on how to render the reflected radiance in conjunction with the photon map in order to render an image using photon mapping. You'll build a few more structures and methods for searching for photons in the photon map. When you're done, you'll inherit everything in the cScene class. The cScene class will then be capable of rendering images using ray tracing and photon mapping. The photon-mapping technique becomes simple once you understand the procedures necessary for implementing the algorithm.


Implementing Photon Mapping

The major step when implementing photon mapping is to break each section into a component form. Let's take a look at what you need to do in order to implement the algorithm:

  1. Release photons from the light sources in the scene.

  2. Process the photons at the points of intersection.

  3. Store the photons in the photon map based on a conditional state.

  4. Render the scene using the photon map.

The Second Pass of the Photon-Mapping Process

The second pass of the photon mapping process is where the final image is rendered. The rendering process calculates the reflected radiance of each point on a surface using the photon map. The camera or observer is taken into account here. The process that's used is the same as in backward ray tracing.

You shoot rays from the view plane and find intersections; the point of interest is the point where the ray hits. Recall from Chapter 9 that you used the cosine shading method to calculate the radiance of a point projected on the image plane. In order to apply the irradiance (light to object) and the radiance (light to eye) to the point of interest, however, you must incorporate the photon map into the solution. Remember the photon map is a cache of light paths taken from the light sources to the objects in the scene but is then used again to find the reflected radiance of light leaving a specific area. This means that the photon map is used twice. You apply the irradiance to the point of interest by adding something called the radiance estimate.

NOTE

NOTE

The photon rendering process uses backward ray tracing to render the final image to the image plane using photon mapping.

The radiance estimate finds the closest photons around the point of intersection (irradiance) and calculates the reflected radiance for the point of interest using these photons. These are neighboring photons that are in close vicinity to each other. The amount of neighboring photons that are returned is a factor of a pre-defined distance. One photon by itself cannot determine the radiance of a point in 3D space, but grouping a set of photons together and finding the average in a small area yields a good estimation of how much light is arriving and leaving the point of interest. See Figure 12.1.

Figure 12.1. The closest photons around the point of intersection are used to determine the amount of light arriving and leaving a specific area on a surface.

You gather the neighboring photons at the point of intersection by locating the plane the point sits upon. The tangent plane for a point on a sphere is along its curve, whereas the tangent plane for a flat surface (such as a triangle or polygon) is the surface normal. You need to search for the closest pho-tons that sit in the tangent plane of the point. This is how you'll match the illumination information with the geometry of the scene. You'll use the pho-tons sitting across the surfaces in the scene when rendering using the photon map. See Figure12.2.

Figure 12.2. The more pho-tons you search for,the better the radiance estimation for each point on a surface.

So how do you find the closest neighbors? The process is very simple. If you think about it for a moment, all you need to do is use a geometric volume such as a cube or a sphere to encompass the closest photons around the point of intersection. See Figure 12.3. You can use other volumes as well, such as cylinders or pyramids. Choosing the ideal bounding volume is a choice of selection, meaning you can increase better accuracy and speed for photons that are returned.

Figure 12.3. A bounding volume such as a cube or sphere is used to encompass the closest photons around the point of intersection.

The examples in this chapter use a sphere to find the radiance estimate for every point on a surface. However, the sphere that you initially start out with is going to turn into a circle. To find the area of a circle, you simply multiply pi by the radius squared. The area of the circle is the number of squared units that make up the circle.

Photon Mapping Example

Now let's get back to the example of the polygon sitting 30 units in the screen. You are going to shade this polygon using the radiance estimate. The observer is located at (0,0,-10); the light source is located at (0,0,-5). See Figure 12.4.

Figure 12.4. This example scene shows an observer located behind the light source.The observer is in front of the polygon sitting 30 units into the screen.

You might be wondering why a circle is being used, because a circle represents 2D, rather than 3D, space. Before I address this issue, I must propose a few more examples. Remember, the objective is to shade every point for every object in the observer's field of view using photon mapping. In the polygon example's case, you cast rays using the local coordinate system towards the polygon. See Figure 12.5.

Figure 12.5. Photon tracing begins by shooting photons to the object in the form of rays from the light source in a spiral fashion.

The polygon's material diffuse coefficient is 1.0. This means that most of the photons that intersect the object will randomly reflect photons in arbitrary directions as well as save local copies in the photon map at the points of intersection. See Figure 12.6.

Figure 12.6. The pho-tons are shot from the light source and are stored and reflected (dif-fusively) on the object surface at different points of intersection.

To render the polygon projected on the image plane, you must find the radiance estimate for each point on the polygon. But before you can find the radiance estimate, you must find the closest photons around the point of intersection, also known as the photon contribution. This process is as follows:

  1. Place a sphere around the point of intersection.

  2. Expand the sphere until it includes enough photons as you initially requested. For example, if you searched for 50 photons, you expand the sphere until it returns 50 photons.

  3. Expand the sphere that's placed around the point of intersection until it returns with the farthest photon found.

NOTE

NOTE

Sometimes the initial request for the closest photons may return with fewer photons found if the photon map is small and the initial search request is greater than all the photons in the photon map.

The process of expanding a sphere around the point of intersection gives you a good estimation of the overall incoming energy (flux) at the surface. Figure 12.7 shows an expanding sphere.

Figure 12.7. The sphere expands based on the location of the farthest photon found.

So, why is the sphere going to turn into a circle? Recall that polygons and triangles are flat primitives. The points that make up these primitives lie within one tangent plane. A sphere is a little more complicated because of the large number of tangent planes. But this doesn't matter because the expanding sphere at the point of interest is infinitely small on the tangent plane. The unit sphere's infinite angles are controlled by 0 < a < 1. In the current example, the polygon is flat and the sphere is going to turn into a circle. The circle is what you're interested in. The radius of the sphere is from the center point of the circle to the farthest photon found. The diameter of the sphere is the radius squared. See Figure 12.8.

Figure 12.8. The sphere turns into a circle because the surface is flat.

By finding the closest set of photons around the point of interest, you'll get a pretty good estimation of the radiance estimate. The radiance estimate is determined by summing the closest photons found and dividing them by the area of the circle found. On large surfaces with slow changing lighting, some photons that lie in close vicinity at the surface are not very different in intensity. This means that when shading each point on the surface, the returned radiance estimate will be a slow changing value. This is a benefit when rendering because the final image will have very little noise.

Upgrading the PhotonMap Class

You can now add a variable to the PhotonMap class that counts the number of photons to search for when finding the radiance estimate:

long      lMaxPhotonNeighbors;

You can also write a set method to define the scattering and rendering parameters of photons in the PhotonMap class. The member variable lMaxPhotonPerObject is responsible for the maximum photons shot per object in the scene. The variable lMaxPhotonNeighbors is now responsible for finding the closest neighbors at the point of intersection. The code is as follows

void PhotonMap::SetupPhotonMapping(long max_photons, int max_friends)
{
   // max photons per object   lMaxPhotonPerObject = max_photons;

   // max neighbors to search for when finding the radiance estimate
   lMaxPhotonNeighbors = max_friends;
}

Creating a Photon List Structure

To implement the logic of finding the radiance estimate, you need to traverse the photon map array (or search the photon map) to find the closest set of photons for the point of intersection. In order to keep track of each photon that was found with its distance relative to the point of interest, you need a new structure that's to be used when searching for photons. This new structure is composed of the photon structure added to a distance scalar. The distance scalar determines how far each neighboring photon is away from the point of intersection.

typedef struct
{
   photon_t tPhoton;
   float dDistance;
} photonlist_t;

Finding the Closest Photons

You can now write a method that will search through your photon map and gather photons based on a pre-defined number. This method passes three parameters:

  • The first parameter (tList[]) is a storage array that holds the returned photons closest to the point of intersection.

  • The second parameter (lNumber_Of_Friends) is the number of neighboring photons to search for.

  • The third parameter (tInter) passes the intersection point from where all searching will originate.

The function searches through the photon map, finds the closest number of photons (based on their distance to the intersection point), and saves them in the storage array. The function finally returns with the number of photons found. When the function completes, the storage array is filled with the closest photons around the point of intersection.

The method can also list the photons in distance order. For example, the nearest photons to the point of intersection are listed first in the array and farther photons are listed towards the end of the array. The farthest photon is listed as the last element in the array; this is what creates the expanding sphere around the intersection point. The radius of the expanding sphere is the distance from the sphere's center to the last photon in the array. This photon is used to calculate the area of the bounding sphere.

long PhotonMap::GetNearestPhotons(
           photonlist_t tList[],
           long lNumber_Of_Friends,
          cVector3 tInter )
{
   // Note the function's job is to find closest photons using distance
   long i,j;
   float dMaxDist = 1000000000; // bogus distance
   float dCurrDist;
   long lCount = 0;
   cVector3 Dummy;

   // for all the photons in photon map
   for (i = 0; i < lNumGlobalPhotons; i++)
   {
      // find distance between photon and intersection point
      Dummy = pGlobalPhoton[i].tPosition - tInter;
     // find magnitude of the ray
     dCurrDist = Dummy.Mag();

     // if current squared distance is more than last max distance
     if (dCurrDist > dMaxDist)
        continue;

      // order found photons in distance order for the number
      // of requested photons
     for (j = ((lCount < lNumber_Of_Friends-1)
                ? lCount : lNumber_Of_Friends-1); (j > 0)
                && (tList[j-1].dDistance > dCurrDist); j)
         tList[j] = tList[j-1];
     // assign returned array with close photon
     tList[j].dDistance = dCurrDist;
     tList[j].tPhoton = pGlobalPhoton[i];

     // the maximum request is found,
     // the farthest photon is the maximum distance
     if (lCount == lNumber_Of_Friends)
        dMaxDist = tList[lNumber_Of_Friends-1].dDistance;
     else
        lCount ++;
   }
   // return the count of found photons
   return lCount;
}

The radiance estimate is finally found by adding the returned photons and dividing them by the area of the circle found. See Figure 12.9. The amount of photons that are included within this circle represents the overall incoming illumination for the point of interest. Even though this is one point you are interested in shading, other neighboring points across each scan line of the surface will share the same neighboring photons. Therefore, the illumination information is separated from the geometry and preserved in a small, compact, and clever manner. This makes things much easier because reusing neighboring photons saves memory.

Figure 12.9. The radiance estimate is determined by summing the found photons at the point of intersection and dividing them by the area of the circle found.

Determining the Photon Contribution

In this section, you write a method that will find the radiance estimate of a point in 3D space. The function is going to incorporate the material type and the incoming and outgoing lighting into one solution.

This method passes three parameters:

  • The first parameter (iObjectIndex) is the object index of the object that was intersected first by the ray from the image plane.

  • The second parameter (tDirection) is the direction of the ray that was spawned from the image plane.

  • The third parameter (tInter) is the intersection point of the ray.

The method creates a local array to store the nearest photons found. It continues by calling the GetNearestPhotons() method. The local list is updated with the closest photons around the point of intersection after GetNearestPhotons() is executed. The number of photons found is tallied by lNumPhotons. If no photons were found by GetNearestPhotons(), it will return with the radiance estimate set to black (or no illumination for the point).

To add the irradiance at the point of intersection, the function utilizes the number of found photons. The function will cycle through each photon to find the overall density. This process continues for every photon found in a list returned by GetNearestPhotons(). It calculates the angle between the pho-ton's direction and the normal of the surface. This is done to ensure that the two vectors are facing each other, which in turn ensures that the photon contributes to the surface lighting (also the indirect illumination). You then need to add the sample estimate (or direct illumination) of the ray taken from the eye (image plane) to the point of interest. This combines the direct and indirect illumination into one solution. After the closest photons are found they are added together and divided by the area of the circle found. This is the final solution. Once you have the radiance estimate, you apply the color of the object to the solution. Finally, the estimate is saturated and the local photon array is unallocated. The method returns with the radiance estimate for x hr space:

color3 cScene::PhotonContribution(
               int iObjectIndex,
               cVector3 tDirection,
               cVector3 tInter)
{
    // allocate memory
    photonlist_t *pList = new photonlist_t[lMaxPhotonNeighbors];

    long lNumPhotons;
    long i;
    color3 tColor, tTmp;
    float dDot;
    float dPiR2;

    // this is returned for no illumination
    tColor = color3::Black;

    // get nearest photons around the point of intersection
    lNumPhotons  = GetNearestPhotons(pList,lMaxPhotonNeighbors,tInter);

    // no photons found, omg very bad
    if (lNumPhotons == 0)
    return tColor;

    // for the amount of friends found
    for (i = 0; i < lNumPhotons; i++)
    {
        // is this friend facing the point of intersection
        dDot = pObjectList[ iObjectIndex ].Normal(tInter)
                      *  pList[i].tPhoton.tDirection;

           // if not, let's go to next array node
           if (dDot <= 0.0)
              continue;

           // add the power of the photon and the cosine shading method
           tTmp = pList[i].tPhoton.tPower *
                      (float) dDot *
                      pObjectList[iObjectIndex ].fDiffuseFactor;

           tColor.r = tColor.r + tTmp.r;
           tColor.g = tColor.g + tTmp.g;
           tColor.b = tColor.b + tTmp.b;
        }
        // get area of circle found using last photon in the found list
        dPiR2 = 3.1415926f *
                pList[lNumPhotons-1].dDistance *
                pList[lNumPhotons-1].dDistance;
       // after summing up everything divide it by the area of the circle
       tColor.r = tColor.r / (float) dPiR2;
       tColor.g = tColor.g / (float) dPiR2;
       tColor.b = tColor.b / (float) dPiR2;

      // used to color the photon with the object
      tColor.r *= pObjectList[ iObjectIndex ].Color.r;
      tColor.g *= pObjectList[ iObjectIndex ].Color.g;
      tColor.b *= pObjectList[ iObjectIndex ].Color.b;

     // saturate the colors that are out of range
     tColor.Sat();

    // free memory
    if(pList) delete [] pList;

    // return final illuminated color using photon mapping
    return tColor;
}


Using the PhotonMap Class

Take a look at the final PhotonMap class. This final class includes all the changes you've been developing throughout this chapter. The files photon.h and photon.cpp contain the photon structure, the photon list structure, and the photon map class. The PhotonMap class has all the methods covered in this chapter.

#ifndef _C_PHOTON_H
#define _C_PHOTON_H

#include <math.h>

// how much memory to allocate per call
#define PHOTON_REALLOC_JUMP            1000

// 1.0000000000 is used for the local coordinate system
#define TINY (1.0e-10)

// the photon structure
typedef struct
{
   color3 tPower;
   cVector3 tDirection;
   cVector3 tPosition;
} photon_t;

// the photon list structure used in finding the closest photons at x
typedef struct
{
   photon_t tPhoton;
   float dDistance;
} photonlist_t;

// the photon map class
class PhotonMap
{
public:
   long        lNumGlobalPhotons; // number of photons in photon map
   photon_t * pGlobalPhoton; // the photon map as an array

   long       lMaxPhotonPerObject; // max photons per object to shoot
   long       lMaxPhotonNeighbors; // max closest neighbors at hit point
   long      nAbsorbed;  // number of absorbed photons
   long      nRefracted; // number of refracted/transmitted photons
   long      nSpecularReflected; // number of mirror reflected photons
   long      nDiffuseReflected;  // number of diffused reflected photons
public:
        //constructor used to allocate and set up NULL pointers
    PhotonMap();

    // set method for the max photons per object
    // and max neighbors to return at x
    void SetupPhotonMapping(long max_photons, int max_friends);

    // add a photon to the photon map (array)
    void AddToPhotonList(photon_t **, long *, photon_t);

    // calculate the X and Y axes vectors of local coordinate system
    void MakeXY( cVector3 *vX, cVector3 *vY, cVector3 V );

    // gather nearest photons at X and save them to passed array as param1
    long GetNearestPhotons(photonlist_t [], long, cVector3);
};


Using the Upgraded cScene Class

You can now take a look at the upgraded cScene class with the new photon mapping methods applied. These methods cannot be applied to the PhotonMap class directly because they require special elements of the cScene class. The CreatePhotons() method creates photons from the light sources. The ProcessPhotonHit() method processes the nature of a photon based on a probabilistic solution. The PhotonContribution() method calculates the radiance estimate for a point in 3D space.

class cScene: public PhotonMap
{
   // Rest of Code is assumed…

   // called to scatter photons from the light sources
   void      CreatePhotons();
  // called when an intersection has occurred to select
  // a probabilistic solution
   void      ProcessPhotonHit(
                 int iObjIndex,
                 cVector3 tPhotonDir,
                 cVector3 tInter,
photon_t tPhoton,
                 int depth,
                 BOOL insideobj);
  // used to find the radiance estimate
  color3      PhotonContribution(
                  int iObjectIndex,
                  cVector3 tDirection,
                  cVector3 tInter);

};

Upgrading the Init Scene Method

You also need to upgrade the Init_Scene() method found in the cScene class to accommodate the photon-mapping algorithm. The new logic asks the users for the amount of photons to scatter per object as well as how many photons to search for when finding the radiance estimate for a point in 3D space. It loads this information into the member variables in the PhotonMap class for building and rendering the global photon map. The scene is read in from file and Render_Algorithm is set to photon mapping. Here is the code:

void cScene::Init_Scene()
{
   char buffer[256];
   cout << "Please Type in Path and File Name\n";
   cout << "Usage (ex. C:\\sample.txt)" << endl;
   cout << "Enter Information: ";
   cin  >> buffer;

   // read in scene
   Read_In_Scene(buffer);

   // Which Algorithm?
   //Render_Algorithm |= RAY_TRACING;
   Render_Algorithm |= PHOTON_MAPPING;

   // if photon mapping then ask for more info
   if ( Render_Algorithm & PHOTON_MAPPING )
   {
     // ask for photon mapping
     long m,f;

     cout << "How Many Photons Per Object?\n";
     cin  >> m;

     cout << "How Many Photons for pixel Contribution?\n";
     cin  >> f;

     this->SetupPhotonMapping(m,f);
  }
}

Updating the Trace Ray in Scene Function

You now should add the photon contribution method to the Tray_Ray_In_Scene()function located in the cSceneclass. Only the logic based on the Rendering_Algorithmvariable has changed. If the variable is set to pho-ton mapping, it will ignore all ray tracing code and add the radiance estimate for the point of intersection. This method has one very small change whereby the photon contribution method is added to the point of intersection.

This is the part of the photon mapping application where you use the sample ray from the image plane to the point of intersection and apply the radiance estimate. (See Figure 12.10.) The radiance estimate is a composite of the sample ray and the irradiance to calculate the final lighting solution for the point. This is the final solution that's assigned to the source pixel on the image plane.

Figure 12.10. The radiance estimate is found by combining backward ray tracing and the total incoming irradiance for the point of intersection.

The new code appears in bold:

bool cScene::Tray_Ray_In_Scene(cRay ray, int depth, color3 *pColor)
{
   cVector3 Hit, Normal;

   cVector3 Reflected_Dir,
        Refracted_Dir;

   int      iClosestObject;

   color3       color_Object = color3::Black,
          intensity_Shade = color3::Black;
   color3 color = color3::Black;

   // added in chapter 9 - ray tracer 2
   color3 reflected_Shade = color3::Black;
   color3 refracted_Shade = color3::Black;

   if( depth > MAX_TRACE_DEPTH )
   {
       *pColor = color;
       return false;
   }

   // find closest object
   iClosestObject = Find_Closest_Object(ray, &Hit);

   // get object normal
   Normal = pObjectList[iClosestObject].Normal(Hit);

   // if object is in the local array list
   if( iClosestObject > -1 )
   {
       // =================================================================
       // object color
       // =================================================================
       color_Object = pObjectList[iClosestObject].Color;

       // =================================================================
       // BEGIN RAY TRACING
       // =================================================================
       if ( this->Render_Algorithm & RAY_TRACING )
       {
          // Ray Tracing uses an ambient shading to ensure objects are lit
          color = ( color * pObjectList[iClosestObject].fAmbientFactor);

          // diffuse shading & specular highlights
             intensity_Shade = ShadePoint( Hit, Normal, iClosestObject );

               // combine our information
               color.r += color_Object.r * intensity_Shade.r;
               color.g += color_Object.g * intensity_Shade.g;
               color.b += color_Object.b * intensity_Shade.b;

               // Calculate Reflection
            if(  RenderSetting.use_specular_reflection &&
                pObjectList[iClosestObject].fSpecularFactor > 0.0f )
            {

             Reflected_Dir =
                 ray.direction.Vector_Reflection ( Normal ) ;

             if( !Tray_Ray_In_Scene(cRay(Hit, Reflected_Dir),
                      depth + 1, &reflected_Shade ))

                   reflected_Shade = color3::Black;
            }

            color += (reflected_Shade
                     * pObjectList[iClosestObject].fSpecularFactor);

               // Calculate Refraction
               // FROM AIR (1.003) INTO SURFACE

            if(  RenderSetting.use_refraction &&
                   pObjectList[iClosestObject].fOpacity < 1.0f )
            {
                 if(ray.direction.Vector_Refraction(
                     Normal,
                     1.003f,
                     pObjectList[iClosestObject].fIndex_Of_Refraction,
                     &Refracted_Dir ))
                 {
                  if( !Tray_Ray_In_Scene(cRay(Hit,
                      Refracted_Dir),
                      depth + 1,
           &refracted_Shade ) )
        refracted_Shade = color3::Black;
     }
  }

    color += (
               refracted_Shade *
              pObjectList[iClosestObject].fOpacity );
}

// =================================================================
// END RAY TRACING
// =================================================================

else if ( this->Render_Algorithm & PHOTON_MAPPING )
// =================================================================
// APPLY PHOTON MAPPING
// =================================================================

{
// apply photon mapping intensity for the point
color   += PhotonContribution(
            iClosestObject ,
           ray.direction ,
            Hit);

}

else
{
   // Do nothing; no algorithm used
}

// =================================================================
// Saturate colors out of range
// =================================================================

color.Sat();
// =================================================================
// Save Color
// =================================================================
    *pColor = color;
 }
 else
 {
    *pColor = color3::Black;
    return false;
 }

 return true;
}


Using the Chapter's Source Code

The CD materials for this chapter contain an example of this photon mapping application (/Book Code/Chapter12/Photon Mapping). The general design of the application is the same as the previous applications in this book. You are still building from where you left off in Chapter 11. The environment of the photon mapping application is exactly the same as the ray tracing applications. Figure 12.11 shows an example of the project setup.

Figure 12.11. The project setup of the photon mapping application.

It's time to take a look at the new changes that are implemented in each section of the photon mapping application. The entry point of the photon mapping application is inside main(). This is how you dictate to the application the logic that should be executed for photon mapping. The body and main logic of the program is located in /Chapter12/Photon Mapping/Photon Mapping.cpp. This is the brain of the program, where it calls the different methods of the pho-ton map and scene classes. However, the overall general flow of the application has changed a little bit from the previous ray tracing applications (Ray Tracer.cpp). The new code is incorporated for the photon mapping components and functionality. The changes inside main() appear in bold. If the Rendering_Algorithm variable is set to PHOTON_MAPPING, all required photon-mapping logic is executed within the application.

NOTE

NOTE

The overall main logic of the photon-mapping application is almost the same as in the ray tracing applica-tions.The scene loads in a definition file,parses the file,and loads the internal object arrays.The photon mapping logic is then executed and backward ray tracing is called.After the scene is rendered to the image plane,the scene objects are unloaded.

A few variables are created to tally the time it took to build and render the scene using photon mapping. Information is constantly printed to the DOS window to inform the user that the application is running properly. When the program has commenced, it calls the Beep() method that causes the computer speaker to beep. Here is the code:

int main(int argc, char* argv[])
{
    // for timing
    unsigned long Start_Time      = 0;
    long       Whole_Interval       = 0,
               seconds               = 0;
    int        G_Curr_Mins        = 0,
               G_Curr_Secs            = 0,
               Average               = 0;

    // grab begin time
    Start_Time = GetTickCount();

    // RAY TRACER
    printf("Photon Mapping 1!\n");

   // Begin
   printf("Init Scene!\n");
   TheScene.Init_Scene();

   // Scene Stats
   printf("Scene Stats!\n");
   printf("Objects: %d Lights: %d\n",
              TheScene.nObjectCount, TheScene.nLightCount);

   // Added in Chapter 11
   if ( TheScene.Render_Algorithm == PHOTON_MAPPING )
   {
      // Create Photons
      printf("Creating Photons!\n");
      TheScene.CreatePhotons();
   }

    // Trace
   printf("Tracing Scene!\n");
   TheScene.BeginTracing();

   // We're all done!!
   printf("Done Tracing Scene!\n");
   TheScene.Kill_Scene();

   // actual render time
   Whole_Interval = 0;
   Whole_Interval = (GetTickCount() - Start_Time);
   seconds          = (Whole_Interval / 1000);
   G_Curr_Mins       = (int)seconds / 60;
   G_Curr_Secs       = (int)seconds % 60;

   printf("Render Took %d Minute(s) and %d Second(s)...",
                      G_Curr_Mins, G_Curr_Secs);
   // halt and beep
   Beep(1000, 500);

   return 0;
}

NOTE

NOTE

The Init_Scene() method in the scene class should be used to allocate objects and to set up the rendering environment.

The folder that includes the required classes for this program is in /Book Code/Chapter 12/Photon Mapping/Common. The files in this folder are:

12/21/2002  05:19p      1,381 cLight.h
12/21/2002  05:19p      5,127 cObject.cpp
12/21/2002  05:19p      4,692 cObject.h
12/21/2002  05:19p        442 Color3.cpp
12/21/2002  05:19p      4,480 Color3.h
12/21/2002  05:20p      2,248 cPhoton.cpp
12/21/2002  05:32p      1,408 cPhoton.h
12/21/2002  05:19p         23 cRay.cpp
12/21/2002  05:19p      2,043 cRay.h
12/21/2002  05:19p     34,852 cScene.cpp
12/21/2002  05:23p      3,781 cScene.h
12/19/2002  05:29p      1,991 cVector3.cpp
12/21/2002  05:19p      5,169 cVector3.h
12/21/2002  05:19p      1,060 MathTools.h
12/21/2002  05:19p      1,108 PPM.cpp
12/21/2002  05:19p        154 PPM.h

You compile this program as described in previous chapters. You can also run the program directly by executing the /Book Code/Chapter 12/Photon Mapping/Photon Mapping.exe file. When the program asks you to "Enter Information:", you enter:

/scenes/photon.txt

The program will also ask how many photons you would like to scatter per object. A good number for spheres is 1000, whereas a good number for triangles is 1000. It will also ask you what the photon contribution is. A good number is 50.

The photon-mapping algorithm will then begin scattering photons. The program starts the ray-tracing process and then again will prompt you to enter the output path for the rendered file stored on the image plane. When it asks "Please Enter output Path and Filename:", enter:

/scenes/myoutput.ppm

You can then browse to that path and open up the file in IrfanView. However, I've already created the exported file for you and placed it in the following folder:

/Book Code/Chapter 12/Photon Mapping/Output

The file in this folder is:

10/13/2002  03:30p     3,212,934 photon.ppm

An Example Using the Program

You can now begin compiling the program and executing it using Microsoft Visual C++ 6.0. The application will begin running and you will see the fol-lowing information printed on the screen. The scene definition file for the photon mapping application is located in /Book Code/Chapter 12/Photon Mapping/Scenes. The file you will use when running this program is called photon.txt. The program input is as follows:

Init Scene!

Please Type in Path and File Name
Usage (ex. C:\sample.txt)
Enter Information:

This is where you enter the relevant information pertaining to the scene files. For example, you type into the line this statement: Scenes/photon.txt. This procedure loads all data in the scene for rendering.

NOTE

CAUTION

The array method that searches for the closest photons is extremely slow,so be warned that the render will take a very long time.This test was conducted on my laptop that has a PIII 900 MHz Processor,256MB of RAM and 16MB of Video RAM.

The photon mapping input is as follows:

How Many Photons per Object? 10000
How Many Photons for Photon Contribution? 50

This is the new part of the general logic that requests the number of photons to scatter per object and the number of photons to search for when calculating the radiance estimate.

The program statistics are as follows:

Scene Stats!
Objects: 2 Lights: 2
Generating Photons!

The objects and light lists are updated and will report this information. The application then begins the photon-tracing process.

The photon mapping statistics are as follows:

Creating Photons!
To Object 0: 10000 fired, 65574 hit
To Object 1: 10000 fired, 6557  hit
To Object 0: 10000 fired, 7305  hit
To Object 1: 10000 fired, 7289  hit
[STATS]
Actual Stored Photons = 29970
Absorbed Hits: 29970
Specular Reflected hits: 0
Diffuse Reflected hits: 2262
Refracted Hits: 0

After the photon tracing process has commenced, the lighting effect statistics are printed. This gives you detailed information about when the photon map was constructed.

Tracing Scene!

Once the photon map has been constructed, the program begins using backward ray tracing to calculate the radiance estimate for the point of intersection. This may take some time depending on the geometry of the scene and the specs of your computer.

The program output is as follows:

Please Enter output Path and Filename:
Usage (c:\example.ppm): /Output/photon.ppm

The application completes the rendering process and prompts the users to select the filename and path of the rendered image. The output file must be labeled with a *.PPM extension. After you have entered this information, the application completes.

The program time report is as follows:

Done Tracing Scene!
Render Took 30 Minute(s) and 35 Second(s)...

The application reports the time it took to load the scene and render it. As you can see, the time it took to render Scenes/photon.txt was 30 minutes and 35 seconds on my laptop. This is long because traversing through a linear array is a very slow process. You'll learn how to speed up this process in the next chapter.

Figure 12.12 shows an image of the photon.txt render using the photon mapping application with the different lighting effects discussed in this chapter.

Figure 12.12. Scenes/ photon.txt shows two spheres rendered with diffuse interaction and diffuse reflection.

    [ LiB ] Using the Upgraded cScene Class


Summary

This chapter provided a detailed overview about photon rendering and thus completes the full implementation of the photon-mapping algorithm. You've developed a method to search for the closest photons around the point of intersection and will use the returned photons to average the incoming radiance for a point of interest. You can now render the scene using backward ray tracing by finding the radiance estimate of a point on a surface in the scene. Currently, the photon mapping application is very slow because you're using an array to store photons in the photon map. The render time was very long for the photon.txt render because traversing through a linear array is a very slow process. The next chapter, therefore, discusses how to increase the efficiency of searching for photons when finding the radiance estimate.


Chapter 13. Advanced Searching

Now that you understand the photon-mapping algorithm, you need to find ways to improve its efficiencyto optimize it. Currently, you're using an array to store photons. Arrays, however, are poor candidates for indexing data. In the photon-contribution method, traversing through an array to find the nearest photons around the point of intersection is a very slow task. This is because you need to begin searching the first element of the array and move forward. Sometimes, however, the relevant photons in the photon map are located at the end of the array, so moving through each unimportant ele-ment is really a waste of time. This chapter therefore focuses on a better method for retrieving photons when finding the radiance estimate.


Using Spatial Subdivision

Spatial subdivision is the concept of dividing space into smaller regions based on a specific threshold. The reason why you would want to subdivide space is to limit your search to a specific area. Placing objects in a smaller area speeds up your search. The best way to visualize the purpose of subdividing space is by considering one floor of a building. Each room on the floor is a region of space. This means the overall space is subdivided among the rooms, as illustrated in Figure 13.1. The walls, ceilings, and floors of the rooms create the boundaries between the rooms and the doors define the partitioning between the rooms. Within every room are the walls, ceiling, and floor, which create a volume also known as a cell. The first part of a spatial subdivision algorithm is to divide the space into many smaller portions as well as to balance the subdivisions equally among the available cells.

Figure 13.1. The rooms on the floor of a building create the subdivision of space.Each room represents a volume, also known as a cell.

NOTE

TIP

Spatial subdivision subdivides space into smaller regions so that searching for relevant information is easier because the smaller regions can be searched individually.

The second part of spatial subdivision involves searching through the generated cells for the relevant information. Visualize three rooms on a floor. Room one contains books; room two contains toys; and room three contains food. If you were hungry, which room would you go to for some food? You would go to room three; you wouldn't want to look for food in a room full of toys. What you've just done is divide the space based on a series of parameters. In this example, the objects consist of food, toys, and books. You've limited your search by knowing which room contains the relevant objects you are interested in obtaining.

In the room containing food, you might have all sorts of foods. If you want a can of sausages, you might have to search in the cupboard through the canned products. But because you know where the foods are, you can find the food faster. This is the basic idea behind spatial subdivision.

Now back to the topic of photon mapping. You need to find a way to partition the photons in 3D space using spatial subdivision. A way to reduce the time spent searching for the nearest photons in the photon map is to subdivide the space into smaller regions based on point proximity between point sets. Point proximity refers to the relative distance or the local distance between one or more points in a specific area. For example, say you have a photon map of six photons. You can increase the speed when searching for photons through this incredibly small photon map. Before you get started, your point of intersection is located at 0, 0, and 5 and the nearest photon to search for is set to 2.

The photon locations in the photon map are as follows:

  • Photon 1 (0, 0, 2)

  • Photon 2 (1, 0, 4)

  • Photon 3 (1, 0, 5)

  • Photon 4 (0, 0, 2)

  • Photon 5 (1, 0, 4)

  • Photon 6 (2, 0, 6)

Figure 13.2 shows a top-down view of the photon map.

Figure 13.2. A top view of the points positioned in world space that are contained in the photon map.

Let's see how long it takes to find the nearest photons for the point of intersection (0, 0, 5). The method you'll use is known as the linear search method. The basic idea behind the linear search method is that you search one ele-ment at a time, beginning with the first element of the array.

This process is as follows:

  1. Search element 1, photon 1(not close enough)

  2. Search element 2, photon 2 (not close enough)

  3. Search element 3, photon 3 (not close enough)

  4. Search element 4, photon 4 (not close enough)

  5. Search element 5, photon 5 (very close)

  6. Search element 6, photon 6 (very close)

As you can see, the time required to search through elements 1 to 4 was a waste and the processor had to actually go through these unimportant steps until the two closest photons were found. The two closest photons next to the point of intersection (0, 0, 5) are Photon 5 (1, 0, 4) and Photon 6 (2, 0, 6).

One better method is to break the six-element array into two smaller arrays based on the relative position of the photons (or the points) located in 3D space. The objective is to split the space into smaller regions by using a splitting node. The splitting node will be a photon that has the best position in the input array. When using a splitting node such as a point to partition space, you are partitioning the space along the X, Y, and sometimes the Z axes.

Consider Figure 13.2 one more time and try to find a place where you can subdivide the space into two regions. The goal is to balance the photons divided on each side as much as possible. A good number is preferably three photons per side. After some careful planning, you might decide that the best point (photon) you can use as the splitting node to subdivide the space is Photon 4 (0, 0, 2). You should choose the photon that cuts the space into two halves on a specific dimension. The dimension utilized is the line that runs horizontally, which is the X axis. See Figure 13.3.

Figure 13.3. Partitioning the space equally on both sides is important because it limits the time required to search for relevant photons.

After you divide the space into two regions, you have two arrays that are divided by Photon 4. The first three elements of the photon map are Photons 1 through 3 (located in Array 1), and the remaining three elements are Photons 4 through 6 (located in Array 2).

Array 1 is composed of the following photons:

  • Photon 1 (0, 0, 2)

  • Photon 2 (1, 0, 4)

  • Photon 3 (1, 0, 5)

Array 2 is composed of the following photons:

  • Photon 4 (0, 0, 2)

  • Photon 5 (1, 0, 4)

  • Photon 6 (2, 0, 6)

Now you can try the process again and see whether this new method helps to speed up the searching process. Using the two-array method for searching for the nearest photons, the process is as follows. You already know that the point of intersection is not in Array 1 because all the photons in this array are behind Photon 4, so you can discard the whole array.

Searching through Array 2 would go as follows:

  • Photon 4 (not close enough)

  • Photon 5 (very close)

  • Photon 6 (very close)

You have just cut the time it takes to find the closest photons in half. This was accomplished by splitting the list into two smaller child lists. This concept is known as binary searching.

The linear search algorithm runs in O(n) time whereas binary search takes only O(log_(n)). So a 545 microsecond linear search may take only seven microseconds using the binary search. The choice of optimization is determined by how one builds and balances the binary tree (children upon children). You will see this with your own eyes at the end of the chapter. In this case, it doesn't really matter because you have a very small photon map. But just imagine the time you can save if you implement this technique with millions of photons. If it took 30 minutes to render a scene using photon mapping with the linear search method, it may take 15 minutes to 30 seconds using the binary search, which is a big time saver. You definitely increased the speed when rendering by using spatial subdivision. There are two- and three-component spatial subdivision algorithms that can be used to store and search for photons. A BSP Tree is a great candidate to store and retrieve pho-tons and has been used in some applications. A KD Tree works similar to the method just discussed, which is why you'll use it in this book.


Using the KD Tree Algorithm

A KD Tree is a spatial subdivision algorithm than can be used during orthogonal range searching. Before you learn about the KD Tree, take a look at Figure 13.4. You'll use these photons located in space and spatially subdivide them using the KD Tree algorithm.

Figure 13.4. A top view of photons situated in space and contained in the global photon map.

Try to partition the image so that you hierarchically deconstruct space into many smaller areas, known as cells. Again, the basic premise behind spatial subdivision is to limit the input search so that you search through the fewest objects (in this case, photons) in a specific area. The fewer the objects, of course, the faster the search and the faster the render. This means you need to subdivide the space so that every small area does not contain too many objects. This helps to find the closest set of relevant photons based on an input position (which is the point of intersection). You find the closest photons based on a position value by traversing (or searching) through the hierarchy of the KD Tree until you find a cell and sometimes the surrounding cells containing the nearest photons. The cell that contains the nearest photons is scanned until the relevant neighbors of the photon contribution method are identified.

NOTE

NOTE

The KD Tree algorithm is the perfect candidate for photon mapping because photons in the global pho-ton map are really points located in space with some extra information stored with them.

If you partition the space in the Figure 13.4 by half planes, each point is contained within its own cell, as shown in Figures 13.5 through 13.7. When you create a splitting node in a 2D environment, you can only split horizontally and vertically.

Figure 13.5. The first time a subdivision occurs,the photons in space are separated by a new splitting node.

Figure 13.6. The second time a subdivision occurs,the photons in space are separated by another splitting node.

Figure 13.7. The third time a subdivision occurs,the photons in space are separated by yet another splitting node.

As you can see, each time the algorithm runs, it cuts the space into one of two dimensions with each cell containing only one point. Because this is a 2D example, it's very easy to convey the subdivisions because each cell is made up by a rectangle. Take a look at Figure 13.8 for an example of the new cells created based on point proximity.

Figure 13.8. The cells generated by the 2D KD Tree subdivision algorithm.

Transforming the KD Tree into a 3D environment is very simple. All you need to do is simply take the third component of the splitting node into account when partitioning the space. Instead of splitting the partitioning point sets on the X and Y dimensions, the Z dimension is utilized as well.


Building the KD Tree

The next logical step is to actually implement this concept. You build a KD Tree by taking a large input list of points (photons in this case) and partitioning them based on their positions in the world. The input point list is going to be the photon map array located in the PhotonMapclass. The resulting effect after the algorithm takes its course yields a hierarchy of point lists (or point sets) that are partitioned by each other. See Figure 13.9 for an example.

Figure 13.9. An example of the hierarchy of point lists that partition each other.

There has been much research conducted on building KD Trees over the past few years. Typically, it's a simple process whereby each node in the tree is defined by a rectangle (in a 2D environment) or a plane (in a 3D environment). In a 3D environment, the plane in each node dictates the type of dimensional split that takes place. Again, the divisions are made along the world X, Y, and Z planes depending on a decision variable. When a node is selected to be the splitter and the subdivision takes place, the partitioning node creates two child lists that are partitioned in a left or right order. See Figure 13.10.

Figure 13.10. The partitioning nodes split space into two sides in left or right order.The hierarchy of point lists is composed of left and right photon divisions.

When a subdivision occurs, the space is split into two halves. For example, the first time a subdivision occurs the whole world is cut into two regions by a selected node. This means each side (or child of the parent node) will carry half the photons of the parent node. Thus, the division of two point-sets separated by one node is known as the median split. If you had a total of six points you would have a left and right list of three points each. After the first split is conducted the new children that are generated are subdivided into equal halves by planes on different dimensions. Then those children are subdivided with the same procedure until each point of the original list sits within its own leaf cell.

This process creates a hierarchy of points separated by median splits and sub-trees. The partitioning stops after O (Log2N) levels. Note that in a 2D environment the cells of a KD Tree are rectangles, but in 3D they are volumes known as cubes. Cubes are also known as bounding boxes that encompass a series of objects in a specific volume. See Figure 13.11. The objects included within a bounding box can be points, photons, spheres, polygons, triangles, meshes, and the like. The subdivision of space using the KD Tree in a 3D environment involves subdividing one large cube into smaller cubes. Those smaller cubes (or volumes) are then divided again until each point sits within only one cube. This is essentially what the 3D KD Tree subdivision algorithm does.

Figure 13.11. This is an example of the splitting nodes (planes) that subdivide the space into smaller cubes using the 3D KD Tree spatial subdivision algorithm.

    [ LiB ] Using the KD Tree Algorithm Searching the KD Tree


Searching the KD Tree

After the KD Tree is constructed, you'll use it in the rendering process to find the nearest neighbor for the point of intersection. The general idea is to find the photons within a given volume for n amount of photons. Because the photon map is now constructed spatially it returns the relevant photons faster. The process for returning the nearest photons is as follows:

  1. You know that the space was divided in a cube-like manner where cubes make up other cubes. The whole world sits within one big parent cube. All you need to do is find the right cube (or perhaps the neighboring cubes as well) in a specific cube (volume) to find the relevant photons.

  2. You begin from the root of the tree and then search through each ele-ment respectively.

  3. As the algorithm runs through a series of nodes in a specific area, a local photon pool is allocated with photons that are within a certain distance.

  4. The maximum distance between points determines the dimension to search for next.

  5. The input point is constantly tested with each splitting node, which means the search is branched down into the left or right subtree and the process begins once again.


Implementing the KD Tree

In order to implement the KD Tree algorithm, you need a structure that will be used for the nodes of the tree. In this section, you'll write a few methods that will be located in the PhotonMap class for subdivision and searching of the KD Tree. The KD Tree algorithm doesn't require any methods or variables from the cScene class, so it will be defined only in the PhotonMap class. The only element that the KD Tree requires is the list of photons in the photon map array.

The KD Tree is composed of a set of smaller elements called KD nodes. A KD node is a component of the KD Tree that dictates the type of splitting that occurs. You will have a KD node for each photon in the photon map array to give each photon a splitting dimension and sorting value. The KD nodes are used to construct the hierarchy of point sets by partitioning photons with each other and flagging each KD node of the type of dimensional splitting that occurs. This in essence preserves the splitting information or the hierarchy of the point list so that you can search through the KD nodes based on their assigned sorting values.

Because you already have a method for locating the nearest photons found using the photon map array, you need a way to toggle between using the KD Tree and the linear search method of retrieving the closest photons. This will be a simple pre-compiled macro located in the PhotonMap class. If this macro is defined, your code can use the KD Tree when building and searching for photons.

Adding the KD Node

The KD node structure is the most important element of the KD Treeit contains a photon and two scalar values. The iSort value is used to build the photon map as well as to determine which kind of splitting should be conducted. The dValue holds the dimensional value for the partitioning plane.

For example, a photon located in space (0,0,10) that is used as the splitting node would be listed in the structure as follows:

  • You have a splitting node at location 0,0, and 10 in space for the pho-ton position.

  • If you decipher the point, you know that a partition should be conducted positive 10 units on the Z axis, indicating that the dValue is set to 10 units into the Z axis.

  • The iSort or splitting dimension is set to the Z plane in world space.

The node structure is defined as follows:

typedef struct
{
   photon_t tPhoton;
   int iSort;
   float dValue;
} KDNode;

Adding the KD Tree

Typically, you add the KD Tree algorithm to photon mapping by using a macro to switch the logic of the code to the KD Tree's methods and variables. The KD node array is listed in the PhotonMap class. The pre-compiled macro is listed as follows:

#define   USE_KD_TREE

The KD Tree is listed in the PhotonMap class as follows:

class  PhotonMap
{
// rest of code assumed
KDNode   * pKDNodes;
}

Building the KD Tree

You'll now write a method that will take the photon map array and create a local photon array to mangle photons. A new KD node is created for each photon listed in the photon map array. A variable, called sortKey, is defined for partitioning photons on a specific dimension. If the sort key is 1, the split was conducted on the X dimension. If it's 2, it's for the Y dimension, whereas 3 is for the Z dimension. Each node of the KD Tree is unsorted in the beginning. The function ends by calling MakeSubTree(), which begins the process of building and spacing the partitioning photons.

photon_t *tLocalPhotons;
long lLocalPhotonCount;
int sortKey;

void PhotonMap::MakeKDTree()
{
   long i;

  // allocate memory to hold photons in the photon map array
   lLocalPhotonCount = lNumGlobalPhotons;
   tLocalPhotons = (photon_t *) malloc(sizeof(photon_t)
          * lNumGlobalPhotons);

   // copy elements over to local array
   for (i = 0; i < lNumGlobalPhotons; i++)
      tLocalPhotons[i] = pGlobalPhoton[i];
   // allocate memory for each KD node for
   // each photon stored in photon map array
   pKDNodes = (KDNode *) malloc(sizeof(KDNode) * lNumGlobalPhotons);

   // set each KD node to unsorted
   for (i = 0; i < lNumGlobalPhotons;  i++)
   {
      pKDNodes[i].iSort = -1;
   }

  // build tree from available photons
   MakeSubTree(0,lNumGlobalPhotons);

Using the SortProc Method

If you recall, when building the KD Tree, points need to be relative to one another. Sometimes during the build, the elements of the photon-mapping array need to be placed in an orderly fashion. In order to split a set of points on a desired plane, you need to sort the points based on their positions in the array. For example, if the sort key was the Y dimension, all points in the local array must be in Y distance order, from closest to farthest. This example uses the quick sort method for reordering photons in the local array. Using quick sort is an easy way of sorting. All you need to do is define a recursive function for the algorithm to call. The recursive function will be called constantly during the re-ordering process. After quick sort completes, the list of photons are in their appropriate order based on a specific dimension.

int SortProc( const void *a, const void *b )
{
   float r1, r2;
   photon_t pa = *(photon_t *)a;
   photon_t pb = *(photon_t *)b;

   // if the sort key was on the X Axis
   if (sortKey == 0)
   {
      r1 = pa.tPosition.x;
      r2 = pb.tPosition.x;
   }
   // if the sort key was on the Y Axis
   else if (sortKey == 1)
   {
      r1 = pa.tPosition.y;
      r2 = pb.tPosition.y;
   }
   // if the sort key was on the Z Axis
   else if (sortKey == 2)
   {
      r1 = pa.tPosition.z;
      r2 = pb.tPosition.z;
   }
   else
      return 0;

   if( r1 < r2 )
      return -1;
   else if( r1 > r2 )
       return 1;
   else
      return 0;
}

Using the MakeSubTree() Method

The MakeSubTree() method's main purpose is to recursively subdivide the input point list. Because this is a KD Tree in a 3D environment, you use cubes (or bounding boxes) when partitioning photons. Each bounding box is sliced through the center divided by X, Y, and Z planes. If the photon map array has only one photon, one KD node is returned unsorted and unchanged. The first time the algorithm begins, a few bogus values are used to set up the maximum distance for the large bounding box encompassing the scene. This is conducted so as to accommodate the whole scene without leaving out any photons. The new correct bounding box is then computed by the list of photons in the photon map array. The farthest photons on each side of the box are used to create the boundaries of the box.

The process then cycles through each photon in the array and decides how to conduct the specific type of split. The job of the algorithm is to yield a dimension to split on for every photon. Subtracting the maximum (in terms of X, Y, and Z) from the minimum (in terms of X, Y, and Z) determines a range; the largest range of the three dictates the dimension to subdivide the remaining photons to split on. The current point is used as the median split where two new child lists will be spawned. The algorithm calls the recursive function again for each new child list. Every time a splitting occurs, two lists (photons to the left and photons to the right) are generated. The whole process begins once again with the child as the parent node.

void PhotonMap::MakeSubTree(long start, long n)
{
long left, nleft, right, nright;
long maxRangeIndex;
double maxRange;
long j;
photon_t p;
double rmin_x, rmin_y, rmin_z;
double rmax_x, rmax_y, rmax_z;

   // only one photon
if (n == 1)
{
   pKDNodes[start].tPhoton = tLocalPhotons[start];
   //pKDNodes[start].dValue = 0;
   //pKDNodes[start].iSort = 0;
   return;
}

// Initialize tree stuff -

left   = start;
nleft  = n / 2;
right  = left + nleft;
nright = n - nleft;

maxRangeIndex = -1;
maxRange      = -100000000000000;

// set some bogus ranges so that we don't leave out any photons

// PS: The objects in the scene cannot
// possibly fall outside this big bounding box
rmin_x  = 100000000000000;
rmax_x  = -100000000000000;
rmin_y  = 100000000000000;
rmax_y  = -100000000000000;
rmin_z  = 100000000000000;
rmax_z  = -100000000000000;

// get bounding box sides from available list of photons
for(j = start; j < start+n; j++ )
{
   p = tLocalPhotons[j];
   if( p.tPosition.x < rmin_x )
      rmin_x = p.tPosition.x;
   else if( p.tPosition.x > rmax_x )
      rmax_x = p.tPosition.x;

   if( p.tPosition.y < rmin_y )
      rmin_y = p.tPosition.y;
   else if( p.tPosition.y > rmax_y )
      rmax_y = p.tPosition.y;

   if( p.tPosition.z < rmin_z )
       rmin_z = p.tPosition.z;
   else if( p.tPosition.z > rmax_z )
      rmax_z = p.tPosition.z;
 }

// select the axis that has the maximum range
// of the farthest distance of available photons
 if (rmax_x-rmin_x > maxRange)
 {
    maxRangeIndex = 0;
    maxRange = rmax_x-rmin_x;
 }
 if (rmax_y-rmin_y > maxRange)
 {
     maxRangeIndex = 1;
     maxRange = rmax_y-rmin_y;
 }
 if (rmax_z-rmin_z > maxRange)
 {
    maxRangeIndex = 2;
    maxRange = rmax_z-rmin_z;
 }

// the sort key is set to the dimensional split
 sortKey   = maxRangeIndex;

// sort available photons in list to selected dimension
 qsort( (void *)&(tLocalPhotons[start]), n, sizeof(photon_t), SortProc );

  // Median split

 pKDNodes[right].tPhoton = tLocalPhotons[right];
 pKDNodes[right].iSort   = sortKey;
   // what kind of dimensional split? X, Y, or Z?
   if (sortKey == 0)
      pKDNodes[right].dValue = tLocalPhotons[right].tPosition.x;
   else if (sortKey == 1)
      pKDNodes[right].dValue = tLocalPhotons[right].tPosition.y;
   else
      pKDNodes[right].dValue = tLocalPhotons[right].tPosition.z;

   // the children to the left of me
   MakeSubTree( left, nleft );

   // the children to the right of me
   MakeSubTree( right, nright );
}

Checking the Photons

As you run through different nodes in the tree and create a photon pool, you need a method to track how many photons are accumulated in the temporary distance array. This requires a few global variables for retrieving the relevant photons because the distance array is used to hold relevant photons found by comparing the distance between found photons and the point of intersection. A counter is used to tally the number of photons found. By testing new photons with the point of intersection, the program can then decide whether to add the photon to the distance array or not.

The function searches through the photon map, finds the closest number of photons (based on their distance to the intersection point), and saves them in the distance array. The function finally returns with the number of pho-tons found as well as the distance array filled with the closest photons. It also lists the photons in distance order. For example, the nearest photons to the point of intersection are listed first in the distance array and farther photons are listed towards the end of the array. The farthest photon is listed as the last element in the array; it creates the expanding sphere. The radius of the expanding sphere is the distance from the sphere's center to the last photon in the array. This photon is used to calculate the area of the bounding sphere.

photon_t    *tLocalPhotons;
long        lLocalPhotonCount;
cVector3    tRefPoint;
float       maxDistance;
long        lFoundCount;
photonlist_t  *tLocalList;
long        tLocalListSize;
   void PhotonMap::CheckPhoton(photon_t p)
   {
      float dCurrDist;
      long j;
      cVector3 Dummy;

      Dummy     = p.tPosition - tRefPoint;
      dCurrDist = Dummy.Mag();

      if (dCurrDist < maxDistance)
      {

         for (j = ((lFoundCount < tLocalListSize-1) ?
            lFoundCount : tLocalListSize-1); (j > 0)
         && (tLocalList[ j - 1].dDistance > dCurrDist); j)
            tLocalList[j] = tLocalList[j-1];

         tLocalList[j].dDistance = dCurrDist;
         tLocalList[j].tPhoton = p;

     if (lFoundCount != tLocalListSize)
        lFoundCount ++;

     if (lFoundCount == tLocalListSize)
        maxDistance = tLocalList[tLocalListSize-1].dDistance;
   }
}

Using the SearchTree Function

The SearchTree function is recursively called until n photons are found in the KD Tree for the point of intersection within a given volume. You begin searching from the root of the tree and then through each node respectively. As the algorithm runs through a series of nodes in a specific area, a local photon pool is allocated with photons that are within a certain distance from the hit point. When a photon is found, it is tested to determine what kind of split was conducted in the build process. The intersection point is constantly tested with each splitting node and is either branched down into the left or right subtrees. The global distance array for the closest neighbors is constantly re-ordered with photons in distance order. This is done so that, as the algorithm runs through a series of nodes in the KD Tree, it would be easy to replace the farthest photons with newly found closer photons.

void PhotonMap::SearchTree( int start, int n )
{
   int left, nleft, right, nright;
   int sk;
   photon_t ph;
   float d, d2;

   // Check terminator
   if( n <= 2)
   {
     CheckPhoton(pKDNodes[start].tPhoton);

     if (n == 2)
        CheckPhoton(pKDNodes[start+1].tPhoton);
     return;
   }
   // Initialize tree stuff -
   left = start;
   nleft = n / 2;
   right = left + nleft;
   nright = n - nleft;
   //- Do search

   sk = pKDNodes[right].iSort;
   ph = pKDNodes[right].tPhoton;
   if (sk == 0)
      d = tRefPoint.x - pKDNodes[right].dValue;
   else if (sk == 1)
      d = tRefPoint.y - pKDNodes[right].dValue;
   else if (sk == 2)
      d = tRefPoint.z - pKDNodes[right].dValue;
   else
   {
      CheckPhoton(pKDNodes[right].tPhoton);
      return;
   }

   d2 = fabsf(d);

   if( d <= 0.0 )
   {
      SearchTree( left, nleft);
      if( d2 <= maxDistance )
         SearchTree( right, nright );
   }
   else
   {
      SearchTree( right, nright );
      if( d2 <= maxDistance )
         SearchTree( left, nleft );
   }
}

Using the GetNearestPhotonsKD Method

This method replaces the previous method but also utilizes the KD Tree. The searching variables are initialized for optimal performance. The reference point is set and the found count is set to zero. The local distance array is set to the local return array passed as a parameter. The maximum photons to return are set and the maximum search distance is set to a very large number so that it includes all photons in the scene. This value will, of course, be updated when the algorithm begins. The SearchTree() method is called and begins searching through the relevant nodes of the KD Tree. When the function is done calling SearchTree(), the nearest count is returned and the passed array is updated with the closest photons found.

long PhotonMap::GetNearestPhotonsKD(photonlist_t tList[],
      long lLength, cVector3 tInter)
{
   tRefPoint        = tInter;
   lFoundCount      = 0;
   tLocalList       = tList;
   tLocalListSize   = lLength;
   maxDistance      = 100000000;
   SearchTree(0,lLocalPhotonCount);
   return lFoundCount;
}

Updating the Photon-Contribution Method

You can now update the photon-contribution method so that it encapsulates the KD Tree algorithm. Based on the preprocessor macro, the KD Tree is utilized or discarded. The new code is listed in bold and is fairly straightforward. The change to the method is minimal with only a few lines of new code.

color3 cScene::PhotonContribution(
       int iObjectIndex,cVector3 tDirection, cVector3 tInter)
{
    // allocate memory
    photonlist_t *pList = new photonlist_t[lMaxPhotonNeighbors];

    long lNumPhotons;
    long i;
    color3 tColor, tTmp;
    float dDot;
    float dPiR2;

    tColor = color3::Black;
#ifdef USE__KD__TREE
    lNumPhotons = GetNearestPhotonsKD((pList,lMaxPhotonNeighbors,tInter);
#else
    lNumPhotons = GetNearestPhotons(pList,lMaxPhotonNeighbors,tInter);
#endif
    // no photons found, omg very bad
    if (lNumPhotons == 0)
       return tColor;
    // for the amount of friends found
    for (i = 0; i < lNumPhotons; i++)
    {
        // is this friend facing the point of intersection
        dDot = pObjectList[ iObjectIndex ].Normal(tInter) *
                 pList[i].tPhoton.tDirection;

        // if not, let's go to next array node
        if (dDot <= 0.0)
        continue;

        // add the power of the photon and the cosine shading method
        tTmp = pList[i].tPhoton.tPower *
         (float) dDot * pObjectList[iObjectIndex ].fDiffuseFactor;

        // sum up everything
        tColor.r = tColor.r + tTmp.r;
        tColor.g = tColor.g + tTmp.g;
        tColor.b = tColor.b + tTmp.b;
     }
    // divide the summed photons by the area of the circle found
    dPiR2 = 3.1415926f *
           pList[lNumPhotons-1].dDistance *
           pList[lNumPhotons-1].dDistance;

    tColor.r = tColor.r / (float) dPiR2;
    tColor.g = tColor.g / (float) dPiR2;
    tColor.b = tColor.b / (float) dPiR2;

    // used to color the photon with the object
    tColor.r *= pObjectList[ iObjectIndex ].Color.r;
    tColor.g *= pObjectList[ iObjectIndex ].Color.g;
    tColor.b *= pObjectList[ iObjectIndex ].Color.b;

    // saturate colors out of unit range
    tColor.Sat();

    // free memory
if(pList) delete [] pList;

   // return final solution
    return tColor;
}


Using the Chapter's Code

The CD materials for this chapter contain an example of the photon mapping application (in /Book Code/Chapter13/Photon Mapping). The general design of the application is the same as the previous applications in this book. The environment of the photon mapping application is exactly the same as the previous application. This application incorporates the KD Tree algorithm. The folder that includes the required classes for this program is in /Book Code/Chapter 13/Photon Mapping/Common. The files in this folder are:

12/21/2002  05:19p        1,381 cLight.h
12/21/2002  05:19p        5,127 cObject.cpp
12/21/2002  05:19p        4,692 cObject.h
12/21/2002  05:19p          442 Color3.cpp
12/21/2002  05:19p        4,480 Color3.h
12/21/2002  05:20p        2,248 cPhoton.cpp
12/21/2002  05:32p        1,408 cPhoton.h
12/21/2002  05:19p           23 cRay.cpp
12/21/2002  05:19p        2,043 cRay.h
12/21/2002  05:19p       34,852 cScene.cpp
12/21/2002  05:23p        3,781 cScene.h
12/19/2002  05:29p        1,991 cVector3.cpp
12/21/2002  05:19p        5,169 cVector3.h
12/21/2002  05:19p        1,060 MathTools.h
12/21/2002  05:19p        1,108 PPM.cpp
12/21/2002  05:19p          154 PPM.h

You compile this program as described in previous chapters. You can also run the program directly by executing the /Book Code/Chapter 13/Photon Mapping/Photon Mapping.exe file. When the program asks you to "Enter Information:", you enter:

/scenes/photon.txt

The program will also ask for how many photons you would like to scatter per object. A good number for spheres is 1000, whereas a good number for triangles is 1000. It will also ask you what the photon contribution is. A good number is 50.

The photon-mapping algorithm will then begin scattering photons. The program starts the ray-tracing process and then again will prompt you to enter the output path for the rendered file stored on the image plane. When it asks "Please Enter output Path and Filename:", enter:

/scenes/myoutput.ppm

You can then browse to that path and open up the file in IrfanView. However, I've already created the exported file for you and placed it in the following folder:

/Book Code/Chapter 13/Photon Mapping/Output

The file in this folder is:

10/13/2002  03:30p           3,212,934 photon.ppm

The program time report is as follows:

Done Tracing Scene!
Render Took 0 Minute(s) and 33 Second(s)...

The application reports the time it took to load and render the scene with photon mapping using the KD Tree algorithm. As you can see, it took 33 seconds to render Scenes/photon.txt. This is much faster compared to the old linear method covered in Chapter 12 (which took 30 minutes and 35 seconds).


Summary

This chapter covered a better method for speeding up the photon-mapping rendering process, which involved attaching the KD Tree to the application environment. Note that the KD Tree isn't equally balanced because photons scatter randomly in the scene. Light scatters in a proportional manner because of occlusion, so balancing the KD Tree is fast at times and slow at others. The KD Tree is essential to photon mapping because searching through a linear (one-dimensional) list isn't optimal for a 3D algorithm like photon mapping. As you've seen, you've increased the speed for retrieving the nearest neighbors by using the KD Tree for photon.txt. The KD Tree is a two- or three-dimensional algorithm best suited for points (or photons) in a multidimensional environment. Using a spatial subdivision algorithm is a must when storing and retrieving photons.


Chapter 14. The Power of Photon Mapping

You have finally completed the algorithm and increased its performance by applying the KD Treenow it's time to reap the rewards of your hard work. Photon mapping includes many of the benefits that each of the previous rendering algorithms included. The goal here is to derive a new algorithm with all of the positive aspects and as few of the problems from the previous two algorithms as possible.

Let's take a look at the different lighting effects that can be simulated using photon mapping. Keep in mind that you can also mix photon mapping with ray tracing. Once you separate the light-ing effects between the two algorithms, you have a very good solution. Photon mapping is better for certain effects, whereas ray tracing is better for others. They, therefore, complement each other well.

NOTE

NOTE

It's important to remember that diffuse lighting on surfaces is called diffuse interaction,whereas diffuse light transferred to other surfaces is called diffuse reflection.


Simulating Diffuse Interactions

Diffuse light reflecting across a surface is easily simulated using photon mapping because the photon map is constructed only by diffuse interactions. Figures 14.1 through 14.3 show some diffuse interactions that were rendered using photon mapping. Figure 14.1 shows an example of diffuse interaction and indirect illumination on a desk model with a pyramid-looking object on top of the desk. The scene includes one light source that casts a shadow for the pyramid object.

Figure 14.1. An example of diffuse interaction and indirect illumination in a desk model with a pyramid object using photon mapping.

Figure 14.2. An example of diffuse interaction and indirect illumination in a desk model with a cup, box, and two light sources using photon mapping.

Figure 14.3. An example of diffuse interaction and indirect illumination in a textured desk model with a sphere, cup, box, and two light sources using photon mapping.

Figure 14.2 shows a scene with two lights that cast two shadows per object. If you look closely, you actually see the penumbra in the shadows.

Figure 14.3 shows diffuse interaction and indirect illumination on a textured desk model with a sphere, cup, and box. This scene has two lights that cast multiple shadows reflected on the desk from the three objects.

Triangles require a relatively low number of photons to look realistic, whereas spheres, due to their curved nature, require a minimum of 10,000 photons. Figure 14.4 shows a comparison of a photon and a sphere.

Figure 14.4. Sphere comparison (1,000 versus 10,000 photons).

    [ LiB ] Chapter 14. The Power of Photon Mapping Simulating Diffuse Reflections


Simulating Diffuse Reflections

Creating diffuse light that's reflecting to other surfaces in the scene is easily accomplished by using photon mapping. Remember, in the photon-tracing process, you traced the diffused energy being reflected to other surfaces in the scene. See Figure 14.5 for an example.

Figure 14.5. An example of diffuse interaction,indi-rect illumination,and diffuse reflection.They are simulated in a box with one face missing using photon mapping.

This process is the same as in radiosity because the same color bleeding effects are simulated. For example, if a white photon reflects a red wall, the reflected photon is red. See Figure 14.6 for an example.

Figure 14.6. An example of diffuse interaction,indi-rect illumination,and diffuse reflection.They are simulated with two spheres next to a polygon.

The problem with using too few photons in diffuse reflection for color-bleeding effects is that it causes blotchy and discolored results. To remedy this problem, a large number of photons are to be used to blend the discoloration. A value of at least 10,000+ photons per object should be used to render diffuse reflected energy. Figure 14.7 shows the difference between 1,000 photons and 10,000 photons per object.

Figure 14.7. Diffuse reflection comparison.

NOTE

NOTE

It isn't a good idea to simulate specular (mirror) reflections in pho-ton mapping.The photon map can grow tremendously in size due to the large number of photons required to reflect light on a mir-ror's surface.


Simulating Specular Reflections

Specular energy reflected in the opposite direction shouldn't be simulated using photon mapping. Remember, a large number of photons are required to reflect a mirror image. It's therefore better to use ray tracing to create mirror reflections. This is done at the time of the rendering process when a new ray is traced in the mirror direction using backward ray tracing. The mirror reflection code used in ray tracing is added to the photon contribution method to combine the lighting effects (ray tracing and photon mapping effects) into one solution. Figure 14.8 shows an example of diffuse interaction done with photon mapping and mirror reflection done with ray tracing.

Figure 14.8. Ray tracing (mirror reflection) and pho-ton mapping (diffuse interaction).

    [ LiB ] Simulating Diffuse Reflections Simulating Caustics with Photon Mapping


Simulating Caustics with Photon Mapping

A very powerful advantage to using photon mapping is its capability to simulate beautiful caustics. Caustics have been around for some time but were poorly simulated using the previous rendering techniques. Caustics occur when multiple specular reflections interact with a diffuse surface. For example, a caustic is generated at the bottom of a champagne glass or the bright focused light transmitted through a glass sphere on a wooden table.

Caustics normally require a large number of photons and, for this reason, many programmers create a separate photon map for storing caustics. This photon map is called the caustic photon map. It helps keep the global photon map to a minimum without overloading it with caustic photons and speeds up the rendering process by searching for fewer photons in the global pho-ton map. The problem with using a caustic photon map is that you need to carefully build the caustic photon map without having duplicates in the global photon map. See Figure 14.9 for an example of caustics.

Figure 14.9. An example of caustics generated from light refracted on the table (diffuse surface) by the sphere (specular surface).

Many programmers implement a special parameter on surfaces for receiving caustics in their applications. It then would be up to the artist who uses the application to set the state of an object to receive caustics. To simulate caustics I used a translucent sphere and refracted the light so that the light would bounce off the sphere and hit the diffuse surface (the table top). Simulating caustics in photon mapping is pretty straightforward.

NOTE

NOTE

Remember,caustics are generated by at least one specular reflection that hits a diffuse surface.If you tried generating caustics with ray tracing,you wouldn't get this effect because ray tracing lacks the capability to handle this type of complex effect.


Simulating Indirect Illumination

Indirectly reflecting light from one surface to another is easily simulated using photon mapping. The photon map was constructed by direct and indirect incoming flux. The photon map is very good at telling the indirect energy in a given area. For this reason, some programmers use photon mapping only for indirect illumination and use ray tracing for all direct illumination effects. The only energy that is recorded in the photon map is the energy that is reflected. This means the photon map is constructed only by reflected energy. Figure 14.10 shows an example of direct illumination done with ray tracing and indirect illumination done with photon mapping.

Figure 14.10. An example of indirect illumination (done with photon mapping) and direct illumination (done with ray tracing).

    [ LiB ] Simulating Caustics with Photon Mapping Creating Better Penumbras in Shadows


Creating Better Penumbras in Shadows

The photon map is so cleverly designed that photons that are reflected can be homed in dark areas in the model contributing very little power. Ray tracing was poor at simulating the penumbras in shadows, but with photon mapping it's an automatic and simple effect. This is because photons sit within dark areas that contribute to a shadow's penumbra. Light in the real world is everywhere and no shadow is completely black. You used the ambient factor to fix this problem with shadows in ray tracing, but this is a forced-looking fix. Ray tracing is poor at calculating shadows because it can't determine if the light is gathered in some small parts of a shadow. This isn't the case with photon mapping because photons contribute to more detail in some lighting effects. See Figure 14.11.

Figure 14.11. Penumbra comparison using a few hundred photons per object.

    [ LiB ] Simulating Indirect Illumination Using the Chapter's Source Code


Using the Chapter's Source Code

You can use the sample files to render images using the photon mapping application. These are scene files that simulate specific lighting effects for photon mapping. The files are located in the path /BookCode/Chapter14/Scenes on the CD. The files are as follows:

  • Diffuse_Interaction1.txt

  • Diffuse_Interaction2.txt

  • Diffuse_Reflection1.txt

  • Diffuse_Reflection2.txt

To run the final program in the book, execute the following file:

/Book Code/Chapter 14/Photon Mapping.exe

The program begins running and will soon prompt you to enter the path for the scene definition file. When it asks "Enter Information:", you enter one of the following:

/scenes/diffuse_interaction1.txt
/scenes/diffuse_interaction2.txt
/scenes/diffuse_reflection1.txt
/scenes/diffuse_reflection2.txt

The program will also ask for how many photons you would like to scatter per object. A good number for spheres is 10000, whereas a good number for triangles is 1000. It will also ask what the photon contribution is. A good range is 50-100.

The photon mapping algorithm will then begin scattering photons. The program starts the ray tracing process and then again will prompt you to enter the output path for the rendered file stored on the image plane. You'll enter one of the following:

/scenes/myoutput1.ppm
/scenes/myoutput2.ppm
/scenes/myoutput3.ppm
/scenes/myoutput4.ppm

You can then browse to that path and open up the files in IrfanView. However, I've already created the exported files for you and placed them in the following folder:

/Book Code/Chapter 14/Output

The files in this folder are:

12/16/2002  03:11p           3,236,471 diffuse_interaction1.ppm
12/16/2002  05:04p           2,964,059 diffuse_interaction2.ppm
12/16/2002  03:09p           3,092,857 diffuse_reflection1.ppm
12/16/2002  05:26p           3,018,282 diffuse_reflection2.ppm

If you don't want to compile or run any programs, you can see an example of some renders I have completed for you (at /Book Code/Chapter 14/Renders). These 17 renders show different examples of lighting effects using ray tracing and photon mapping. Remember you need to have IrfanView installed in order to view them.


Using Photon Mapping in Games

The next logical step to using photon mapping in a practical application is to either place the algorithm into a 3D CAD program or into a video game. With the recent development of the X-Box and Play Station 2, it would be wise for any game development company to add photon mapping to their repertoire. The addition of the algorithm to a game is advantageous due to the heightened realism in lighting effects. Ideally, photon mapping should be done in hardware with the new hardware rasterizers. This adds a tremendous advantage to games with the new color bleeding, indirect illumination, and caustic effects.

So how do you add mapping to a game using software programming? Well, it can be done in two ways. The two photon-mapping methods for lighting in a game are known as pre-processed and real-time. The following sections briefly cover these two techniques.

NOTE

NOTE

Photon mapping has been used in real commercial games such as Ghostmaster by Empire Interactive.

Pre-Processed Photon Mapping

The concept of pre-processing light and storing it as light maps isn't new. For many years, games like Quake and Unreal have used pre-processed light maps for lighting in games. Currently, it is almost impossible to add real-time light-ing to a software application on a PC because PCs are still considered slow when it comes to lighting. With the recent development of hardware rasterizers, lighting effects are possible. Today, there are incredible games like Halo 2, Spider-man, and Grand Theft AutoVice City that have wonderful hardware light-ing. The new hardware boards available today by nVIDIA can render 60-150 million triangles per second with 128MB of video RAM at 60+ frames per second.

Adding light maps to textures in a game isn't a costly process anymore with the high amount of video RAM available. Empire Interactive's Ghostmaster uses photon mapping for lighting in the game. To add photon mapping to a game using the pre-processed method, you extract the rendered lighting from the scene. The lighting information that is extracted represents the intensity shade for each object in the scene. The downside to using this technique is a few lighting effects, such as color bleeding and mirror reflections, are omitted in the process. Extracting the lighting information from the scene in a practical application is very simpleall you need to do is render the photon map and then extract the lighting information as a textured light map for each object in the scene.

This brings up another problem when extracting light maps from the scenerecall that curved surfaces like spheres are a pain in the neck to extract and create light maps from. Figure 14.12 shows a general idea for how to light an object.

Figure 14.12. Light mapping.

Real-Time Photon Mapping

The concept of making photon mapping real-time involves looking at the algorithm and trying to determine how you can increase speed by changing the way you render the photon map. Currently, you pool all photons together in one build. But if you were to use photon mapping in a real-time game, you must change the way you build and render the photon map. For example, a typical build of a global photon map is one million photons. If you were to do this in a game, you would only shoot a few hundred photons per frame based on your current location in the world. Using fewer photons per frame yields a poor estimation of the indirect illumination effects. If the camera is located at one position and changes orientation, the same pre-built photon map in the last frame can be used. This means you can build a cache of photon maps for the last few frames to increase performance in the application. As soon as old photons become stale, you replace them with the newly built photon map.

You add such a real-time process to your game by using vertex shading. You can use DirectX, OpenGL, or the XBox XDK. The photon contribution is applied to each vertex of a triangle in the frustum of the camera. When a triangle is rasterized in hardware, the radiance estimate is applied to each vertex. Figure 14.13 shows an example. This would retain the color bleeding effects that were lost by using the pre-processed method.

Figure 14.13. The pho-ton contribution at each vertex of a triangle.

The general process to implement this technique is as follows:

  1. Build the photon map in each frame by shooting a selective set of photons.

  2. Render each triangle using the radiance estimate for each vertex.

  3. Keep the scattering and rendering passes to a minimum where the frame rate is smooth.

NOTE

NOTE

Real-time lighting using photon mapping seems more easily emulated than radiosity because it's a relatively simple algorithm.


BSP Trees and Photon Mapping

It seems possible to build a leafy BSP tree instead of using the KD Tree to store and retrieve photons for a game. For example if you built a leafy BSP tree you can create a convex hull (cells) of where polygons are located. See Figure 14.14.

Figure 14.14. A room of cells.

This means you build the photon map as you build the BSP tree and store the photons in each leaf of the tree. Figure 14.15 shows a room of cells.

Figure 14.15. The photons contained at each polygon vertex in the subdivided cell.

The list of photons would be stored in each leaf spatially divided. This would be the perfect candidate for a PVS (potentially visible set) system where you can build and render each vertex of a triangle (using the real-time technique) for each frame based on the current cells that are visible. See Figure 14.16.

Figure 14.16. The potentially visible set system processes the pho-tons in the cells that are required for visibility.

This would definitely shorten the rendering time for a game and manage the data in a better fashion.


Summary

This chapter has tried to show that photon mapping is superior to ray tracing and radiosity. It's a much simpler algorithm that achieves the same results. Photon mapping combines all the lighting effects into one solution for a pixel on the image plane during the rendering process. Combining all of these lighting effects into one solution achieves global illumination. Summing the direct illumination, indirect illumination, specular reflections, and caustic illumination in one solution creates a very advanced rendering algorithm. Photon mapping is faster than radiosity because it doesn't require a large amount of memory when rendering complex models. It would be wise to make photon mapping real-time and apply it to games.

This chapter briefly covered some of the undiscovered territory when implementing photon mapping in games. Photon mapping is still relatively new; not many gamers and folks in the graphics community use it. It is up to you to take this information and do great things with it. I hope to see more 3D rendering applications and video games in the near future that are created with photon mapping.


Wrapping Up

There are a variety of techniques that you can achieve using photon mapping and the algorithm can do so much more than was presented in this book. I hope this book gives you a good idea of how to implement photon mapping from a practical perspective. If it has, my job was successful.

This book isn't as easy as reading a DirectX or game programming book; there is lots of undiscovered territory that must be uncovered and explained. The best way to begin to learn more about anything is to search for it on the Web. Search for "photon mapping" in google.com, yahoo.com, infoseek.com, or any search engine to get a start.

This book attempted to give you the basic knowledge you need to implement your own photon-mapping algorithm. The code in this book can be easily changed to a scattering algorithm you are more comfortable with. The scattering algorithm you choose is up to you. For example, if you wanted to maximize speed using photon mapping you would use a simple scattering algorithm.

The photon mapping code is mostly written in C because I want people to understand the algorithm without knowing much about C++. I leave it up to you to take this information and create your own 3D application. When you are done creating your application, please send it to me at photonmapping@yahoo.com. I am open to any questions you have. Thank you for spending your quality time with me.

Marlon John


Team LiB

Before you start

This file is not for reading, but just for acquaintance with the book you are going to buy. Do not ruin your eyes reading it from a monitor or pda. Just decide is the book worth your money. Buy hardcovered books, support authors and publishers.

Like the book? Buy it!