6 n__iso__engine_8h
gullradriel edited this page 2026-10-09 00:55:18 +02:00

include/nilorea/n_iso_engine.h

Isometric/axonometric tile engine with height maps, terrain transitions, and A* pathfinding integration. More...

Structures

Name
struct N_ISO_OBJECT
Drawable object for depth-sorted isometric rendering.
struct N_ISO_CAMERA
2D isometric camera for viewport management.
struct ISO_VISIBLE_EDGES
Per-tile edge visibility flags for height border rendering.
struct ISO_TILE_SEGMENTS
Per-tile height segments.
struct ISO_TILE_SEGMENT
Single vertical segment of a tile: solid matter from bottom to top.
struct ISO_PROJECTION
Axonometric projection parameters.
struct ISO_MAP
Height-aware isometric map with terrain and height layers, per-cell height values, configurable projection, and terrain transition support.
struct ISO_DRAW_ENTRY
Draw order entry for segment-sorted rendering.

Detailed Description

Isometric/axonometric tile engine with height maps, terrain transitions, and A* pathfinding integration.

Author: Castagnier Mickael

Version: 2.0

Date: 01/03/2026

Provides data structures and utility functions for diamond-layout isometric maps with:

  • Per-cell terrain type, height, ability, object, and music layers
  • Configurable axonometric projections (classic, steep, wide, military)
  • Map-to-screen and screen-to-map coordinate conversion
  • Bilinear height interpolation for smooth terrain
  • Terrain transition bitmask computation (edge + corner)
  • Integration with n_astar for pathfinding on the tile grid
  • Chunk-based binary map file format (save/load)

Based on:

  • GameDev.net Articles 747/748: "Isometric Tile Engine" series
  • GameDev.net Article 934: "Terrain Transitions"
  • GameDev.net Article 1269: "Axonometric Projections"
  • GameDev.net Article 2026: "Height-Mapped Isometric"

Source code

/*
 * Nilorea Library
 * Copyright (C) 2005-2026 Castagnier Mickael
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */


#ifndef NILOREA_ISOMETRIC_ENGINE
#define NILOREA_ISOMETRIC_ENGINE

