Myopic Rhino

Staff Emeritus
  • Content count

    8805
  • Joined

  • Last visited

Everything posted by Myopic Rhino

  1. So, What Exactly Is OpenAL? OpenAL is a (smallish) API that is designed to aid cross platform development, specifically in the area of sound and music. It is designed to be very simple at the expense of (personally speaking) functionality e.g. Panning is not supported (though some people would claim that today's 3D games would rarely, if ever, use features like that), though through clever coding, it is possible to simulate all the missing features Its style is influenced by OpenGL, in regards to function / variable name layout, type definitions etc. so any seasoned OpenGL coder will easily be able to find their way around, but even to novices, it is simple and easy to understand (even the docs are helpful!). Okay, now you know a little about it, let's see how to use it! Where Can I Get My Hands On It? Of course, the first thing you need to do is actually get the OpenAL SDK. You can do this by going to www.openAL.org . It's only about 7MB, so it shouldn't take that long to do. When you've installed it (for the sake of this article, I installed it at C:\oalsdk), have a quick look at the docs and the examples to see how simple it is to use. Initialization What we need to do first is to set up OpenAL so we can actually use its feature set. This is extremely simple to do: // Init openAL alutInit(0, NULL); // Clear Error Code (so we can catch any new errors) alGetError(); That's it, now we are ready to use it! Sources, Buffers and Listeners Before we carry on, I'll explain the main concept behind OpenAL. The API uses 3 main components: sources, buffers and listeners. Here is a quick explanation of each - Sources: A source in OpenAL is exactly what it sounds like, a source of a sound in the world. A source is linked to one or more buffers, and then asked to play them. They have position and orientation in the world, as well as velocity, for doppler effects. Listener: A listener is (simply put) the ears of the world. It is generally set at the location of the player, so that all sounds will be relative to the person playing. This could be the actual camera location, or some other random position, whatever you want... Buffers: Buffers are exactly like any other kind of sound buffers you may have used. Sound is loaded into the buffer, and then the sound in the buffer is played. The thing that differs from normal is that no functions directly call the buffer. Instead, they are linked to a source (or multiple sources) and then the sources themselves play, playing the sound from the buffers in the order that they have been queued Okay, now that we know about the things that make OpenAL what it is, how do you use them? Well, read on... How Do We Create What We Need? Buffers The first things we should create are buffers, so we can actually store the sound we want to play. This is done with one simple call ALuint buffers[NUM_BUFFERS]; // Create the buffers alGenBuffers(NUM_BUFFERS, buffers); if ((error = alGetError()) != AL_NO_ERROR) { printf("alGenBuffers : %d", error); return 0; } All this does is create however many buffers you wish, ready to be filled with sound. The error checking is there for completeness. The buffers do not need to be created at the start of the program; they can be created at any time. However, it's (in my opinion) neater if you create everything you need when you start. Of course, this isn't always possible, so create them in exactly the same way if you need to. Now, let's load in the sound(s) we want to use ALenum format; ALsizei size; ALsizei freq; ALboolean loop; ALvoid* data; alutLoadWAVFile("exciting_sound.wav", &format, &data, &size, &freq, &loop); if ((error = alGetError()) != AL_NO_ERROR) { printf("alutLoadWAVFile exciting_sound.wav : %d", error); // Delete Buffers alDeleteBuffers(NUM_BUFFERS, buffers); return 0; } This loads in the file we have requested and fills in the given the data. Again, the error check is there for completeness. Note though the call to alDeleteBuffers(...). Even though it has failed, we need to clean up after ourselves, unless of course we want memory leaks ;)... There is also a function (alutLoadWAVMemory(...) ) that lets you load the data from memory. Take a peek at the documentation for notes on that one. Now that we have loaded in the sound, we need to fill up one of the buffers with the data. alBufferData(buffers[0],format,data,size,freq); if ((error = alGetError()) != AL_NO_ERROR) { printf("alBufferData buffer 0 : %d", error); // Delete buffers alDeleteBuffers(NUM_BUFFERS, buffers); return 0; } This takes the data we received from alutLoadWAVFile(...) and puts it into the buffer, ready for it to be assigned to a waiting source and played. Again, notice how we delete the buffers if we fail. Now that the buffer is filled with data, we still have the sound data in memory also, taking up valuable space. Get rid of it with: alutUnloadWAV(format,data,size,freq); if ((error = alGetError()) != AL_NO_ERROR) { printf("alutUnloadWAV : %d", error); // Delete buffers alDeleteBuffers(NUM_BUFFERS, buffers); return 0; } This just deletes the no longer needed sound data from memory, as the data we actually need is stored in the buffer! Sources Okay, so we have loaded the sound into the buffer, but as I said earlier, we can't play it until we have attached it to a source. Here's how to do that! ALuint source[NUM_SOURCES]; // Generate the sources alGenSources(NUM_SOURCES, source); if ((error = alGetError()) != AL_NO_ERROR) { printf("alGenSources : %d", error); return 0; } The above piece of code creates all the sources we need. This is exactly the same procedure as alGenBuffers(...), and the same rules apply. It is common to have far more sources than buffers, as a buffer can be attached to more than one source at the same time. Now that the source is created we need to attach a buffer to it, so we can play it... Attaching buffers to sources is again very easy. There are two ways to do it: One way is to attach a single buffer to the source, whereas the other way is to queue multiple buffers to the source, and play them in turn. For the sake of this article, I'm just going to show you the first way. alSourcei(source[0], AL_BUFFER, buffers[0]); if ((error = alGetError()) != AL_NO_ERROR) { printf("alSourcei : %d", error); return 0; } This takes the buffer and attaches it to the source, so that when we play the source this buffer is used. The function alSourcei(...) is actually used in more than one place (in that it is a generic set integer property function for sources). I won't really go into this here, maybe another day. Now that we have set the source and buffer, we need to position the source so that we can actually simulate 3D sound. The properties we will need to set are position, orientation and velocity. These are pretty self explanatory, but I'll mention velocity a little. Velocity is used to simulate a doppler effect. Personally, I can't stand doppler effects so I set the array to 0's, but if you want the effect, the effect will be calculated in respect to the listener's position (That's coming up next!) Okay to set these values, you use one simple function. ALvoid alSourcefv(ALuint source,ALenum pname,ALfloat *values); This function is similar to the alSourcei(...) function except this is a generic function to set an array of floats as a source property, where ALfloat *values will be an array of 3 ALfloats. So, to set our position, velocity and orientaion we call : alSourcefv (source[0], AL_POSITION, sourcePos); alSourcefv (source[0], AL_VELOCITY, sourceVel); alSourcefv (source[0], AL_DIRECTION, sourceOri); These variables default them selves to 0 if you do not set them, although it is more than likely that you will. I haven't included it here, but it is best to check for errors in the standard way, because it's a good idea to catch them as they happen. Okay, that is a source set up, now we need one more thing - the Listener. Listeners As I said before, the listener is the ears of the world, and as such, you can only have one of them. It wouldn't make sense to have more than one, as OpenAL wouldn't know which one to make the sounds relevant to. Because there is only ever one, we do not need to create it, as it comes ready made. All we have to do is set where it is, the direction it is facing and its velocity (again for doppler effects). This is done in exactly the same way as sources with the one function ALvoid alListenerfv(ALenum pname,ALfloat *values); This works in exactly the same way as alSourcefv(...) and alSourcei(...) except it works on the listener. As I said, the listener comes ready made; you do not have to specify it. To set the position, orientation and velocity call: alListenerfv(AL_POSITION,listenerPos); alListenerfv(AL_VELOCITY,listenerVel); alListenerfv(AL_ORIENTATION,listenerOri); Again, it will be a good idea to check for errors. One thing to note here is that in this case, orientation is an array of 6 ALfloat's . The first three define its look at direction, while the second three define the 'up' vector. Once this is done, everything is finally set up (It wasn't that hard was it?). Now we can get down to the thing we are all here for: Playing the sound at last The Hills Are Alive, With The Sound of Music (!) alSourcePlay(source[0]); Yup, that's it. That's all that is required to play your sample. OpenAL will calculate the volume regarding the distance and the orientation. If you specified a velocity for either sources or listeners you'll also get doppler effects. Now that it's playing, we might need it to stop playing... alSourceStop(source[0]); Or reset it to the start of the sample... alSourceRewind(source[0]); Or pause it... alSourcePause(source[0]); To unpause it, just call alSourcePlay(...) on the sample. 've rushed through those, but they really are that simple. If you have a look at the OpenAL Specification and Reference document, on page 32, it will tell you exactly the actions that will happen if you call these functions on a source, depending on its state. It's simple enough, but I'm not going to go into it now. That's all you need to do to get OpenAL up and running, but what about shutting it down? Shutting It All Down Shutting down is even easier than it was to set everything up. We just need to get rid of our sources, buffers and close OpenAL. alDeleteSources(NUM_SOURCES, source); alDeleteBuffers(NUM_BUFFERS, buffers); This will delete all the sources in that array of sources. Make sure you have deleted all of them before you call the next line. alutExit(); And that's it. OpenAL is now shut down and you can carry on with what ever you want to do next As this is the first article I have written, I hope it is of some use to at least one person. There's lots more to OpenAL, but this simple how-to should easily get you set up so you can look at the more complicated features (if there are any) of the API. Lee Winder Leewinder@hotmail.com
  2. Introduction WHOA! What do you know, I'm finally doing a tutorial on an actual Programming Topic. I think the temperature in Hell must have dropped below 32. The source code examples in this tutorial will be both in Pascal, and C (wherever I can translate it). Since the techniques are cross platform, I won't be showing code on how to Blit the images to the screen. All of the tiles I use are 40x40, and all of the calculations are based on it. Depending on the size of bitmap you use, you may have to scale up or down. What the Hell are Isometric and Hexagonal Maps for? Isometric Maps are maps that use rhombuses instead of squares or rectangles. (A rhombus is a four sided figure, with all sides the same length, but not necessarily 90 degrees at the corners. Yes, a Square is technically a rhombus). Isometric maps give sort of an illusion of being 3d, but without actually being so. Sid Meier's Civilization II uses Isometric Maps. Here is an Isometric Tile: (This tile is actually 40x40, but the rhombus only takes up the bottom 40x21) Hexagonal Maps are maps that use Hexagons (6 sided figures) instead of squares or rectangles. Hexagonal maps are used mostly for overhead view strategy games (The use of these dates back to Avalon Hill, and other strategy game companies). Here is a Hexagonal Tile: Forcing Isometric and Hexagonal Maps onto a Rectangular Grid Okay, you can make Chessboard-like maps all day, you just have to use a 2d array. Spiffy. But Isometric and Hexagonal Maps don't work that way. Every other line is offset. We can still put Iso and Hex maps into a 2d Array, but the WAY in which we map them is different. Here's an IsoMap: Here's a HexMap: As demonstrated in the above pictures, for odd Y values, you shift the line over by half of a tile (20 pixels, in my case). (The White Spaces are border tiles not on the map. Usually, you would fill these with black.) Plotting the Iso/Hex Tiles on the Map Since both Iso and Hex tiles are contained in overlapping rectangles, you MUST USE BITMASKS! My Iso Tiles, and the Iso BitMask: My Hex Tiles, and the Hex BitMask: {The Brief Review of BitMasking: You blit the bitmask using AND, then Blit the Tile using OR.} Pixel Coordinates of Iso/Hex Tiles When calculating X,Y coordinates for Rectangular tiles, you use the following calculations: PlotX=MapX*Width PlotY=MapY*Height For Iso/Hex maps, it's a little trickier, since the bounding rectangles overlap. Iso Maps: {(MapY AND 1) tells us if MapY is odd or even, and shifts the tile to the right if it is odd} PlotX=MapX*Width+(MapY AND 1)*(Width/2) PlotY=MapY*HeightOverLapping-YOffset Important: Width should always be an even number, or you wind up with a black zigzag line between rows of tiles {This assumes you have shaped your rhombus like mine, with one pixel} {at the left and right corners, and two at the top and bottom.} HeightOverLapping=(Height of Rhombus)/2+1 {to make the first row flush with the top of the map} Yoffset=Height-(Height of Rhombus) HexMaps: PlotX=MapX*Width+(MapY AND 1)*(Width/2) PlotY=MapY*HeightOverLapping HeightOverLapping=(Height of Hexagon)*0.75 {Assuming your hexagon looks like mine} Moving Around in Iso/Hex Maps In Rectangular maps, movement from square to square is easy. Just add/subtract 1 to X and/or Y, and you have made the move. Iso and Hex maps make THAT more difficult, as well. Due to the fact that every other line is offset, there are different calculations, depending if whatever is moving is on an Even Row, or an Odd Row. Isometric Directions: For coding purposes, we will give names to these directions: {Pascal} Const IsoEast=0; IsoSouthEast=1; IsoSouth=2; IsoSouthWest=3; IsoWest=4; IsoNorthWest=5; IsoNorth=6; IsoNorthEast=7; {C} #define ISOEAST 0 #define ISOSOUTHEAST 1 #define ISOSOUTH 2 #define ISOSOUTHWEST 3 #define ISOWEST 4 #define ISONORTHWEST 5 #define ISONORTH 6 #define ISONORTHEAST 7 Hexagonal Directions: The names for these directions: {Pascal} Const HexEast=0; HexSouthEast=1; HexSouthWest=2; HexWest=3; HexNorthWest=4; HexNorthEast=5; {C} #define HEXEAST 0 #define HEXSOUTHEAST 1 #define HEXSOUTHWEST 2 #define HEXWEST 3 #define HEXNORTHWEST 4 #define HEXNORTHEAST 5 Here is a table of DX(Change In X), and DY(Change in Y) for each direction on the Iso and Hex maps, divided into two lists, one for even Y values, and one for odd Y values. As you can see, DeltaY is the same, no matter what row you are on. Only DeltaX changes. Also, For the Cardinal Directions (North, East, South, and West), DeltaX is the same no matter what row you are on. Its only diagonal movement that is tricky. So now, let's build a few functions: IsoDeltaX, IsoDeltaY, HexDeltaX, and HexDeltaY. {Pascal} {To find out how we should modify X in order to move a given direction.} {Dir is direction of intended movement, and OddRow is whether or not the} {current Y position is odd or even. you can feed the expression ((Y and 1)=1} Function IsoDeltaX(Dir:byte;OddRow:boolean):integer; Var Temp:integer; Begin Temp:=0; {The default change in X is 0. We'll only modify it if we have to} Case Dir of IsoEast: Temp:=1; IsoWest: Temp:=-1; IsoNorth: Temp:=Temp-2; IsoSouth: Temp:=Temp+2; IsoSouthEast, IsoNorthEast: If OddRow then Temp:=1;{If Not OddRow, then leave as 0} IsoSouthWest, IsoNorthWest: If Not OddRow then Temp:=-1; {If OddRow, the leave as 0} End; IsoDeltaX:=Temp;{Return the Value} End; {To find out how we should modify Y in order to move in a given direction.} {Dir is the direction of intended movement} Function IsoDeltaY(Dir:byte):integer; Var Temp:integer; Begin Temp:=0;{Default Value of 0. We will change it only if we have to} Case Dir of IsoNorth: Temp:=-2; IsoSouth: Temp:=2; IsoNorthWest, IsoNorthEast: Temp:=-1; IsoSouthWest,IsoSouthEast: Temp:=1; End; IsoDeltaY:=Temp;{Return the value} End; Function HexDeltaX(Dir:byte;OddRow:boolean):integer; Var Temp:integer; Begin Temp:=0; Case Dir of HexEast: Temp:=1; HexWest: Temp:=-1; HexSouthEast, HexNorthEast: If OddRow then Temp:=1; HexSouthWest, HexNorthWest: If Not OddRow then Temp:=-1; End; HexDeltaX:=Temp; End; Function HexDeltaY(Dir:byte):integer; Var Temp:integer; Begin Temp:=0; Case Dir of HexNorthWest, HexNorthEast: Temp:=-1; HexSouthWest,HexSouthEast: Temp:=1; End; HexDeltaY:=Temp; End; {C} int isodeltax(unsigned char dir, BOOL oddrow) { int temp=0; switch(dir) { case ISOEAST: temp=1; break; case ISOWEST: temp=-1;break; case ISOSOUTHEAST: case ISONORTHEAST: if (oddrow==TRUE) temp=1; break; case ISOSOUTHWEST: case ISONORTHWEST: if (oddrow==FALSE) temp=-1;break; } return(temp); } int isodeltay(unsigned char dir) { int temp=0; switch(dir) { case ISONORTH: temp=-2;break; case ISOSOUTH: temp=2;break; case ISOSOUTHEAST: case ISOSOUTHWEST: temp=1;break; case ISONORTHEAST: case ISONORTHWEST: temp=-1;break; } return(temp); } int hexdeltax(unsigned char dir, BOOL oddrow) { int temp=0; switch(dir) { case HEXEAST: temp=1; break; case HEXWEST: temp=-1;break; case HEXSOUTHEAST: case HEXNORTHEAST: if (oddrow==TRUE) temp=1; break; case HEXSOUTHWEST: case HEXNORTHWEST: if (oddrow==FALSE) temp=-1;break; } return(temp); } int hexdeltay(unsigned char dir) { int temp=0; switch(dir) { case HEXSOUTHEAST: case HEXSOUTHWEST: temp=1;break; case HEXNORTHEAST: case HEXNORTHWEST: temp=-1;break; } return(temp); } Facing and Turning In some games, like strategy games, as well as others, the direction that something on a tile is facing is just as important as what tile they are on. (for things like arc fire, etc.) Keeping track of facing is no big deal. It's just a byte (char) that keeps track of the unit's direction (0 to 7 for Iso, 0 to 5 for Hex) For Turning the unit, we may want to have a function or two, as well as some turning constants. In Iso, we turn in increments of 45 degrees, in Hex, we turn in increments of 60. {Pascal} Const {Iso Turning Constants} IsoTurnNone=0; IsoTurnRight45=1; IsoTurnRight90=2; IsoTurnRight135=3; IsoTurnAround=4; IsoTurnLeft135=5; IsoTurnLeft90=6; IsoTurnLeft45=7; {Hex Turning Constants} HexTurnNone=0; HexTurnRight60=1; HexTurnRight120=2; HexTurnAround=3; HexTurnLeft120=4; HexTurnLeft60=5; Function IsoTurn(Dir,Turn:byte):byte; Begin IsoTurn:=(Dir+Turn) AND 7; End; Function HexTurn(Dir,Turn:byte):byte; Begin HexTurn:=(Dir+Turn) MOD 6; End; {C} /*Iso Turn Constants*/ #define ISOTURNNONE 0 #define ISOTURNRIGHT45 1 #define ISOTURNRIGHT90 2 #define ISOTURNRIGHT135 3 #define ISOTURNAROUND 4 #define ISOTURNLEFT135 5 #define ISOTURNLEFT90 6 #define ISOTURNLEFT45 7 /*Hex Turn Constants*/ #define HEXTURNNONE 0 #define HEXTURNRIGHT60 1 #define HEXTURNRIGHT120 2 #define HEXTURNAROUND 3 #define HEXTURNLEFT120 4 #define HEXTURNLEFT60 5 unsigned char isoturn(unsigned char dir, unsigned char turn) { return((dir+turn) & 7); } unsigned char hexturn(unsigned char dir, unsigned char turn) { return((dir+turn) % 6); } Mouse Matters Another major difficulty of Iso/Hex mapping is the mouse cursor. This was one of my difficulties for a long time. Then, I took a look at one of the GIFs that shipped with Civilization II. It had a little picture, kind of like this: AHA! I said. Then I understood. We don't have to do bizarre calculations in order to figure out what tile we're on! We just divide the screen (or map) into little rectangles like the one above, figure out where in a given rectangle our mouse is, and find the color on the picture above that corresponds! This will allow us to figure out which tile our mouse is hovering over. (After stumbling on to this epiphany, I promptly smacked myself in the forehead and said "DUH!") I call the above picture the Isometric MouseMap. Here's how to use it. (For Hex Maps, use the same algorithm, but with the following MouseMap: ) First Step: Find out what region of the map the mouse is in. RegionX=int(MouseX/MouseMapWidth) RegionY=int(MouseY/MouseMapHeight)*2 {The multiplying by two is very important} Second Step: Find out WHERE in the mousemap our mouse is, by finding MouseMapX and MouseMapY. MouseMapX=MouseX MOD MouseMapWidth MouseMapY=MouseY MOD MouseMapHeight Third Step: Determine the color in the MouseMap at (MouseMapX,MouseMapY). Fourth Step: Find RegionDX and RegionDY in the following table Fifth Step: Use RegionX,RegionY, RegionDX, and RegionDY to find out TileX and TileY TileX=RegionX+RegionDX TileY=RegionY+RegionDY Next Time: I will discuss putting Objects onto our Iso/Hex tiles, with a minimum of muss and fuss, and proper screen updating, so you don't have to draw the whole map every time.
  3. Global illumination (GI) is a term used in computer graphics to refer to all lighting phenomena caused by interaction between surfaces (light rebounding off them, refracting, or getting blocked), for example: color bleeding, caustics, and shadows. Many times the term GI is used to refer only to color bleeding and realistic ambient lighting. Direct illumination - light that comes directly from a light source - is easily computed in real-time with today's hardware, but we can't say the same about GI because we need to gather information about nearby surfaces for every surface in the scene and the complexity of this quickly gets out of control. However, there are some approximations to GI that are easier to manage. When light travels through a scene, rebounding off surfaces, there are some places that have a smaller chance of getting hit with light: corners, tight gaps between objects, creases, etc. This results in those areas being darker than their surroundings. This effect is called ambient occlusion (AO), and the usual method to simulate this darkening of certain areas of the scene involves testing, for each surface, how much it is "occluded" or "blocked from light" by other surfaces. Calculating this is faster than trying to account for all global lighting effects, but most existing AO algorithms still can't run in real-time. Real-time AO was out of the reach until Screen Space Ambient Occlusion (SSAO) appeared. SSAO is a method to approximate ambient occlusion in screen space. It was first used in games by Crytek, in their "Crysis" franchise and has been used in many other games since. In this article I will explain a simple and concise SSAO method that achieves better quality than the traditional implementation. The SSAO in Crysis Prerequisites The original implementation by Crytek had a depth buffer as input and worked roughly like this: for each pixel in the depth buffer, sample a few points in 3D around it, project them back to screen space and compare the depth of the sample and the depth at that position in the depth buffer to determine if the sample is in front (no occlusion) or behind a surface (it hits an occluding object). An occlusion buffer is generated by averaging the distances of occluded samples to the depth buffer. However this approach has some problems (such as self occlusion, haloing) that I will illustrate later. The algorithm I describe here does all calculations in 2D, no projection is needed. It uses per-pixel position and normal buffers, so if you're using a deferred renderer you have half of the work done already. If you're not, you can try to reconstruct position from depth or you can store per-pixel position directly in a floating point buffer. I recommend the later if this is your first time implementing SSAO as I will not discuss position reconstruction from depth here. Either way, for the rest of the article I'll assume you have both buffers available. Positions and normals need to be in view space. What we are going to do in this article is exactly this: take the position and normal buffer, and generate a one-component-per-pixel occlusion buffer. How to use this occlusion information is up to you; the usual way is to subtract it from the ambient lighting in your scene, but you can also use it in more convoluted or strange ways for NPR (non-photorealistic) rendering if you wish. Algorithm Given any pixel in the scene, it is possible to calculate its ambient occlusion by treating all neighboring pixels as small spheres, and adding together their contributions. To simplify things, we will work with points instead of spheres: occluders will be just points with no orientation and the occludee (the pixel which receives occlusion) will be a pair. Then, the occlusion contribution of each occluder depends on two factors: Distance "d" to the occludee. Angle between the occludee's normal "N" and the vector between occluder and occludee "V". With these two factors in mind, a simple formula to calculate occlusion is: Occlusion = max( 0.0, dot( N, V) ) * ( 1.0 / ( 1.0 + d ) ) The first term, max( 0.0, dot( N,V ) ), works based on the intuitive idea that points directly above the occludee contribute more than points near it but not quite right on top. The purpose of the second term ( 1.0 / ( 1.0 + d ) ) is to attenuate the effect linearly with distance. You could choose to use quadratic attenuation or any other function, it's just a matter of taste. The algorithm is very easy: sample a few neighbors around the current pixel and accumulate their occlusion contribution using the formula above. To gather occlusion, I use 4 samples (,,,) rotated at 45o and 90o, and reflected using a random normal texture. Some tricks can be applied to accelerate the calculations: you can use half-sized position and normal buffers, or you can also apply a bilateral blur to the resulting SSAO buffer to hide sampling artifacts if you wish. Note that these two techniques can be applied to any SSAO algorithm. This is the HLSL pixel shader code for the effect that has to be applied to a full screen quad: sampler g_buffer_norm; sampler g_buffer_pos; sampler g_random; float random_size; float g_sample_rad; float g_intensity; float g_scale; float g_bias; struct PS_INPUT { float2 uv : TEXCOORD0; }; struct PS_OUTPUT { float4 color : COLOR0; }; float3 getPosition(in float2 uv) { return tex2D(g_buffer_pos,uv).xyz; } float3 getNormal(in float2 uv) { return normalize(tex2D(g_buffer_norm, uv).xyz * 2.0f - 1.0f); } float2 getRandom(in float2 uv) { return normalize(tex2D(g_random, g_screen_size * uv / random_size).xy * 2.0f - 1.0f); } float doAmbientOcclusion(in float2 tcoord,in float2 uv, in float3 p, in float3 cnorm) { float3 diff = getPosition(tcoord + uv) - p; const float3 v = normalize(diff); const float d = length(diff)*g_scale; return max(0.0,dot(cnorm,v)-g_bias)*(1.0/(1.0+d))*g_intensity; } PS_OUTPUT main(PS_INPUT i) { PS_OUTPUT o = (PS_OUTPUT)0; o.color.rgb = 1.0f; const float2 vec[4] = {float2(1,0),float2(-1,0), float2(0,1),float2(0,-1)}; float3 p = getPosition(i.uv); float3 n = getNormal(i.uv); float2 rand = getRandom(i.uv); float ao = 0.0f; float rad = g_sample_rad/p.z; //**SSAO Calculation**// int iterations = 4; for (int j = 0; j < iterations; ++j) { float2 coord1 = reflect(vec[j],rand)*rad; float2 coord2 = float2(coord1.x*0.707 - coord1.y*0.707, coord1.x*0.707 + coord1.y*0.707); ao += doAmbientOcclusion(i.uv,coord1*0.25, p, n); ao += doAmbientOcclusion(i.uv,coord2*0.5, p, n); ao += doAmbientOcclusion(i.uv,coord1*0.75, p, n); ao += doAmbientOcclusion(i.uv,coord2, p, n); } ao/=(float)iterations*4.0; //**END**// //Do stuff here with your occlusion value AcaoAc: modulate ambient lighting, write it to a buffer for later //use, etc. return o; } The concept is very similar to the image space approach presented in "Hardware Accelerated Ambient Occlusion Techniques on GPUs" [1] the main differences being the sampling pattern and the AO function. It can also be understood as an image-space version of "Dynamic Ambient Occlusion and Indirect Lighting" [2] Some details worth mentioning about the code: The radius is divided by p.z, to scale it depending on the distance to the camera. If you bypass this division, all pixels on screen will use the same sampling radius, and the output will lose the perspective illusion. During the for loop, coord1 are the original sampling coordinates, at 90o. coord2 are the same coordinates, rotated 45o. The random texture contains randomized normal vectors, so it is your average normal map. This is the random normal texture I use: It is tiled across the screen and then sampled for each pixel, using these texture coordinates: [font=Courier New]g_screen_size * uv / random_size [/font] Where "g_screen_size" contains the width and height of the screen in pixels and "random_size" is the size of the random texture (the one I use is 64x64). The normal you obtain by sampling the texture is then used to reflect the sampling vector inside the for loop, thus getting a different sampling pattern for each pixel on the screen. (check out "interleaved sampling" in the references section) At the end, the shader reduces to iterating through some occluders, invoking our AO function for each of them and accumulating the results. There are four artist variables in it: g_scale: scales distance between occluders and occludee. g_bias: controls the width of the occlusion cone considered by the occludee. g_sample_rad: the sampling radius. g_intensity: the ao intensity. Once you tweak the values a bit and see how the AO reacts to them, it becomes very intuitive to achieve the effect you want. Results a) raw output, 1 pass 16 samples b] raw output, 1 pass 8 samples c) directional light only d) directional light - ao, 2 passes 16 samples each. As you can see, the code is short and simple, and the results show no self occlusion and very little to no haloing. These are the two main problems of all the SSAO algorithms that use only the depth buffer as input, you can see them in these images: ? The self-occlusion appears because the traditional algorithm samples inside a sphere around each pixel, so in non-occluded planar surfaces at least half of the samples are marked as 'occluded'. This yields a grayish color to the overall occlusion. Haloing causes soft white edges around objects, because in these areas self-occlusion does not take place. So getting rid of self-occlusion actually helps a lot hiding the halos. The resulting occlusion from this method is also surprisingly consistent when moving the camera around. If you go for quality instead of speed, it is possible to use two or more passes of the algorithm (duplicate the for loop in the code) with different radiuses, one for capturing more global AO and other to bring out small crevices. With lighting and/or textures applied, the sampling artifacts are less apparent and because of this, usually you should not need an extra blurring pass. Taking it further I have described a down-to-earth, simple SSAO implementation that suits games very well. However, it is easy to extend it to take into account hidden surfaces that face away from the camera, obtaining better quality. Usually this would require three buffers: two position/depth buffers (front/back faces) and one normal buffer. But you can do it with only two buffers: store depth of front faces and back faces in red and green channels of a buffer respectively, then reconstruct position from each one. This way you have one buffer for positions and a second buffer for normal. These are the results when taking 16 samples for each position buffer: left: front faces occlusion, right: back faces occlusion To implement it just and extra calls to "doAmbientOcclusion()" inside the sampling loop that sample the back faces position buffer when searching for occluders. As you can see, the back faces contribute very little and they require doubling the number of samples, almost doubling the render time. You could of course take fewer samples for back faces, but it is still not very practical. This is the extra code that needs to be added: inside the for loop, add these calls: ao += doAmbientOcclusionBack(i.uv,coord1*(0.25+0.125), p, n); ao += doAmbientOcclusionBack(i.uv,coord2*(0.5+0.125), p, n); ao += doAmbientOcclusionBack(i.uv,coord1*(0.75+0.125), p, n); ao += doAmbientOcclusionBack(i.uv,coord2*1.125, p, n); Add these two functions to the shader: float3 getPositionBack(in float2 uv) { return tex2D(g_buffer_posb,uv).xyz; } float doAmbientOcclusionBack(in float2 tcoord,in float2 uv, in float3 p, in float3 cnorm) { float3 diff = getPositionBack(tcoord + uv) - p; const float3 v = normalize(diff); const float d = length(diff)*g_scale; return max(0.0,dot(cnorm,v)-g_bias)*(1.0/(1.0+d)); } Add a sampler named "g_buffer_posb" containing the position of back faces. (draw the scene with front face culling enabled to generate it) Another small change that can be made, this time to improve speed instead of quality, is adding a simple LOD (level of detail) system to our shader. Change the fixed amount of iterations with this: [font=Courier New]int iterations = lerp(6.0,2.0,p.z/g_far_clip); [/font] The variable "g_far_clip" is the distance of the far clipping plane, which must be passed to the shader. Now the amount of iterations applied to each pixel depends on distance to the camera. Thus, distant pixels perform a coarser sampling, improving performance with no noticeable quality loss. I've not used this in the performance measurements (below), however. Conclusion and Performance Measurements As I said at the beginning of the article, this method is very well suited for games using deferred lighting pipelines because it requires two buffers that are usually already available. It is straightforward to implement, and the quality is very good. It solves the self-occlusion issue and reduces haloing, but apart from that it has the same limitations as other screen-space ambient occlusion techniques: Disadvantages: Does not take into account hidden geometry (especially geometry outside the frustum). The performance is very dependent on sampling radius and distance to the camera, since objects near the front plane of the frustum will use bigger radiuses than those far away. The output is noisy. Speed wise, it is roughly equal to a 4x4 Gaussian blur for a 16 sample implementation, since it samples only 1 texture per sample and the AO function is really simple, but in practice it is a bit slower. Here's a table showing the measured speed in a scene with the Hebe model at 900x650 with no blur applied on a Nvidia 8800GT: SettingsFPSSSAO time (ms)High (32 samples front/back) 150 3.3Medium (16 samples front)2900.27Low (8 samples front)3100.08 In these last screenshots you can see how this algorithm looks when applied to different models. At highest quality (32 samples front and back faces, very big radius, 3x3 bilateral blur): At lowest quality (8 samples front faces only, no blur, small radius): It is also useful to consider how this technique compares to ray-traced AO. The purpose of this comparison is to see if the method would converge to real AO when using enough samples. Left: the SSAO presented here, 48 samples per pixel (32 for front faces and 16 for back faces), no blur. Right: Ray traced AO in Mental Ray. 32 samples, spread = 2.0, maxdistance = 1.0; falloff = 1.0. One last word of advice: don't expect to plug the shader into your pipeline and get a realistic look automatically. Despite this implementation having a good performance/quality ratio, SSAO is a time consuming effect and you should tweak it carefully to suit your needs and obtain the best performance possible. Add or remove samples, add a bilateral blur on top, change intensity, etc. You should also consider if SSAO is the way to go for you. Unless you have lots of dynamic objects in your scene, you should not need SSAO at all; maybe light maps are enough for your purpose as they can provide better quality for static scenes. I hope you will benefit in some way from this method. All code included in this article is made available under the MIT license References [1] Hardware Accelerated Ambient Occlusion Techniques on GPUs (Perumaal Shanmugam) [2] Dynamic Ambient Occlusion and Indirect Lighting (Michael Bunnell) [3] Image-Based Proxy Accumulation for Real-Time Soft Global Illumination (Peter-Pike Sloan, Naga K. Govindaraju, Derek Nowrouzezahrai, John Snyder) [4] Interleaved Sampling (Alexander Keller, Wolfgang Heidrich) Crytek's Sponza rendered at 1024x768, 175 fps with a directional light. The same scene rendered at 1024x768, 110 fps using SSAO medium settings: 16 samples, front faces, no blur. Ambient lighting has been multiplied by (1.0-AO). The Sponza model was downloaded from Crytek's website.
  4. Happy Father's Day to both of my dads! See you both in a few weeks.
  5. Happy half-birthday to my best friend and love of my life!
  6. On the train, I overheard a group of people talking about the NSA getting phone records from Verizon. One woman stated that once her contract is up, she's canceling her service with Verizon. Another said that we shouldn't worry about it because NPR said it isn't a big deal. /facepalm. This is why we can't have nice things.
  7. I think I just found my birth mother...
  8. Dave Foley tonight :D
  9. I just received a robo-spam (i.e. our sites cover similar topics) from a speech pathology site because GameDev.net has a thread about LISP...
  10. Talking to marketing people is making me feel stabby
  11. Can't sleep, clown will eat me
  12. Thanks for all of the birthday wishes! I had one of the weirdest birthdays ever, but it dampened the blow of turning 40, and I had a good time. Plus, I got my suitcase back :)
  13. Useless coworkers are useless
  14. Just tuned in to the debates to see the "analysts" demonizing Ron Paul for his views on Iran. What a trio of loathsome trolls.
  15. Me: Are you mad at me? Tyler: That depends. What did you actually do? Me: I just summoned Cthulhu. Tyler: ...
  16. Nate: "How do you spell everything?" Evan: "That's going to take a while, Nate"
  17. Should I be concerned by a resume from a guy with an interest in "human-computer interaction" and experience working with "vibro-tactile interfaces"?
  18. I'm watching Conan get his ordination as a minister from the same website I got mine. Awesome! :)
  19. I am looking for people to do tools development for mobile graphics. These are full-time, permanent positions with excellent compensation and benefits. If you are interested or know someone who might be, get in touch with me. I'll post a link to an actual job listing soon.
  20. Looking for someone in Logan with a red hot poker and lots of free time
  21. Battlefield 3 can suck it
  22. I did not buy nearly enough Mr. Clean Magic Erasers.
  23. I just secured a new home for Amy and I to live in! One more (big) thing off the list of things to do before the wedding/move.