2D water shader in SFML - c++

I want to implement the algorithm for a 2D water surface described here and here.
But instead of using two int arrays and calculating on the CPU I would like to use SFML's sf::RenderTexture's (FBO's basically) and a GLSL shader to run everything on the GPU. I want to use SFML, because it's so simple and I have worked with it before, so I know my way around it a little.
I've made some good progress so far. I was able to set up 3 sf::RenderTextures and ping-pong between them correctly (because other than int array you can't read and write to the same sf::RenderTexture at the same time). I was also able to adapt the algorithm for the height field creation form being in the range -32.767 to 32.767 to the range 0 to 1 (or to be more precise -0.5 to 0.5 for the calculation). Also adding new ripples works to some extend. So up to this point you can actually see a little of waves going on.
Here comes my problem now: The waves disappear really, really fast and I don't even apply any damping yet. According to the algorithm the ripples are not stopping if there is no damping applied. It's even the other way around. If I apply "amplification" the waves look close to what you would expect them to look like (but they still disappear without any damping applied to them). My first thought was that this is, because I use float's in range 0 - 1 instead of integers, but I only see this being a problem if multiplication is used, but I only use addition and subtraction.
Here is my SFML C++ code :
#include <SFML/Graphics.hpp>
#include <iostream>
int main()
{
sf::RenderWindow window(sf::VideoMode(1000, 1000), "SFML works!");
window.setFramerateLimit(12);
sf::RenderTexture buffers[3];
buffers[0].create(500, 500);
buffers[1].create(500, 500);
buffers[2].create(500, 500);
sf::RenderTexture* firstBuffer = buffers;
sf::RenderTexture* secondBuffer = &buffers[1];
sf::RenderTexture* finalBuffer = &buffers[2];
firstBuffer->clear(sf::Color(128, 128, 128));
secondBuffer->clear(sf::Color(128, 128, 128));
finalBuffer->clear(sf::Color(128, 128, 128));
sf::Shader waterHeightmapShader;
waterHeightmapShader.loadFromFile("waterHeightmapShader.glsl", sf::Shader::Fragment);
sf::Sprite spritefirst;
spritefirst.setPosition(0, 0);
spritefirst.setTexture(firstBuffer->getTexture());
sf::Sprite spritesecond;
spritesecond.setPosition(500, 0);
spritesecond.setTexture(secondBuffer->getTexture());
sf::Sprite spritefinal;
spritefinal.setPosition(0, 500);
spritefinal.setTexture(finalBuffer->getTexture());
while (window.isOpen())
{
sf::Event event;
while (window.pollEvent(event))
{
if(event.type == sf::Event::Closed)
window.close();
if(event.type == sf::Event::KeyReleased && event.key.code == sf::Keyboard::Escape)
window.close();
}
waterHeightmapShader.setParameter("mousePosition", sf::Vector2f(-1.f, -1.f));
// if mouse button is pressed add new ripples
if(sf::Mouse::isButtonPressed(sf::Mouse::Left))
{
sf::Vector2i mousePosition = sf::Mouse::getPosition(window);
if(mousePosition.x < 500 && mousePosition.y < 500)
{
sf::Vector2f mouse(mousePosition);
mouse.x /= 500.f;
mouse.y /= 500.f;
mouse.y = 1 - mouse.y;
std::cout << mouse.x << " " << mouse.y << std::endl;
waterHeightmapShader.setParameter("mousePosition", mouse);
}
}
waterHeightmapShader.setParameter("textureTwoFramesAgo", firstBuffer->getTexture());
waterHeightmapShader.setParameter("textureOneFrameAgo", secondBuffer->getTexture());
// create the heightmap
secondBuffer->display();
finalBuffer->clear(sf::Color(128, 128, 128));
finalBuffer->draw(sf::Sprite(secondBuffer->getTexture()), &waterHeightmapShader);
finalBuffer->display();
spritefirst.setTexture(firstBuffer->getTexture());
spritesecond.setTexture(secondBuffer->getTexture());
spritefinal.setTexture(finalBuffer->getTexture());
window.clear();
window.draw(spritefirst);
window.draw(spritesecond);
window.draw(spritefinal);
window.display();
// swap the buffers around, first becomes second, second becomes third and third becomes first
sf::RenderTexture* swapper = firstBuffer;
firstBuffer = secondBuffer;
secondBuffer = finalBuffer;
finalBuffer = swapper;
}
return 0;
}
And here is my GLSL shader code :
uniform sampler2D textureTwoFramesAgo;
uniform sampler2D textureOneFrameAgo;
uniform vec2 mousePosition;
const float textureSize = 500.0;
const float pixelSize = 1.0 / textureSize;
void main()
{
// pixels position
vec2 position = gl_TexCoord[0].st;
vec4 finalColor = ((texture2D(textureOneFrameAgo, vec2(position.x - pixelSize, position.y)) +
texture2D(textureOneFrameAgo, vec2(position.x + pixelSize, position.y)) +
texture2D(textureOneFrameAgo, vec2(position.x, position.y + pixelSize)) +
texture2D(textureOneFrameAgo, vec2(position.x, position.y - pixelSize)) - 2.0) / 2) -
(texture2D(textureTwoFramesAgo, position) - 0.5);
// damping
// finalColor.rgb *= 1.9; // <---- uncomment this for the "amplifiction" ie. to see the waves better
finalColor.rgb += 0.5;
// add new ripples
if(mousePosition.x > 0.0)
{
if(distance(position, mousePosition) < pixelSize * 5)
{
finalColor = vec4(0.9, 0.9, 0.9, 1.0);
}
}
gl_FragColor = finalColor;
}
Please remember that this is all just about the height field creation. There is no shading of the water yet.
Do you know why the waves disappear by them self without damping?