#ifdef __cplusplus
extern "C" {
#endif


#include "n_common.h"
#include "n_log.h"

/* Ability flags */

#define WALK 1
#define SWIM 2
#define BLCK 3

/* Terrain transition bitmask definitions (Article 934)
 * Edge bits represent which cardinal sides have a higher-priority terrain.
 * Corner bits represent which diagonal neighbors have a higher-priority
 * terrain (only when the adjacent edges do NOT have that terrain).
 */

#define ISO_EDGE_W (1 << 0)
#define ISO_EDGE_N (1 << 1)
#define ISO_EDGE_E (1 << 2)
#define ISO_EDGE_S (1 << 3)
#define ISO_NUM_EDGE_MASKS 16

#define ISO_CORNER_NW (1 << 0)
#define ISO_CORNER_NE (1 << 1)
#define ISO_CORNER_SE (1 << 2)
#define ISO_CORNER_SW (1 << 3)
#define ISO_NUM_CORNER_MASKS 16
#define ISO_NUM_MASKS (ISO_NUM_EDGE_MASKS + ISO_NUM_CORNER_MASKS)

/* Projection presets */

#define ISO_PROJ_CLASSIC 0
#define ISO_PROJ_ISOMETRIC 1
#define ISO_PROJ_MILITARY 2
#define ISO_PROJ_STAGGERED 3
#define ISO_NUM_PROJECTIONS 4

/* Legacy aliases */
#define ISO_PROJ_STEEP ISO_PROJ_ISOMETRIC
#define ISO_PROJ_WIDE ISO_PROJ_STAGGERED

/* New height-aware isometric data structures (no Allegro dependency) */

#define ISO_MAX_SEGMENTS_PER_TILE 2

typedef struct ISO_TILE_SEGMENT {
    int bottom;     
    int top;        
    int upper_tile; 
    int lower_tile; 
} ISO_TILE_SEGMENT;

typedef struct ISO_TILE_SEGMENTS {
    int count; 
    ISO_TILE_SEGMENT segs[ISO_MAX_SEGMENTS_PER_TILE];
} ISO_TILE_SEGMENTS;

typedef struct ISO_DRAW_ENTRY {
    int mx, my;    
    int seg_idx;   
    int bottom;    
    int top;       
    int underside; 
} ISO_DRAW_ENTRY;

typedef struct ISO_PROJECTION {
    float half_w;       
    float half_h;       
    float angle_deg;    
    float tile_lift;    
    float target_angle; 
    float lerp_speed;   
} ISO_PROJECTION;

/* 2D Isometric Camera
 * Manages camera position, zoom, and coordinate conversion between
 * screen pixels and world coordinates.  Works with iso_map_draw()
 * and iso_screen_to_map_height() using the library coordinate system.
 */

typedef struct N_ISO_CAMERA {
    float x;        
    float y;        
    float zoom;     
    float zoom_min; 
    float zoom_max; 
} N_ISO_CAMERA;

typedef struct ISO_MAP {
    int width;        
    int height;       
    int num_terrains; 
    int max_height;   

    int* terrain;   
    int* heightmap; 
    int* ability;   
    int* overlay;   

    ISO_TILE_SEGMENTS* segments;

    ISO_PROJECTION proj; 

    /* rendering flags (used by iso_map_draw) */
    int smooth_height;    
    int smooth_slope_max; 
    int show_grid;        
    int hover_mx;         
    int hover_my;         

    /* height-based ambient tinting */
    float height_tint_intensity; 

    /* neighbor edge heights for chunked rendering */
    int* neighbor_heights_west;
    int* neighbor_heights_north;
    int* neighbor_heights_east;
    int* neighbor_heights_south;

    /* neighbor edge terrains for chunked transition masks
     * When a tile sits at a chunk edge, iso_map_calc_transitions_full
     * needs to know the terrain type on the far side to decide whether
     * to blit a bleed mask. Without these, the out-of-bounds terrain
     * lookup returns 0 (water), so every cross-chunk transition is
     * silently dropped and a hard diagonal seam appears along the
     * chunk boundary. NULL means the chunk is on the world boundary
     * or the neighbor is not resident; callers fall back to the
     * previous "treat as base" behaviour. */
    int* neighbor_terrains_west;  
    int* neighbor_terrains_north; 
    int* neighbor_terrains_east;  
    int* neighbor_terrains_south; 

    float ambient_r, ambient_g, ambient_b;

    float* dynamic_light_map;

    ISO_DRAW_ENTRY* cached_draw_order;
    int cached_draw_order_count;
    int cached_draw_order_cap;
    int draw_order_dirty;
} ISO_MAP;

/* New height-aware isometric API (Articles 747/748/934/1269/2026)
 * These functions do NOT require Allegro.
 */

ISO_MAP* iso_map_new(int width, int height, int num_terrains, int max_height);
void iso_map_free(ISO_MAP** map);

void iso_map_set_projection(ISO_MAP* map, int preset, float tile_width);
void iso_map_set_projection_custom(ISO_MAP* map, float half_w, float half_h, float tile_lift);
void iso_map_set_projection_target(ISO_MAP* map, int preset);
void iso_map_lerp_projection(ISO_MAP* map, float dt);
const char* iso_projection_name(int preset);

int iso_map_get_terrain(const ISO_MAP* map, int mx, int my);
void iso_map_set_terrain(ISO_MAP* map, int mx, int my, int terrain);
int iso_map_get_height(const ISO_MAP* map, int mx, int my);
void iso_map_set_height(ISO_MAP* map, int mx, int my, int h);
int iso_map_get_ability(const ISO_MAP* map, int mx, int my);
void iso_map_set_ability(ISO_MAP* map, int mx, int my, int ab);

void iso_map_to_screen(const ISO_MAP* map, int mx, int my, int h, float* screen_x, float* screen_y);
void iso_screen_to_map(const ISO_MAP* map, float screen_x, float screen_y, int* mx, int* my);
void iso_map_to_screen_f(const ISO_MAP* map, float fmx, float fmy, float h, float* screen_x, float* screen_y);
void iso_corner_to_screen(const ISO_MAP* map, int cx, int cy, float fh, float cam_x, float cam_y, float zoom, float* sx, float* sy);
void iso_screen_to_map_height(const ISO_MAP* map, float screen_x, float screen_y, int tile_w, int tile_h, int* mx, int* my);
void iso_screen_to_map_height_f(const ISO_MAP* map, float screen_x, float screen_y, int tile_w, int tile_h, int* mx, int* my, float* out_fx, float* out_fy);

int iso_is_in_diamond(int px, int py, int tile_w, int tile_h);
float iso_diamond_dist(int px, int py, int tile_w, int tile_h);

float iso_map_interpolate_height(const ISO_MAP* map, float fx, float fy);

void iso_map_calc_transitions(const ISO_MAP* map, int mx, int my, int* edge_bits, int* corner_bits);
int iso_map_should_transition(const ISO_MAP* map, int mx1, int my1, int mx2, int my2);

void iso_map_corner_heights(const ISO_MAP* map, int mx, int my, float* h_n, float* h_e, float* h_s, float* h_w);

int iso_map_save(const ISO_MAP* map, const char* filename);
ISO_MAP* iso_map_load(const char* filename);

void iso_map_randomize(ISO_MAP* map);

#ifdef N_ASTAR_H
ASTAR_GRID* iso_map_to_astar_grid(const ISO_MAP* map, int max_height_diff, int start_x, int start_y);
#endif

void iso_map_smooth_corner_heights(const ISO_MAP* map, int mx, int my, float* h_n, float* h_e, float* h_s, float* h_w);

int iso_map_set_segments(ISO_MAP* map, int mx, int my, const ISO_TILE_SEGMENT* segs, int count);

const ISO_TILE_SEGMENTS* iso_map_get_segments(const ISO_MAP* map, int mx, int my);

int iso_map_build_draw_order(const ISO_MAP* map, ISO_DRAW_ENTRY* out, int max_entries);

int iso_map_should_transition_smooth(const ISO_MAP* map, int mx1, int my1, int mx2, int my2);

void iso_map_calc_transitions_full(const ISO_MAP* map, int mx, int my, int* edge_bits, int* corner_bits);

/* Height border edge visibility
 * Determines which diamond edges of an elevated tile should be outlined.
 * An edge is drawn when the adjacent tile has a height difference > 1
 * or does not exist (map boundary).
 */

typedef struct ISO_VISIBLE_EDGES {
    int draw_nw;         
    int draw_ne;         
    int draw_se;         
    int draw_sw;         
    int draw_underside;  
    int neighbor_top_nw; 
    int neighbor_top_ne; 
    int neighbor_top_se; 
    int neighbor_top_sw; 
} ISO_VISIBLE_EDGES;

ISO_VISIBLE_EDGES iso_map_get_visible_edges(const ISO_MAP* map, int mx, int my);

ISO_VISIBLE_EDGES iso_map_get_visible_edges_segment(const ISO_MAP* map, int mx, int my, int seg_idx);

/* N_ISO_CAMERA API
 * 2D camera management for isometric viewports.  Coordinates match the
 * library convention used by iso_map_to_screen() and iso_map_draw().
 */

N_ISO_CAMERA* n_iso_camera_new(float zoom_min, float zoom_max);

void n_iso_camera_free(N_ISO_CAMERA** cam);

void n_iso_camera_scroll(N_ISO_CAMERA* cam, float dx, float dy);

void n_iso_camera_zoom(N_ISO_CAMERA* cam, float dz, float mouse_x, float mouse_y);

void n_iso_camera_center_on(N_ISO_CAMERA* cam, float world_x, float world_y, int screen_w, int screen_h);

void n_iso_camera_follow(N_ISO_CAMERA* cam, float target_x, float target_y, int screen_w, int screen_h, float smoothing, float dt);

void n_iso_camera_screen_to_world(const N_ISO_CAMERA* cam, float sx, float sy, float* wx, float* wy);

void n_iso_camera_world_to_screen(const N_ISO_CAMERA* cam, float wx, float wy, float* sx, float* sy);

/* Isometric drawable object for depth-sorted rendering
 * Objects are interleaved with tiles during iso_map_draw() based on
 * their map position, achieving correct painter's algorithm ordering.
 * When an object is behind a taller tile, it can optionally be redrawn
 * as a transparent overlay so the player can still see it.
 */

typedef void (*N_ISO_OBJECT_DRAW_FN)(float screen_x, float screen_y, float zoom, float alpha, void* user_data);

typedef struct N_ISO_OBJECT {
    float fx;                  
    float fy;                  
    float fz;                  
    float occluded_alpha;      
    float sprite_h;            
    float sprite_half_w;       
    int is_occluded;           
    float occlude_clip_y;      
    N_ISO_OBJECT_DRAW_FN draw; 
    void* user_data;           
} N_ISO_OBJECT;

/* ISO_MAP rendering API (requires Allegro 5)
 * Provides full isometric rendering with height, terrain transitions,
 * cliff walls, hover highlight, and grid overlay. Requires pre-generated
 * tile bitmaps and transition mask overlays from the application.
 */
#ifdef HAVE_ALLEGRO

#include <allegro5/allegro.h>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_font.h>

void iso_map_draw(const ISO_MAP* map,
                  ALLEGRO_BITMAP** tile_bitmaps,
                  ALLEGRO_BITMAP*** transition_tiles,
                  int num_masks,
                  ALLEGRO_BITMAP** overlay_bitmaps,
                  int num_overlay_tiles,
                  float cam_px,
                  float cam_py,
                  float zoom,
                  int screen_w,
                  int screen_h,
                  int player_mode,
                  N_ISO_OBJECT* objects,
                  int num_objects);

void iso_mask_tile_to_diamond(ALLEGRO_BITMAP* bmp, int tile_w, int tile_h);

void iso_generate_transition_masks(ALLEGRO_BITMAP** masks, int tile_w, int tile_h);

void iso_generate_transition_tiles(ALLEGRO_BITMAP*** tiles,
                                   ALLEGRO_BITMAP** masks,
                                   ALLEGRO_BITMAP** tile_bitmaps,
                                   int num_terrains,
                                   int tile_w,
                                   int tile_h);

extern double n_iso_pass1_us;
extern double n_iso_pass2_us;
extern double n_iso_pass3_us;
/* Pre-pass setup cost inside iso_map_draw: draw_order alloc +
 * iso_map_build_draw_order + object screen-pos compute + insertion
 * sort. Surfaced separately from Pass 1/2/3 because in profiled runs
 * this section can be > 50 % of the total iso_map_draw cost when
 * objects are dense. */
extern double n_iso_setup_us;

typedef ALLEGRO_COLOR (*IsoPrimitiveColorFilter)(ALLEGRO_COLOR c);
void iso_set_primitive_color_filter(IsoPrimitiveColorFilter fn);
ALLEGRO_COLOR iso_apply_primitive_color_filter(ALLEGRO_COLOR c);

/* Per-frame callback that returns the screen-space (dx, dy) offset
 * which the consumer has added to its world camera AFTER the iso
 * engine's `cam_px / cam_py` were sampled (e.g. a temporal-AA
 * sub-pixel jitter applied on top of the camera snap). The engine
 * subtracts this offset from the endpoints of every decorative line
 * draw (cliff-base edge lines in `_draw_segment_sides`; top-face
 * edges, vertical corner lines, and underside borders in
 * `_draw_height_borders`) so those lines stay pixel-stable across
 * frames while filled polygon faces and bitmap sprites continue to
 * inherit the jitter (which is what drives the temporal-AA averaging
 * on bitmap content). Without the compensation, thin anti-aliased
 * lines hit a different sub-pixel position each frame; the
 * rasterizer's AA coverage flickers; any temporal blend downstream
 * (TAA's history-weight composite) then averages two slightly
 * offset semi-transparent lines into visible grey/white pixels along
 * the line.
 *
 * Default NULL = no compensation. Consumers without a jitter source
 * pay nothing. The callback is invoked per-tile during
 * `iso_map_draw` (potentially hundreds of times per frame), so it
 * must be O(1): typically returns the cached jitter from a
 * `_begin_frame` call elsewhere. Returning (0, 0) is the "no
 * compensation this frame" signal and is the expected steady state
 * when the jitter source is off (e.g. camera stationary, TAA
 * disabled). */
typedef void (*IsoLineJitterCompensationFn)(float* out_dx, float* out_dy);
void iso_set_line_jitter_compensation(IsoLineJitterCompensationFn fn);
void iso_apply_line_jitter_compensation(float* out_dx, float* out_dy);

#endif /* HAVE_ALLEGRO */


#ifdef __cplusplus
}
#endif

#endif /* #ifndef NILOREA_ISOMETRIC_ENGINE */