Done with it. I call it ‘Escape the dots’ : )
static bool visDiffusionPF = 1; ImGui::Checkbox("visDiffusionPF", &visDiffusionPF);
if (visDiffusionPF)
{
constexpr int grid_exp = 6;
constexpr int grid_res = 1<<grid_exp;
constexpr int grid_mask = grid_res-1;
constexpr float delta = 1.f / float(grid_res);
static std::vector<float> map(grid_res*grid_res, 0);
static std::vector<float> temp(grid_res*grid_res, 0);
static std::vector<vec2> gradientField(grid_res*grid_res, vec2(0));
struct Agent
{
vec2 position = vec2(0);
vec2 velocity = vec2(0); // EDIT: this is not used - forgot to delete it
};
static std::vector<Agent> agents (5);
// init
static int init = true;
if (init)
{
// make level
for (int y=0; y<grid_res; y++)
for (int x=0; x<grid_res; x++)
{
int i = x + y*grid_res;
map[i] = 0;
if (x+2==grid_mask-y) map[i] = -1; // diagonal wall
if (x==10) map[i] = -1; // vert. wall
if (y==20) map[i] = -1; // horiz. wall
if (x==0 || y==0 || x==grid_mask || y==grid_mask)
map[i] = -1; // boundary
else
if (((x^y)&24) == 0) map[i] = 0; // open walls
}
int prn = 0;
for (auto &a : agents)
{
for (int n=0; n<2; n++)
{
prn = prn * 1664525 + 371;
a.position[n] = float (prn) * 0.0001f;
a.position[n] -= floor(a.position[n]);
a.position[n] = 0.05f + a.position[n] * 0.9f;
}
a.velocity = vec2(0);
}
init = false;
}
// game loop
// bilinear filter
auto SetupSampleCoords2 = [&] (int *gridI, float *gridF, const float *coords)
{
float offset[2] = {
coords[0] - .5f,
coords[1] - .5f};
float fl[3] = {
floor(offset[0]),
floor(offset[1]) };
for (int i=0; i<2; i++)
{
gridI[i] = int(fl[i]) & grid_mask;
gridF[i] = offset[i] - fl[i];
}
};
auto Inject2 = [&] (auto &map,
const int *gridI, const float *gridF, const float sample)
{
const int dimX = grid_res;
const int mX = grid_mask;
const int mY = grid_mask;
float gridG[2] = {
1-gridF[0],
1-gridF[1]};
int g00 = (gridI[0]&mX) + (gridI[1]&mY) * dimX;
int g01 = (gridI[0]&mX) + ((gridI[1]+1)&mY) * dimX;
int g10 = ((gridI[0]+1)&mX) + (gridI[1]&mY) * dimX;
int g11 = ((gridI[0]+1)&mX) + ((gridI[1]+1)&mY) * dimX;
bool isObstacle00 = map[g00] < 0;
bool isObstacle01 = map[g01] < 0;
bool isObstacle10 = map[g10] < 0;
bool isObstacle11 = map[g11] < 0;
if (isObstacle00)
return;
if (!isObstacle00) map[g00] += sample * gridG[0] * gridG[1];
if (!isObstacle01) map[g01] += sample * gridG[0] * gridF[1];
if (!isObstacle10) map[g10] += sample * gridF[0] * gridG[1];
if (!isObstacle11 && (!isObstacle01 || !isObstacle10)) map[g11] += sample * gridF[0] * gridF[1];
};
auto Sample2 = [&] (const auto gradientMap, const auto &map,
const int *gridI, const float *gridF)
{
const int dimX = grid_res;
const int mX = grid_mask;
const int mY = grid_mask;
float gridG[2] = {
1-gridF[0],
1-gridF[1]};
int g00 = (gridI[0]&mX) + (gridI[1]&mY) * dimX;
int g01 = (gridI[0]&mX) + ((gridI[1]+1)&mY) * dimX;
int g10 = ((gridI[0]+1)&mX) + (gridI[1]&mY) * dimX;
int g11 = ((gridI[0]+1)&mX) + ((gridI[1]+1)&mY) * dimX;
bool isObstacle00 = map[g00] < 0;
bool isObstacle01 = map[g01] < 0;
bool isObstacle10 = map[g10] < 0;
bool isObstacle11 = map[g11] < 0;
if (isObstacle00)
return vec2(0);
vec2 sample (0);
if (!isObstacle00) sample += gradientMap[g00] * gridG[0] * gridG[1];
if (!isObstacle01) sample += gradientMap[g01] * gridG[0] * gridF[1];
if (!isObstacle10) sample += gradientMap[g10] * gridF[0] * gridG[1];
if (!isObstacle11 && (!isObstacle01 || !isObstacle10)) sample += gradientMap[g11] * gridF[0] * gridF[1];
return sample;
};
#if 0
// cubic filter (unused)
auto SetupSampleCoords3 = [&] (int *gridI, float *gridF, const float *coords)
{
float fl[2] = {
floor(coords[0]),
floor(coords[1]) };
for (int i=0; i<2; i++)
{
gridI[i] = (int(fl[i]) - 1) & grid_mask;
gridF[i] = coords[i] - fl[i];
}
};
auto Inject3 = [&] (auto &map,
const int *gridI, const float *gridF, const float sample)
{
auto square = [] (const vec &v)
{
return mulPerElem(v,v); // v[0]*=v[0]; v[1]*=v[1]; v[2]*=v[2];
};
vec p(gridF[0], gridF[1], gridF[2]);
vec fw[3];
{
fw[0] = mulPerElem (vec(.5f), square(vec(1.f) - p));
fw[1] = vec(.75f) - square(p - vec(.5f));
fw[2] = mulPerElem (vec(.5f), square(p));
}
for (int j=0; j<3; j++)
for (int i=0; i<3; i++)
{
float w = fw[i][0] * fw[j][1];
int X = (gridI[0] + i) & grid_mask;
int Y = (gridI[1] + j) & grid_mask;
int g = X + Y * grid_res;
if (map[g] >= 0) // don't change obstacles
map[g] += sample * w;
}
};
#endif
{
// player movement
constexpr float playerSpeed = 0.001f;
Agent &player = agents[0];
vec2 vel = vec2(0);
if (application.KeyDown('K')) vel += vec2(0,-1.f);
if (application.KeyDown('I')) vel += vec2(0,1.f);
if (application.KeyDown('J')) vel += vec2(-1.f,0);
if (application.KeyDown('L')) vel += vec2(1.f,0);
vel *= playerSpeed;
player.position += vel;
player.position[0] -= floor(player.position[0]);
player.position[1] -= floor(player.position[1]);
// inject fluid energy from player
constexpr float injectAmount = 10.f;
float tc[2] = {player.position[0] * float(grid_res), player.position[1] * float(grid_res)};
int gridI[2]; float gridF[2];
#if 0 // cubic
SetupSampleCoords3 (gridI, gridF, tc);
Inject3 (map, gridI, gridF, injectAmount);
#else // bilinear
SetupSampleCoords2 (gridI, gridF, tc);
Inject2 (map, gridI, gridF, injectAmount);
#endif
if (map[gridI[0] + gridI[1] * grid_res] < 0)
init = true; // game over if running into wall
// blur fluid and calc gradient
constexpr float evaporate = 0.01f;
#if 0 // HQ blur but leaks through diagonal walls :(
for (int y=0; y<grid_res; y++)
for (int x=0; x<grid_res; x++)
{
int i = x + y*grid_res;
float value = map[i];
if (value < 0)
{
temp[i] = value;
continue;
}
float weightedSum = 0;
vec2 gradient(0);
for (int dy=-1; dy<=1; dy++)
for (int dx=-1; dx<=1; dx++)
{
// blur
int ay = (y+dy) & grid_mask;
int ax = (x+dx) & grid_mask;
int ai = ax + ay * grid_res;
float aValue = map[ai];
if (aValue < 0) aValue = 0; // set obstacles to zero fluid
float weight = (dx==0 ? 2.0f : 1.0f) *
(dy==0 ? 2.0f : 1.0f) / 16.f;
weightedSum += weight * aValue;
// gradient (from the old state)
vec2 gDir (dx,dy);
float repellingAValue = (aValue > 0 ? aValue : injectAmount * -0.5f); // hack to prevent velocity pointing towards walls; would be better to make sure injection avoids leaking
float vDiff = repellingAValue - value;
gDir *= vDiff;
gradient += gDir * weight;
}
temp[i] = weightedSum * (1.f - evaporate);
gradientField[i] = gradient;
#else
for (int y=0; y<grid_res; y++)
for (int x=0; x<grid_res; x++)
{
int i = x + y*grid_res;
float value = map[i];
if (value < 0)
{
temp[i] = value;
continue;
}
float aValueL = max(0.f, map[((x-1)&grid_mask) + y * grid_res]);
float aValueR = max(0.f, map[((x+1)&grid_mask) + y * grid_res]);
float aValueT = max(0.f, map[x + ((y-1)&grid_mask) * grid_res]);
float aValueB = max(0.f, map[x + ((y+1)&grid_mask) * grid_res]);
float weightedSum = (aValueL + aValueR + aValueT + aValueB) * .25f;
temp[i] = weightedSum * (1.f - evaporate);
vec2 gradient = vec2(
(aValueR - aValueL),
(aValueB - aValueT));
gradientField[i] = gradient;
#endif
}
std::swap(temp, map);
// enemy movement
constexpr float enemySpeed = playerSpeed * .75f;
vec2 playerPosition = agents[0].position;
for (int i=1; i<(int)agents.size(); i++)
{
Agent &enemy = agents[i];
float tc[2] = {enemy.position[0] * float(grid_res), enemy.position[1] * float(grid_res)};
int gridI[2]; float gridF[2];
SetupSampleCoords2 (gridI, gridF, tc);
vec2 vel = Sample2 (gradientField, map, gridI, gridF);
float mag = vel.Length();
if (mag > 1.0e-12f)
vel *= enemySpeed / mag;
else
vel = vec2(0);
enemy.position += vel;
float distToPlayer = vec2(enemy.position - playerPosition).Length();
if (distToPlayer < delta)
init = 1; // game over
}
}
// visualize
auto VisCell = [&](int x, int y, float value)
{
sVec3 w[4];
for (int n=0; n<4; n++)
{
int u = x + (n&1);
int v = y + ((n>>1)&1);
w[n] = sVec3( (float(u)) / float(grid_res),
(float(v)) / float(grid_res), -0.01f);
}
sVec3 color (1);
if (value>=0)
{
color[0] = max(0.f,min(1.f, value));
color[1] = max(0.f,min(1.f, value * 10.f));
color[2] = max(0.f,min(.5f, value * 100.f));
}
RenderTriangle(w[1], w[2], w[0], color[0], color[1], color[2]);
RenderTriangle(w[2], w[1], w[3], color[0], color[1], color[2]);
};
static bool visLevel = 1; ImGui::Checkbox("visLevel", &visLevel);
if (visLevel)
{
for (int y=0; y<grid_res; y++)
for (int x=0; x<grid_res; x++)
{
int i = x + y*grid_res;
float value = map[i];
VisCell(x, y, value);
}
}
static bool visGradient = 1; ImGui::Checkbox("visGradient", &visGradient);
if (visGradient)
{
for (int y=0; y<grid_res; y++)
for (int x=0; x<grid_res; x++)
{
int i = x + y*grid_res;
vec2 gradient = gradientField[i];
float mag = gradient.Length();
if (mag > 1.0e-12f)
gradient *= delta / mag;
else
gradient = vec2(0);
RenderArrow(vec2((float(x)+.5f)/float(grid_res), (float(y)+.5f)/float(grid_res)), gradient, delta/4, 1,0,0);
}
}
static bool visAgents = 1; ImGui::Checkbox("visAgents", &visAgents);
if (visAgents)
{
for (int i=0; i<(int)agents.size(); i++)
{
Agent &agent = agents[i];
sVec3 color;
color[0] = float ((i+0)*3 * 1664525) *.01f; color[0] -= floor(color[0]);
color[1] = float ((i+1)*3 * 1664525) *.01f; color[1] -= floor(color[1]);
color[2] = float ((i+2)*3 * 1664525) *.01f; color[2] -= floor(color[2]);
RenderPoint (agent.position, color[0], color[1], color[2]);
}
}
}
Should be easy to port. In case you're unfamiliar with C++, this code is called once per frame. Static variables keep in memory between the calls and are initialized only once. This way i have level setup and game loop all in one place to keep it simple.
All my higher quality ideas lead to leaking problems. They would work of all obstacle walls would be thicker, so i kept the code in but disabled with #if 0. I don't think it would be worth it. Current cheaper methods are faster and have nice quality too.
One problem is, if there are singularities in the field, enemies get stuck there for a moment. But then they move on and find the player. Otherwise it seems to work robustly.
Now i think it's still a bit different from the video. I assume they do not blur, but instead do region growing, taking min / max value from neighborhood and increasing / decreasing it. So it's likely an integer approach, even if they use floating point numbers. My method gives smoother / more continuous results i guess, as it's based on scalar fluid energy and gradual flow. Cost shoudl be pretty equal for both, but behavior differs slightly.
I don't use any max or min energy (like clamping between -1 and 1) - would not know what's that good for. Instead i use negative energy to mark obstacles but there is no maximum. To prevent growing energy to infinity, i scale the whole energy map down each step, see ‘evaporate’ constant.
I think it would work to update only a subset of cells each step, e.g.using a checkerboard or dithering pattern to make it faster while still good enough. Multi level grids would also work to find paths faster, but could not handle narrow corridors which would collapse to be solid at lower resolution.