If I am reading the code correctly you sample the previous frame for the texture's colors/height and use four neighboring pixels/texels to determine the color/height of the current pixel.
As you are calculating (scaling) these neighbors you might run into missing the texel that contains the color/height you are looking for. It might not be the heighest texel, just one next to it a little bit lower causing the unexpected damping.
This is where you do not just use addition and subtraction:
const float pixelSize = 1.0 / textureSize;
By using this value you could just miss the texel you are looking for.
EDIT
Also: you are averaging the samples so the result will always be less than the maximum value of the samples. So instead of averaging you could select the maximum value. That might give weird results but also extra insight.

Here are some "Processing" codes which implements the same algorithm you've posted above, and its damping is correct, I hope you can get some points from it :
// codes begin
int Width = 800;
int Height = 600;
int FullSize = 0;
//int Spacing = 10;
int[] source, dest;
PImage bg;
void setup()
{
// if you want to run these codes by "Processing"
// please make a picture named "HelloWorld.png"
bg = loadImage("HelloWorld.png");
Width = bg.width;
Height = bg.height;
FullSize = Width * Height;
size(Width, Height);
source = new int[FullSize];
dest = new int[FullSize];
for (int i=0; i< FullSize; i++)
source[i] = dest[i] = 0;
}
void draw()
{
for (int i=Width; i< FullSize-Width; i++)
{
// check for bounds
int xi = i % Width;
if ((xi==0) || (xi==Width-1)) continue;
dest[i] = (
((source[i-1]+
source[i+1]+
source[i-Width]+
source[i+Width]) >>1) ) -dest[i];
int dampFactor = 1000;
dest[i] -= (dest[i] >> dampFactor); // Damping - Quick divde by 32 (5 bits)
}
//image(bg, 0, 0);
loadPixels();
for (int i=Width; i< FullSize-Width; i++)
{
// check for bounds
int xi = i % Width;
if ((xi==0) || (xi==Width-1)) continue;
int xoffset = dest[i-1] - dest[i+1];
int yoffset = dest[i-Width] - dest[i+Width];
int offset = i+xoffset+yoffset*Width;
if (offset>0 && offset<FullSize)
{
// TODO: make better map
pixels[i] = bg.pixels[offset];
}
}
//bg.updatePixels();
updatePixels();
//swap
int[] temp = source;
source = dest;
dest = temp;
}
void mouseDragged()
{
if (mouseX > 0 && mouseX < Width && mouseY > 0 && mouseY < Height)
source[mouseY*Width+mouseX] = (int)random(50, 100);
}
void mousePressed()
{
// TODO: make a area pulse value, like a radius circle
if (mouseX > 0 && mouseX < Width && mouseY > 0 && mouseY < Height)
source[mouseY*Width+mouseX] = (int)random(50, 100);
}
// codes end

