84 lines
4.2 KiB
C++
84 lines
4.2 KiB
C++
|
|
#include "render/atmos.h"
|
|||
|
|
|
|||
|
|
#include "render/iso.h"
|
|||
|
|
#include "render/noise.h"
|
|||
|
|
|
|||
|
|
namespace
|
|||
|
|
{
|
|||
|
|
|
|||
|
|
// Мировой квадрат вокруг камеры, гарантированно покрывающий кадр 480x270.
|
|||
|
|
// В изометрии |wx-wy| <= 15 по ширине и |wx+wy| <= 17 по высоте, так что 19
|
|||
|
|
// тайлов в каждую сторону — с запасом на подъём пылинки по экрану.
|
|||
|
|
constexpr int ASH_REACH = 19;
|
|||
|
|
|
|||
|
|
// Больше трёх пылинок на тайл — это уже не воздух, а снегопад.
|
|||
|
|
constexpr int ASH_PER_CELL = 3;
|
|||
|
|
|
|||
|
|
constexpr uint32_t COL_ASH = FXC(198, 186, 166); // тёплый серый: пепел, не снег
|
|||
|
|
|
|||
|
|
} // namespace
|
|||
|
|
|
|||
|
|
void Atmosphere::DrawAsh(Framebuffer& fb, Vec2 camPos, int ox, int oy, const Lighting& light) const
|
|||
|
|
{
|
|||
|
|
const float density = g_tune.ashDensity;
|
|||
|
|
const float bright = g_tune.ashBrightness;
|
|||
|
|
if (density <= 0.0f || bright <= 0.0f) return;
|
|||
|
|
|
|||
|
|
const int cx = int(std::floor(camPos.x));
|
|||
|
|
const int cy = int(std::floor(camPos.y));
|
|||
|
|
const float drift = g_tune.ashSpeed;
|
|||
|
|
|
|||
|
|
for (int gy = cy - ASH_REACH; gy <= cy + ASH_REACH; ++gy)
|
|||
|
|
{
|
|||
|
|
for (int gx = cx - ASH_REACH; gx <= cx + ASH_REACH; ++gx)
|
|||
|
|
{
|
|||
|
|
for (int k = 0; k < ASH_PER_CELL; ++k)
|
|||
|
|
{
|
|||
|
|
// Дробная плотность: последней пылинке в ячейке «положено»
|
|||
|
|
// столько, сколько осталось от density.
|
|||
|
|
const float quota = density - float(k);
|
|||
|
|
if (quota <= 0.0f) break;
|
|||
|
|
|
|||
|
|
const uint32_t h = Hash3(uint32_t(gx + 4096), uint32_t(gy + 4096),
|
|||
|
|
uint32_t(k) * 977u + 13u);
|
|||
|
|
if (quota < 1.0f && Hash01(h) > quota) continue;
|
|||
|
|
|
|||
|
|
const Vec2 base{float(gx) + Hash01(Hash2(h, 1u)),
|
|||
|
|
float(gy) + Hash01(Hash2(h, 2u))};
|
|||
|
|
|
|||
|
|
// Дрейф — замкнутая петля, а не бесконечный снос: иначе пылинку
|
|||
|
|
// пришлось бы «заворачивать», и она телепортировалась бы на глазах.
|
|||
|
|
const float ph = Hash01(Hash2(h, 3u)) * TWO_PI_F;
|
|||
|
|
const float rate = 0.20f + 0.45f * Hash01(Hash2(h, 4u));
|
|||
|
|
const float amp = 0.30f + 0.30f * Hash01(Hash2(h, 5u));
|
|||
|
|
const float t = time * drift * rate;
|
|||
|
|
const Vec2 p{base.x + std::cos(t * TWO_PI_F + ph) * amp,
|
|||
|
|
base.y + std::sin(t * TWO_PI_F * 0.73f + ph * 1.7f) * amp};
|
|||
|
|
|
|||
|
|
const float lit = light.LightAt(p);
|
|||
|
|
if (lit < 0.05f) continue;
|
|||
|
|
|
|||
|
|
// Высота: часть взвеси стелется по полу, часть висит на уровне
|
|||
|
|
// головы. Медленное покачивание вверх-вниз добавляет объёма.
|
|||
|
|
const float liftFrac = 0.15f + 0.95f * Hash01(Hash2(h, 6u));
|
|||
|
|
const float bob = std::sin(t * TWO_PI_F * 1.31f + ph) * 2.5f;
|
|||
|
|
const int lift = int(g_tune.ashLiftPx * liftFrac + bob);
|
|||
|
|
|
|||
|
|
const Vec2 s = iso::WorldToScreen(p);
|
|||
|
|
const int sx = int(std::lround(s.x)) + ox;
|
|||
|
|
const int sy = int(std::lround(s.y)) + oy - lift;
|
|||
|
|
if (unsigned(sx) >= unsigned(fb.w) || unsigned(sy) >= unsigned(fb.h)) continue;
|
|||
|
|
|
|||
|
|
// Мерцание: пылинка поворачивается к свету то одним боком, то другим.
|
|||
|
|
const float twinkle = 0.45f + 0.55f * (0.5f + 0.5f * std::sin(t * 9.0f + ph * 3.0f));
|
|||
|
|
const float amount = lit * bright * twinkle * (0.35f + 0.65f * Hash01(Hash2(h, 8u)));
|
|||
|
|
const uint32_t col = ScaleColor8(COL_ASH, uint32_t(Clampf(amount, 0.0f, 1.0f) * 256.0f));
|
|||
|
|
|
|||
|
|
// Крупные хлопья редки и получают ореол — глазу есть за что зацепиться.
|
|||
|
|
if ((h & 15u) == 0u) fb.AddGlowDisc(sx, sy, 1, col);
|
|||
|
|
else fb.AddPx(sx, sy, col);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|