#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); } } } }