15constexpr float TAU = std::numbers::pi_v<float> * 2.0F;
29 x = ((x >> 16U) ^ x) * 0x45d9f3bU;
30 x = ((x >> 16U) ^ x) * 0x45d9f3bU;
32 return static_cast<float>(x & 0xffffU) / 65535.0F;
38 const double cell = std::floor(x);
40 const auto i =
static_cast<uint32_t
>(
static_cast<int64_t
>(
cell));
45float wrap(
float time,
float length)
47 const float t = std::fmod(time, length);
48 return t < 0.0F ? t + length : t;
64 if (r ==
nullptr || points.size() < 2) {
70 return SDL_FPoint { std::clamp(0.5F + ((p.x - center.
x) / (radius * 2.0F)), 0.0F, 1.0F),
71 std::clamp(0.5F + ((p.y - center.
y) / (radius * 2.0F)), 0.0F, 1.0F) };
74 static std::vector<SDL_Vertex> vertices;
75 static std::vector<int> indices;
79 vertices.push_back({ { center.
x, center.
y }, fcolor, { 0.5F, 0.5F } });
80 for (
const auto& p : points) {
81 vertices.push_back({ { p.x, p.y }, fcolor, uv(p) });
84 const int count =
static_cast<int>(points.size());
85 for (
int i = 1; i < count; ++i) {
86 indices.insert(indices.end(), { 0, i, i + 1 });
89 indices.insert(indices.end(), { 0, count, 1 });
92 SDL_RenderGeometry(r, texture.get(), vertices.data(),
static_cast<int>(vertices.size()),
93 indices.data(),
static_cast<int>(indices.size()));
99 const int segments = std::max(4,
static_cast<int>(std::ceil(cone /
TAU * 64.0F)));
101 for (
int i = 0; i <= segments; ++i) {
102 const float angle = direction - (cone / 2.0F)
103 + (cone *
static_cast<float>(i) /
static_cast<float>(segments));
105 center.
x + (std::cos(angle) * radius), center.
y + (std::sin(angle) * radius));
111std::size_t
cell(
int x,
int y,
int width)
113 return (
static_cast<std::size_t
>(y) *
static_cast<std::size_t
>(width))
114 +
static_cast<std::size_t
>(x);
119 return cone > 0.0F && cone <
TAU;
126 : length(std::max(length, 0.0001F))
132 keys.emplace_back(wrap(time, length), color);
133 std::ranges::sort(keys, { }, &std::pair<float, asw::Color>::first);
142 const float t = wrap(time, length);
145 auto next = std::ranges::upper_bound(keys, t, { }, &std::pair<float, asw::Color>::first);
146 const auto& to = next == keys.end() ? keys.front() : *next;
147 const auto& from = next == keys.begin() ? keys.back() : *std::prev(next);
149 float span = to.first - from.first;
150 float into = t - from.first;
158 const float f = std::clamp(into / span, 0.0F, 1.0F);
170 : width(std::max(width, 1))
171 , height(std::max(height, 1))
172 , tile_size(tile_size)
173 , solid(static_cast<std::size_t>(this->width) * static_cast<std::size_t>(this->height), 0)
175 static_cast<std::size_t>(this->width) * static_cast<std::size_t>(this->height) * 3, 0.0F)
181 return x >= 0 && y >= 0 && x < width && y < height;
187 solid[cell(x, y, width)] = is_solid ? 1 : 0;
193 return !in_grid(x, y) || solid[cell(x, y, width)] != 0;
198 falloff = std::clamp(amount, 0.001F, 1.0F);
209 sources.push_back({ x, y, color });
215 std::ranges::fill(light, 0.0F);
217 const auto cells =
static_cast<std::size_t
>(width) *
static_cast<std::size_t
>(height);
218 if (level.size() != cells) {
219 level.assign(cells, 0.0F);
225 for (
const auto& source : sources) {
226 const auto start = cell(source.x, source.y, width);
229 open.push_back(start);
231 for (std::size_t head = 0; head < open.size(); ++head) {
232 const auto index = open[head];
233 const int x =
static_cast<int>(index %
static_cast<std::size_t
>(width));
234 const int y =
static_cast<int>(index /
static_cast<std::size_t
>(width));
236 const float here = level[index];
237 light[(index * 3) + 0]
238 = std::max(light[(index * 3) + 0], here *
static_cast<float>(source.color.r));
239 light[(index * 3) + 1]
240 = std::max(light[(index * 3) + 1], here *
static_cast<float>(source.color.g));
241 light[(index * 3) + 2]
242 = std::max(light[(index * 3) + 2], here *
static_cast<float>(source.color.b));
245 if (is_solid(x, y) && index != start) {
249 const float next = here - falloff;
254 constexpr std::array<std::pair<int, int>, 4> STEPS { {
260 for (
const auto& [dx, dy] : STEPS) {
261 const int nx = x + dx;
262 const int ny = y + dy;
263 if (!in_grid(nx, ny)) {
266 const auto n = cell(nx, ny, width);
267 if (level[n] < next) {
275 for (
const auto index : open) {
286 const auto cells =
static_cast<std::size_t
>(width) *
static_cast<std::size_t
>(height);
293 if (texture ==
nullptr) {
294 SDL_Texture* txr = SDL_CreateTexture(
295 r, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, width, height);
296 if (txr ==
nullptr) {
299 SDL_SetTextureScaleMode(txr, SDL_SCALEMODE_LINEAR);
300 SDL_SetTextureBlendMode(txr, SDL_BLENDMODE_ADD);
301 texture = { txr, [](SDL_Texture* t) {
303 SDL_DestroyTexture(t);
308 pixels.resize(cells * 4);
309 for (std::size_t i = 0; i < cells; ++i) {
313 pixels[(i * 4) + 3] = 255;
315 SDL_UpdateTexture(texture.get(),
nullptr, pixels.data(), width * 4);
320 if (!in_grid(x, y)) {
323 const auto index = cell(x, y, width) * 3;
335 return {
static_cast<float>(width) * tile_size,
static_cast<float>(height) * tile_size };
367 time +=
static_cast<double>(dt);
379 lights.push_back(light);
385 glows.push_back({ texture, dest, tint });
391 tiles.push_back({ &tile_light, position });
396 occluders.push_back(polygon);
401 const auto& p = rect.position;
402 const auto& s = rect.size;
403 occluders.push_back({ p, { p.x + s.x, p.y }, p + s, { p.x, p.y + s.y } });
418 return camera !=
nullptr ? camera->world_to_screen(point) : point;
423 return camera !=
nullptr ? camera->world_to_screen(rect) : rect;
428 for (
const auto& [kind, texture] : gradients) {
429 if (kind == falloff) {
437 gradients.emplace_back(falloff, texture);
438 return gradients.back().second;
445 screen_occluders.resize(occluders.size());
446 screen_bounds.resize(occluders.size());
447 screen_winding.resize(occluders.size());
449 for (std::size_t i = 0; i < occluders.size(); ++i) {
450 auto& screen = screen_occluders[i];
452 for (
const auto& p : occluders[i]) {
453 screen.push_back(to_screen(p));
462 for (std::size_t i = 0; i < screen_occluders.size(); ++i) {
463 if (screen_occluders[i].size() >= 2 && screen_bounds[i].collides(reach)) {
476 shadow_vertices.clear();
477 shadow_indices.clear();
483 return length > 0.0F ? p + (dir * (SHADOW_FAR / length)) : p;
486 for (std::size_t i = 0; i < screen_occluders.size(); ++i) {
487 const auto& polygon = screen_occluders[i];
488 if (polygon.size() < 2 || !screen_bounds[i].collides(reach)) {
492 for (std::size_t j = 0; j < polygon.size(); ++j) {
493 const auto& a = polygon[j];
494 const auto& b = polygon[(j + 1) % polygon.size()];
501 if (outward.
dot(mid - center) <= 0.0F) {
506 const int base =
static_cast<int>(shadow_vertices.size());
507 for (
const auto& p : { a, b, away(b), away(mid), away(a) }) {
508 shadow_vertices.push_back({ { p.x, p.y }, black, { 0.0F, 0.0F } });
510 shadow_indices.insert(shadow_indices.end(),
511 { base, base + 1, base + 2, base, base + 2, base + 3, base, base + 3, base + 4 });
515 if (shadow_indices.empty()) {
521 SDL_BlendMode previous = SDL_BLENDMODE_NONE;
522 SDL_GetRenderDrawBlendMode(r, &previous);
523 SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
524 SDL_RenderGeometry(r,
nullptr, shadow_vertices.data(),
static_cast<int>(shadow_vertices.size()),
525 shadow_indices.data(),
static_cast<int>(shadow_indices.size()));
526 SDL_SetRenderDrawBlendMode(r, previous);
533 const float flicker = 1.0F
534 - (std::clamp(light.
flicker, 0.0F, 1.0F)
535 * noise((time * 12.0) + (
static_cast<double>(light.
seed) * 31.7)));
537 =
static_cast<float>(std::fmod(time *
static_cast<double>(light.
pulse_speed), 1.0));
538 const float pulse = 1.0F
539 + (std::clamp(light.
pulse, 0.0F, 1.0F)
540 * std::sin((phase * TAU) +
static_cast<float>(light.
seed)));
541 const float strength = std::max(flicker * pulse, 0.0F);
543 const float radius = light.
radius * (1.0F + ((strength - 1.0F) * 0.35F));
544 if (radius <= 0.0F) {
548 const auto center = to_screen(light.
position);
554 0.0F, 0.0F,
static_cast<float>(map_size.x),
static_cast<float>(map_size.y));
559 const auto color = scale_color(light.
color, std::clamp(light.
intensity, 0.0F, 1.0F) * strength);
560 const auto& glow = gradient(light.
falloff);
561 const bool spot = is_spot(light.
cone);
564 auto draw_shape = [&]() {
566 textured_fan(glow, center, radius, arc(center, radius, light.
direction, light.
cone),
582 seen, center, radius, screen_occluders, screen_bounds, light.
direction, light.
cone);
583 textured_fan(glow, center, radius, seen, color, !spot);
590 const float left = std::max(std::floor(square.
position.x), 0.0F);
591 const float top = std::max(std::floor(square.
position.y), 0.0F);
592 const float right = std::min(std::ceil(square.
position.x + square.
size.x), screen.
size.x);
593 const float bottom = std::min(std::ceil(square.
position.y + square.
size.y), screen.
size.y);
595 const SDL_Rect clip {
static_cast<int>(left),
static_cast<int>(top),
596 static_cast<int>(right - left),
static_cast<int>(bottom - top) };
599 SDL_SetRenderClipRect(r, &clip);
603 SDL_BlendMode previous = SDL_BLENDMODE_NONE;
604 SDL_GetRenderDrawBlendMode(r, &previous);
605 SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_NONE);
607 SDL_SetRenderDrawBlendMode(r, previous);
610 draw_shadows(center, square);
611 SDL_SetRenderClipRect(r,
nullptr);
621 if (r ==
nullptr || size.x <= 0 || size.y <= 0) {
625 if (map ==
nullptr || size != map_size) {
635 SDL_Texture* previous_target = SDL_GetRenderTarget(r);
644 for (
const auto& grid : tiles) {
645 if (grid.grid->get_texture() !=
nullptr) {
651 for (
const auto& light : lights) {
656 for (
const auto& glow : glows) {
657 SDL_Texture* txr = glow.texture.get();
658 if (txr ==
nullptr) {
662 SDL_BlendMode mode = SDL_BLENDMODE_NONE;
667 SDL_GetTextureBlendMode(txr, &mode);
668 SDL_GetTextureColorMod(txr, &red, &green, &blue);
669 SDL_GetTextureAlphaMod(txr, &alpha);
671 SDL_SetTextureBlendMode(txr, SDL_BLENDMODE_ADD);
672 SDL_SetTextureColorMod(txr, glow.tint.r, glow.tint.g, glow.tint.b);
673 SDL_SetTextureAlphaMod(txr, glow.tint.a);
676 SDL_SetTextureBlendMode(txr, mode);
677 SDL_SetTextureColorMod(txr, red, green, blue);
678 SDL_SetTextureAlphaMod(txr, alpha);
681 SDL_SetRenderTarget(r, previous_target);
687 const auto& texture = render();
688 if (texture ==
nullptr) {
693 texture, { 0.0F, 0.0F,
static_cast<float>(map_size.x),
static_cast<float>(map_size.y) });
Asset routines for the ASW library.
2D camera that follows a target, stays inside the world and shakes.
bool collides(const Quad &other) const
Check if a rectangle is inside the rectangle.
Vec2< T > size
The size of the rectangle.
Vec2< T > position
The position of the rectangle.
T y
The y component of the vector.
T x
The x component of the vector.
Real magnitude() const
Calculate the magnitude of the vector.
T dot(const Vec2 &other) const
Calculate the dot product of two vectors.
float get_length() const
Get the length of one cycle.
AmbientCycle(float length=1.0F)
Create a cycle.
asw::Color at(float time) const
Get the colour at a time. Times past the length wrap around.
void add(float time, const asw::Color &color)
Add a key colour. Colours between keys are blended.
void clear()
Remove the lights, glows and tiles added this frame.
void add_glow(const asw::Texture &texture, const asw::Quad< float > &dest, const asw::Color &tint=asw::Color(255, 255, 255))
Add a texture that gives off light, such as lava or a lamp sprite. Its colours are added to the light...
void add_occluder(const asw::Polygonf &polygon)
Add a polygon that blocks light.
void set_ambient(const asw::Color &color)
Set the colour of unlit areas. Black is pitch dark, white is fully lit.
void draw_light(const Light &light)
const asw::Texture & gradient(asw::Falloff falloff)
void set_camera(const asw::Camera *camera)
Take positions in world space and convert them with a camera. The camera must outlive the light map,...
const std::vector< asw::Polygonf > & get_occluders() const
Get the occluders.
bool has_blockers(const asw::Quad< float > &reach) const
void draw_shadows(const asw::Vec2< float > ¢er, const asw::Quad< float > &reach)
void draw()
Draw the light map and multiply it over the current render target.
void add_tiles(const TileLight &tile_light, const asw::Vec2< float > &position={ })
Add tile lighting for this frame. The grid must outlive the next render() or draw().
void update(float dt)
Advance flicker and pulse. Call once per update.
void clear_occluders()
Remove all occluders.
const asw::Texture & render()
Render the light map without drawing it, to draw it yourself. The render target is the same afterward...
asw::Color get_ambient() const
Get the colour of unlit areas.
asw::Vec2< float > to_screen(const asw::Vec2< float > &point) const
void set_shadow_mode(ShadowMode mode)
Set how occluders block light.
void add(const Light &light)
Add a light for this frame.
ShadowMode get_shadow_mode() const
Get how occluders block light.
Light that spreads across a grid of tiles and stops at solid tiles.
bool is_solid(int x, int y) const
Get whether a tile blocks light. Tiles off the grid are solid.
void compute()
Spread the lights and update the texture. Call after lights or solid tiles change.
void add_light(int x, int y, const asw::Color &color)
Add a light at a tile.
asw::Color get(int x, int y) const
Get the light at a tile after compute().
asw::Vec2< float > get_size() const
Get the size of the grid in pixels.
const asw::Texture & get_texture() const
Get the texture, with one pixel per tile. Empty before compute().
void clear_lights()
Remove all lights.
bool in_grid(int x, int y) const
TileLight(int width, int height, float tile_size)
Create a grid with no solid tiles and no lights.
void set_falloff(float amount)
Set how much light is lost per tile, from 0 to 1.
void set_solid(int x, int y, bool solid)
Set whether a tile blocks light. A solid tile is lit, but does not pass light on.
Display and window routines for the ASW library.
Routines for drawing sprites and primitives to the screen.
Easing functions for animations.
2D lighting with a light map, shadows and tile lighting
constexpr int GRADIENT_SIZE
float wrap(float time, float length)
void textured_fan(const asw::Texture &texture, const Vec2< float > ¢er, float radius, const Polygonf &points, const asw::Color &color, bool closed)
Polygonf arc(const Vec2< float > ¢er, float radius, float direction, float cone)
constexpr float SHADOW_FAR
std::size_t cell(int x, int y, int width)
asw::Color scale_color(const asw::Color &color, float amount)
asw::Texture create_texture(int w, int h)
Create a Texture given the specified dimensions.
asw::Texture create_radial_gradient(int size, asw::Color inner, asw::Color outer, asw::Falloff falloff=asw::Falloff::Smooth)
Create a square texture with a radial gradient. The colour goes from inner at the centre to outer at ...
asw::Vec2< int > get_logical_size()
Get the logical size of the window. This may differ from the actual size if scaling is enabled.
void set_render_target(const asw::Texture &texture)
Set the render target to the window.
void clear()
Clear the window to black.
asw::Renderer * get_renderer()
Get the SDL renderer.
void set_tint(const asw::Texture &texture, asw::Color tint)
Set the tint of a texture. Each colour channel is multiplied by the tint when drawn,...
void stretch_sprite(const asw::Texture &tex, const asw::Quad< float > &position)
Draw a sprite with the option to stretch it.
void rect_fill(const asw::Quad< float > &position, asw::Color color)
Draw a filled rectangle.
void set_blend_mode(const asw::Texture &texture, asw::BlendMode mode)
Set the blend mode of a texture.
void stretch_sprite_blit(const asw::Texture &tex, const asw::Quad< float > &source, const asw::Quad< float > &dest)
Draw a sprite with the option to stretch a portion of it.
float smoothstep(float t)
Soft at both ends, 3t^2 - 2t^3.
Polygonf visibility(const Vec2f &from, float radius, const std::vector< Polygonf > &occluders, float direction=0.0F, float cone=0.0F)
Find the area that can be seen from a point.
Vec2< T >::Real signed_area(const Polygon< T > &polygon)
Get the signed area of a polygon.
Quad< T > bounds(const Polygon< T > &polygon)
Get the smallest rectangle that holds every corner of a polygon.
ShadowMode
How lights are blocked by occluders.
@ None
Light goes through occluders.
Polygon< float > Polygonf
SDL_FColor FColor
A colour with each channel as a float from 0.0 to 1.0.
std::shared_ptr< SDL_Texture > Texture
Alias for a shared pointer to an SDL_Texture.
Falloff
How a gradient or light fades from its centre to its edge.
RGBA color struct with 8-bit channels.
static uint8_t to_channel(float value)
Round a 0-255 channel value and clamp it into range. Casting an out of range float straight to uint8_...
FColor to_fcolor() const
To a float colour, with each channel from 0.0 to 1.0.
A light. Positions are in world space when the light map has a camera, otherwise in screen space.
float pulse
Size of a steady pulse from 0 to 1.
asw::Color color
Colour of the light.
float flicker
Random flicker from 0 to 1. 0.2 suits a torch.
float cone
Width of a spot light's beam, in radians. 0 lights all round.
float intensity
Brightness from 0 to 1. Scales the colour.
float direction
Direction a spot light points, in radians. 0 is right, and angles turn clockwise on screen.
bool shadows
Whether occluders block this light.
asw::Vec2< float > position
Centre of the light.
float pulse_speed
Pulses per second.
float radius
Distance the light reaches, in pixels.
int seed
Gives lights their own flicker and pulse timing. Use a different seed for each light.
asw::Falloff falloff
How the light fades to its edge.