;;;; raylib [textures] example - fog of war ;;;; ;;;; examples/textures/textures_fog_of_war.c. Picked because it is the only ;;;; example in the whole upstream tree where a *texture filter* is the point ;;;; rather than a detail. The fog is drawn into a render texture one pixel ;;;; per tile — 25 by 15 — and then stretched to 800x450 on the way to the ;;;; screen, and the smooth edge between seen and unseen is entirely the ;;;; bilinear sampler doing the interpolation. With the default ;;;; :filter-point the same program draws 32-pixel squares. ;;;; ;;;; That is what needed adding: `TextureFilter`, a new enum in ;;;; vendor/raylib/raylib.fln, with set-texture-filter moved from the ;;;; generated half to the hand-written one and mapped in `bindings` so its ;;;; six members are checked against raylib.h. The header says `int filter` ;;;; and there is nothing in an `int` to say that 1 is the interesting value; ;;;; `:filter-bilinear` says it. ;;;; ;;;; The three other things it exercises, none of which needed a line: ;;;; ;;;; - A render texture used as a *scratch surface* rather than as a ;;;; post-processing pass. core-scissor-test clipped the real framebuffer; ;;;; this draws a whole second frame into a 25x15 target and then samples ;;;; it. begin-texture-mode/end-texture-mode were already bound. ;;;; - draw-texture-pro with a NEGATIVE source height. A render texture's ;;;; rows come out of GL bottom-up, so every program that samples one has ;;;; to flip it, and raylib's convention for that is a source rectangle ;;;; with a negative height rather than a separate flag. Nothing in the ;;;; corpus had passed one. A Rectangle whose `height` field had been ;;;; permuted with `width` would draw the fog mirrored and the wrong way ;;;; up at once. ;;;; - clear-background with `blank` — alpha 0 — inside a texture mode, so ;;;; the untouched parts of the fog target are transparent and the map ;;;; shows through. ;;;; ;;;; The C's Map struct with its two calloc'd `unsigned char *` is two `[375 ;;;; u8]` globals here. The dimensions are compile-time constants in the C ;;;; too (the #defines and the two assignments right after), so nothing is ;;;; lost by fixing them, and a global cannot hold a Vec in any case ;;;; (docs/PORTING.md §3). 375 is 25*15, written out because Flan's array length ;;;; must be a literal. import rl "vendor:raylib" import d "digits.fln" const screen-width = 800 const screen-height = 450 const map-tile-size = 32 const player-size = 16 ;; How far the player sees, in tiles. The C's loop runs from -this to +this ;; exclusive, so the lit patch is 2*this tiles across and not 2*this+1 — an ;; asymmetry in the original that is kept rather than tidied, because tidying ;; it would change the picture. const player-tile-visibility = 2 const tiles-x = 25 const tiles-y = 15 ;; 0 or 1, picked once: which of the two blues a tile is drawn in. once tile-ids: [375 u8] ;; 0 = never seen (solid black), 1 = visible now (no fog), 2 = seen before ;; (mostly black). The three-way state is why this is a byte per tile and not ;; a bit. once tile-fog: [375 u8] once player-position: rl/Vector2 once player-tile-x: i32 once player-tile-y: i32 once fog-of-war: rl/RenderTexture2D fn main() -> () rl/init-window(screen-width, screen-height, "raylib [textures] example - fog of war") defer rl/close-window() tile-ids = array(375, u8) tile-fog = array(375, u8) for i in range(tiles-x * tiles-y) tile-ids[i] = u8(rl/get-random-value(0, 1)) player-position = rl/Vector2{.x 180.0 .y 130.0} player-tile-x = 0 player-tile-y = 0 ;; One texel per tile. The stretch to full size on draw is where the ;; smoothing happens, which is why the target is this small on purpose. fog-of-war = rl/load-render-texture(tiles-x, tiles-y) defer rl/unload-render-texture(fog-of-war) rl/set-texture-filter(fog-of-war.texture, :filter-bilinear) rl/set-target-fps(60) until rl/window-should-close() ;; Update if rl/is-key-down(:key-right) player-position.x += 5.0 if rl/is-key-down(:key-left) player-position.x -= 5.0 if rl/is-key-down(:key-down) player-position.y += 5.0 if rl/is-key-down(:key-up) player-position.y -= 5.0 ;; Keep the player inside the tilemap. The far edge is measured against ;; the player's far side, which is why player-size is subtracted. let max-x = f32(tiles-x * map-tile-size - player-size) let max-y = f32(tiles-y * map-tile-size - player-size) player-position.x = clamp(player-position.x, 0.0, max-x) player-position.y = clamp(player-position.y, 0.0, max-y) ;; Everything lit on the previous frame drops to "seen before". The lit ;; tiles are then set back to 1 below, so a tile still in view never ;; spends a frame dimmed. for i in range(tiles-x * tiles-y) if tile-fog[i] == 1 tile-fog[i] = 2 ;; Which tile the player's centre is standing on. player-tile-x = i32((player-position.x + f32(map-tile-size / 2)) / f32(map-tile-size)) player-tile-y = i32((player-position.y + f32(map-tile-size / 2)) / f32(map-tile-size)) ;; Light the square around the player, skipping anything off the map. ;; Without that test this reads and writes outside the array, which in the ;; C is undefined and here is a bounds trap — the same bug, reported. let vis-y = player-tile-y - player-tile-visibility while vis-y < player-tile-y + player-tile-visibility let x = player-tile-x - player-tile-visibility while x < player-tile-x + player-tile-visibility if x >= 0 and x < tiles-x and vis-y >= 0 and vis-y < tiles-y tile-fog[vis-y * tiles-x + x] = 1 x += 1 vis-y += 1 ;; Draw the fog into its own little target first, at one pixel per tile. ;; blank is alpha 0, so a tile that is neither unseen nor remembered ;; leaves nothing behind and the map below shows through unmodified. rl/with-texture-mode(fog-of-war): rl/clear-background(rl/blank) for y in range(tiles-y) for x in range(tiles-x) ;; A tile in view (1) draws nothing at all and stays transparent. let f = tile-fog[y * tiles-x + x] if f != 1 rl/draw-rectangle(x, y, 1, 1, if f == 0 then rl/black else rl/fade(rl/black, 0.8)) rl/with-drawing: rl/clear-background(rl/raywhite) ;; The map itself, in full size. for y in range(tiles-y) for x in range(tiles-x) rl/draw-rectangle(x * map-tile-size, y * map-tile-size, map-tile-size, map-tile-size, if tile-ids[y * tiles-x + x] == 0 then rl/blue else rl/fade(rl/blue, 0.9)) rl/draw-rectangle-lines(x * map-tile-size, y * map-tile-size, map-tile-size, map-tile-size, rl/fade(rl/darkblue, 0.5)) rl/draw-rectangle-v(player-position, rl/Vector2{.x f32(player-size) .y f32(player-size)}, rl/red) ;; The fog, stretched over the whole map. The negative source height is ;; the flip — see the header comment. rl/draw-texture-pro(fog-of-war.texture, rl/Rectangle{.x 0.0, .y 0.0, .width f32(fog-of-war.texture.width), .height 0.0 - f32(fog-of-war.texture.height)}, rl/Rectangle{.x 0.0 .y 0.0 .width f32(tiles-x * map-tile-size) .height f32(tiles-y * map-tile-size)}, rl/Vector2{.x 0.0 .y 0.0}, 0.0, rl/white) ;; The C's TextFormat("Current tile: [%i,%i]", ...). Each number is drawn ;; before the next is formatted, which examples/digits.fln requires: ;; both share one static buffer in the runtime. let x = 10 x += d/draw-piece("Current tile: [", x, 10, 20, rl/raywhite) x += d/draw-int(player-tile-x, x, 10, 20, rl/raywhite) x += d/draw-piece(",", x, 10, 20, rl/raywhite) x += d/draw-int(player-tile-y, x, 10, 20, rl/raywhite) d/draw-piece("]", x, 10, 20, rl/raywhite) rl/draw-text("ARROW KEYS to move", 10, screen-height - 25, 20, rl/raywhite)