Related

Checking if vector passes through vertices

I have been struggling with this problem for over a month, so I really need your help.
To further elaborate on the question :
The question is whether a vector called 'direction' that starts at a vertex called 'start' passes through the 'taget'.
You need to confirm the direction and distance.
I decided that using the dot product was impossible because I went through enough debugging.
The result is good when calculated directly, but why is the result different when executed in the shader?
The same thickness should be printed depending on the distance, but why does the thin line appear when the distance is far?
Do you have any good ideas even if it's not the way I use the rotation matrix?
These are three questions.
First of all, my situation is
drawing fSQ.
I want to check whether the direction of start passes through the target.
Compute in the pixel shader.
1 is one pixel
The screen size is 1920*1080
bool intersect(float2 target, float2 direction, float2 start) {
bool intersecting = false;
static const float thresholdX = 0.5 / SCREENWIDTH;
static const float thresholdY = 0.5 / SCREENHEIGHT;
if (direction.x == 0 && direction.y == 0);
else {
float2 startToTarget = target - start;
float changedTargetPositionX = startToTarget.x * direction.x + startToTarget.y * direction.y;
float changedTargetPositionY = startToTarget.x * (-direction.y) + startToTarget.y * direction.x;
float rangeOfX = (direction.x * direction.x) + (direction.y * direction.y);
if (changedTargetPositionX <= rangeOfX + thresholdX && changedTargetPositionX >= -thresholdX &&
changedTargetPositionY <= thresholdY && changedTargetPositionY >= -thresholdY) {
intersecting = true;
}
}
return intersecting;
We use a rotation matrix to rotate a vector and then check the difference between the two vectors, which works in most cases, but fails for very small pixels.
For example
start = (15,0) direction= (10,0) taget = (10,0)
In this case, the intersect function should return false, but it returns true.
But if the pixel difference is bigger then it works fine.
and
#define MAX = 5;
float2 points[MAX*MAX];
for (float fi = 1; fi < MAX; fi++)
for (float fj = 1; fj < MAX; fj++)
points[(int)(fi * MAX + fj)] = float2(fi / MAX , fj / MAX);
for(uint ni=0; ni < MAX*MAX;ni++)
for(uint nj=3; nj < MAX*MAX; nj++)
if (intersect(uv, points[nj]- points[ni], points[ni])) {
color = float4(1, 0, 0, 1);
return color;
}
return float4(0, 0, 0, 1);
When debugging like this, the line becomes thinner depending on the distance.
All the lines should have the same thickness, but I don't know why.
This is the result of running the debugging code:
We look forward to your reply.
thank you

Low framerate with only map and minimap drawing (SFML)

I'm working on a small "game" like project as a practice, and I've managed to get my framerate down to not even 3 FPS. While the only thing that is being drawn is screen filling tiles and a minimap.
Now I've found that the problem is with the minimap, without it caps at 60 FPS. But unfortunately I'm not experienced enough to find out what the real problem is with it...
My draw function:
void StateIngame::draw()
{
m_gui.removeAllWidgets();
m_window.setView(m_view);
// Frame counter
float ct = m_clock.restart().asSeconds();
float fps = 1.f / ct;
m_time = ct;
char c[10];
sprintf(c, "%f", fps);
std::string fpsStr(c);
sf::String str(fpsStr);
auto fpsText = tgui::Label::create();
fpsText->setText(str);
fpsText->setTextSize(16);
fpsText->setPosition(15, 15);
m_gui.add(fpsText);
//////////////
// Draw map //
//////////////
int camOffsetX, camOffsetY;
int tileSize = m_map.getTileSize();
Tile tile;
sf::IntRect bounds = m_camera.getTileBounds(tileSize);
camOffsetX = m_camera.getTileOffset(tileSize).x;
camOffsetY = m_camera.getTileOffset(tileSize).y;
// Loop and draw each tile
// x and y = counters, tileX and tileY is the coordinates of the tile being drawn
for (int y = 0, tileY = bounds.top; y < bounds.height; y++, tileY++)
{
for (int x = 0, tileX = bounds.left; x < bounds.width; x++, tileX++)
{
try {
// Normal view
m_window.setView(m_view);
tile = m_map.getTile(tileX, tileY);
tile.render((x * tileSize) - camOffsetX, (y * tileSize) - camOffsetY, &m_window);
} catch (const std::out_of_range& oor)
{}
}
}
bounds = sf::IntRect(bounds.left - (bounds.width * 2), bounds.top - (bounds.height * 2), bounds.width * 4, bounds.height * 4);
for (int y = 0, tileY = bounds.top; y < bounds.height; y++, tileY++)
{
for (int x = 0, tileX = bounds.left; x < bounds.width; x++, tileX++)
{
try {
// Mini map
m_window.setView(m_minimap);
tile = m_map.getTile(tileX, tileY);
sf::RectangleShape miniTile(sf::Vector2f(4, 4));
miniTile.setFillColor(tile.m_color);
miniTile.setPosition((x * (tileSize / 4)), (y * (tileSize / 4)));
m_window.draw(miniTile);
} catch (const std::out_of_range& oor)
{}
}
}
// Gui
m_window.setView(m_view);
m_gui.draw();
}
The Tile class has a variable of type sf::Color which is set during map generating. This color is then used to draw the minimap instead of the 16x16 texture that is used for the map itself.
So when I leave out the minimap drawing, and only draw the map itself, it's more fluid than I could wish for...
Any help is appreciated!
You are generating the view completly new for every frame. Do this once on startup should be enought.

C++ SFML collision is not accurate

I'm making a 2D game with SFML in C++ and I have a problem with collision. I have a player and a map made of tiles. Thing that doesn't work is that my collision detection is not accurate. When I move player up and then down towards tiles, it ends up differently.
I am aware that source of this problem may be calculating player movement with use of delta time between frames - so it is not constant. But it smooths movement, so I don't know how to do it other way. I tried with constant speed valuses and to make collision fully accurate - speed had to be very low and I am not satisfied with that.
void Player::move() {
sf::Vector2f offsetVec;
if (sf::Keyboard::isKeyPressed(sf::Keyboard::W))
offsetVec += sf::Vector2f(0, -10);
if (sf::Keyboard::isKeyPressed(sf::Keyboard::S))
offsetVec += sf::Vector2f(0, 10);
if (sf::Keyboard::isKeyPressed(sf::Keyboard::A))
offsetVec += sf::Vector2f(-10, 0);
if (sf::Keyboard::isKeyPressed(sf::Keyboard::D))
offsetVec += sf::Vector2f(10, 0);
this->moveVec += offsetVec;
}
void Player::update(float dt, Map *map) {
sf::Vector2f offset = sf::Vector2f(this->moveVec.x * this->playerSpeed * dt,
this->moveVec.y * this->playerSpeed * dt);
sf::Sprite futurePos = this->sprite;
futurePos.move(offset);
if (map->isCollideable(this->pos.x, this->pos.y, futurePos.getGlobalBounds())) {
this->moveVec = sf::Vector2f(0, 0);
return;
}
this->sprite.move(offset);
this->pos += offset;
this->moveVec = sf::Vector2f(0, 0);
return;
}
In player position update I create future sprite object, which is object after applying movement, to get it's boundaries and pass it to collision checker. To collision checker I also pass player pos, because my map is stored in 2d array of tile pointers, so I check only these in player range.
bool Map::isCollideable(float x, float y, const sf::FloatRect &playerBounds) {
int startX = int(x) / Storage::tileSize;
int startY = int(y) / Storage::tileSize;
Tile *tile;
for (int i = startX - 10; i <= startX + 10; ++i) {
for (int j = startY - 10; j <= startY + 10; ++j) {
if (i >= 0 && j >= 0) {
tile = getTile(i, j);
if (tile != nullptr && playerBounds.intersects(tile->getGlobalBounds()))
return true;
}
}
}
return false;
}
Full project on Github
My solution
I have changed if statement in update function to while statement, which decreases my offset vector till no collision is present. I still have to make some adjustments, but general idea is:
void Player::update(float dt, Map *map) {
int repeats = 0;
sf::Vector2f offset = sf::Vector2f(this->moveVec.x * this->playerSpeed * dt,
this->moveVec.y * this->playerSpeed * dt);
sf::Sprite futurePos = this->sprite;
while (map->isCollideable(this->pos.x, this->pos.y, futurePos, offset)) {
offset = 0.7f * offset;
repeats++;
if (repeats > 5) {
this->moveVec = sf::Vector2f(0, 0);
return;
}
}
this->sprite.move(offset);
this->pos += offset;
this->moveVec = sf::Vector2f(0, 0);
return;
}
I also had to rework isCollideable method a little, so it accepts sf::Sprite and offset vector so it can calculate boundaries on it's own.
When the player collides with a tile, you should calculate the penetration, that is, the value of "how much the player went into the tile". When you have this value, nudge your player back that much.
This is just a thought but you could have some inaccuracies in your collision detection when you typecast the float x, and y to integers and then divide them. This could cause problems because some of the data in the float could be lost. If the float was 3.5 or 3.3 or 3.9 then it would become 3 which throws off your collision calculations.

SFML mouse position and collision loading from file

Okay so I have two problems. First off I have made a square that is locked to a grid the same size as my tiles. This is what will be used for changing tiles. It works fine except for when I start scrolling. I know why it is. It's because the mouse position is relative to the window, not the map. I was wondering if there was a way I could code the squares to follow my mouse even when I scroll.
Current code:
if (Event.type == sf::Event::MouseMoved)
{
rect.setFillColor(sf::Color(255, 0, 255));
rect.setSize(sf::Vector2f(BLOCKSIZE, BLOCKSIZE));
int x_offset = (Window.getView().getCenter().x - Window.getSize().x /2);
int y_offset = (Window.getView().getCenter().y - Window.getSize().y /2);
rect.setPosition(((sf::Mouse::getPosition(Window).x/32 *32) + (x_offset/32 *32)), ((sf::Mouse::getPosition(Window).y/32 * 32) + (y_offset/32 * 32)));
std::cout << "Mouse position: x:" << ((sf::Mouse::getPosition(Window).x/32 *32) + (x_offset/32 *32)) << " y:" << ((sf::Mouse::getPosition(Window).y/32 * 32) + (y_offset/32 * 32)) << ")\n\n";
}
Next problem is loading collision.
Code:
for(int i = 0; i < CollisionVector.size(); i++)
{
//Loop through the height of the MapVector
for(int j = 0; j < CollisionVector[i].size(); j++)
{
sf::RectangleShape rect;
//If the stored number is 1
if(CollisionVector[i][j] == 1)
{
rect.setFillColor(sf::Color(255, 0, 255));
rect.setSize(sf::Vector2f(BLOCKSIZE, BLOCKSIZE));
//Set the position of the rectangle
rect.setPosition(j * BLOCKSIZE, i * BLOCKSIZE);
}
//Draw the rectangle
Window.draw(rect);
}
}
I get a blank screen if I add that in. I wanted the rectangles to be transparent but I changed it to pink just in case that was the problem (Which it wasn't)
Screen scrolling code:
void Camera::Update(float x, float y)
{
cameraX = x - (ScreenWidth / 2);
cameraY = y - (ScreenHeight / 2);
if (cameraX < 0)
cameraX = 0.0;
if (cameraY < 0)
cameraY = 0.0;
CameraPosition.reset(sf::FloatRect(cameraX, cameraY, ScreenWidth, ScreenHeight));
CameraPosition.setViewport(sf::FloatRect(0,0,1,1));
}
For the first one: since you are scrolling by changing the sf::View, calculate the view offset and add it to your coordinates:
[...]
float x_offset = Window.getView().getCenter().x - Window.getSize().x * .5f;
float y_offset = Window.getView().getCenter().y - Window.getSize().y * .5f;
rect.setPosition(sf::Mouse::getPosition(Window).x/32 * 32 + x_offset,
sf::Mouse::getPosition(Window).y/32 * 32 + y_offset;
For the second one... uhhh I've got nothing right now. I've checked SFML and a sf::RectangleShape is default-initialized to have a size of (0, 0) so that's not the issue. Maybe the problem is in surrounding code?

SDL - drawing 'negative' circles (Fog of War)

I have this 800x600square I want to draw to the screen. I want to 'cut' circles in it (where alpha would be 0). Basically I'm drawing this whole rectangle over a map so in these 'circles' I drew, you can see the map, otherwise you see the grey square
So, I assume you're trying to add fog of war to one of you game?
I had a small demo I made for a local University a few weeks ago to show A* pathfinding, so I thought I could add fog of war to it for you. Here's the results:
Initial map
First, you start with a complete map, totally visible
Fog
Then, I added a surface to cover the entire screen (take note that my map is smaller than the screen, so for this case I just added fog of war on the screen, but if you have scrolling, make sure it covers each map pixel 1:1)
mFogOfWar = SDL_CreateRGBSurface(SDL_HWSURFACE, in_Width, in_Height, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000);
SDL_Rect screenRect = {0, 0, in_Width, in_Height};
SDL_FillRect(mFogOfWar, &screenRect, 0xFF202020);
Then, you need to draw it... I added this call after drawing the game objects and before drawing the UI
DrawSurface(mFogOfWar, 0, 0);
Where
void RenderingManager::DrawSurface(SDL_Surface* in_Surface, int in_X, int in_Y)
{
SDL_Rect Dest = { in_X, in_Y, 0, 0 };
SDL_BlitSurface(in_Surface, NULL, mScreen, &Dest);
}
Which should give you the following result:
"Punch Surface"
I then created a 32 bits .png that looks like this (checkerboard shows alpha)
When rendering my main character, I added this call:
gRenderingManager.RemoveFogOfWar(int(mX) + SPRITE_X_OFFSET, int(mY) + SPRITE_Y_OFFSET);
The offset is only there to center the punch with the sprite, basically, what I'm passing to RemoveFogOfWar is the center of my sprite.
Remove Fog Of War
Now the meat of the fog of war. I did two versions, one where Fog of War is removed permanently and one where the fog of war is reset. My fog of war reset relies on my punch surface to have a contour where the alpha is reset to 0 and the fact that my character moves of less pixels than the contour contains per frame, otherwise I would keep the Rect where my punch was applied and I would refill it before drawing again the new punch.
Since I couldn't find a "multiply" blend with SDL, I decided to write a simple function that iterates on the punch surface and updates the alpha on the fog of war surface. The most important part is to make sure you stay within the bounds of your surfaces, so it takes up most of the code... there might be some crop functions but I didn't bother checking:
void RenderingManager::RemoveFogOfWar(int in_X, int in_Y)
{
const int halfWidth = mFogOfWarPunch->w / 2;
const int halfHeight = mFogOfWarPunch->h / 2;
SDL_Rect sourceRect = { 0, 0, mFogOfWarPunch->w, mFogOfWarPunch->h };
SDL_Rect destRect = { in_X - halfWidth, in_Y - halfHeight, mFogOfWarPunch->w, mFogOfWarPunch->h };
// Make sure our rects stays within bounds
if(destRect.x < 0)
{
sourceRect.x -= destRect.x; // remove the pixels outside of the surface
sourceRect.w -= sourceRect.x; // shrink to the surface, not to offset fog
destRect.x = 0;
destRect.w -= sourceRect.x; // shrink the width to stay within bounds
}
if(destRect.y < 0)
{
sourceRect.y -= destRect.y; // remove the pixels outside
sourceRect.h -= sourceRect.y; // shrink to the surface, not to offset fog
destRect.y = 0;
destRect.h -= sourceRect.y; // shrink the height to stay within bounds
}
int xDistanceFromEdge = (destRect.x + destRect.w) - mFogOfWar->w;
if(xDistanceFromEdge > 0) // we're busting
{
sourceRect.w -= xDistanceFromEdge;
destRect.w -= xDistanceFromEdge;
}
int yDistanceFromEdge = (destRect.y + destRect.h) - mFogOfWar->h;
if(yDistanceFromEdge > 0) // we're busting
{
sourceRect.h -= yDistanceFromEdge;
destRect.h -= yDistanceFromEdge;
}
SDL_LockSurface(mFogOfWar);
Uint32* destPixels = (Uint32*)mFogOfWar->pixels;
Uint32* srcPixels = (Uint32*)mFogOfWarPunch->pixels;
static bool keepFogRemoved = false;
for(int x = 0; x < destRect.w; ++x)
{
for(int y = 0; y < destRect.h; ++y)
{
Uint32* destPixel = destPixels + (y + destRect.y) * mFogOfWar->w + destRect.x + x;
Uint32* srcPixel = srcPixels + (y + sourceRect.y) * mFogOfWarPunch->w + sourceRect.x + x;
unsigned char* destAlpha = (unsigned char*)destPixel + 3; // fetch alpha channel
unsigned char* srcAlpha = (unsigned char*)srcPixel + 3; // fetch alpha channel
if(keepFogRemoved == true && *srcAlpha > 0)
{
continue; // skip this pixel
}
*destAlpha = *srcAlpha;
}
}
SDL_UnlockSurface(mFogOfWar);
}
Which then gave me this with keepFogRemoved = false even after the character had moved around
And this with keepFogRemoved = true
Validation
The important part is really to make sure you don't write outside of your pixel buffer, so watch out with negative offsets or offsets that would bring you out of the width or height. To validate my code, I added a simple call to RemoveFogOfWar when the mouse is clicked and tried corners and edges to make sure I didn't have a "off by one" problem
case SDL_MOUSEBUTTONDOWN:
{
if(Event.button.button == SDL_BUTTON_LEFT)
{
gRenderingManager.RemoveFogOfWar(Event.button.x, Event.button.y);
}
break;
}
Notes
Obviously, you don't need a 32 bits texture for the "punch", but it was the clearest way I could think of to show you how to do it. It could be done using as little as 1 bit per pixel (on / off). You can also add some gradient, and change the
if(keepFogRemoved == true && *srcAlpha > 0)
{
continue; // skip this pixel
}
To something like
if(*srcAlpha > *destAlpha)
{
continue;
}
To keep a smooth blend like this:
3 State Fog of War
I thought I should add this... I added a way to create a 3 state fog of war: visible, seen and fogged.
To do this, I simply keep the SDL_Rect of where I last "punched" the fog of war, and if the alpha is lower than a certain value, I clamp it at that value.
So, by simply adding
for(int x = 0; x < mLastFogOfWarPunchPosition.w; ++x)
{
for(int y = 0; y < mLastFogOfWarPunchPosition.h; ++y)
{
Uint32* destPixel = destPixels + (y + mLastFogOfWarPunchPosition.y) * mFogOfWar->w + mLastFogOfWarPunchPosition.x + x;
unsigned char* destAlpha = (unsigned char*)destPixel + 3;
if(*destAlpha < 0x60)
{
*destAlpha = 0x60;
}
}
}
mLastFogOfWarPunchPosition = destRect;
right before the loop where the fog of war is "punched", I get a fog of war similar to what you could have in games like StarCraft:
Now, since the "seen" fog of war is semi transparent, you will need to tweak your rendering method to properly clip "enemies" that would be in the fog, so you don't see them but you still see the terrain.
Hope this helps!