diff options
Diffstat (limited to 'src/game')
| -rw-r--r-- | src/game/assets.cpp | 78 | ||||
| -rw-r--r-- | src/game/assets.h | 23 | ||||
| -rw-r--r-- | src/game/object.cpp | 0 | ||||
| -rw-r--r-- | src/game/object.h | 73 | ||||
| -rw-r--r-- | src/game/objlist.cpp | 44 | ||||
| -rw-r--r-- | src/game/objlist.h | 122 | ||||
| -rw-r--r-- | src/game/physics/movement.h | 0 | ||||
| -rw-r--r-- | src/game/player.cpp | 82 | ||||
| -rw-r--r-- | src/game/player.h | 22 | ||||
| -rw-r--r-- | src/game/raycast.cpp | 162 | ||||
| -rw-r--r-- | src/game/raycast.h | 65 | ||||
| -rw-r--r-- | src/game/vars.cpp | 115 | ||||
| -rw-r--r-- | src/game/vars.h | 50 | ||||
| -rw-r--r-- | src/game/world/bsp.cpp | 662 | ||||
| -rw-r--r-- | src/game/world/bsp.h | 103 | ||||
| -rw-r--r-- | src/game/world/bsp_draw.cpp | 124 | ||||
| -rw-r--r-- | src/game/world/bsp_draw.h | 4 | ||||
| -rw-r--r-- | src/game/world/draw.cpp | 216 | ||||
| -rw-r--r-- | src/game/world/draw.h | 19 | ||||
| -rw-r--r-- | src/game/world/map.cpp | 640 | ||||
| -rw-r--r-- | src/game/world/map.h | 137 | ||||
| -rw-r--r-- | src/game/world/trace.cpp | 393 | ||||
| -rw-r--r-- | src/game/world/trace.h | 29 | ||||
| -rw-r--r-- | src/game/world/world.cpp | 14 | ||||
| -rw-r--r-- | src/game/world/world.h | 11 |
25 files changed, 3188 insertions, 0 deletions
diff --git a/src/game/assets.cpp b/src/game/assets.cpp new file mode 100644 index 0000000..7d755c0 --- /dev/null +++ b/src/game/assets.cpp @@ -0,0 +1,78 @@ +#include "assets.h" + +#include "../game.h" +#include "../render/gl_2d_font.h" + +void assets_init( GAME_DATA *game ) { + game->assets.test = gl_texture_create( game->gl, "assets/test.png" ); + game->assets.fonts_init = 0; +} + +void assets_create_fonts( struct GAME_DATA *game ) { + dlog( "creating fonts\n" ); + + game->gl->fonts.each( fn( GL_FONT** font ) { + gl_font_destroy( game->gl, *font ); + }); + + game->gl->fonts.clear(); + GAME_ASSETS* a = &game->assets; + a->font = gl_font_create( game->gl, "cour.ttf", 16 ); + a->jpn12 = gl_font_create( game->gl, "jpn12.ttf", 12 ); + a->jpn16 = gl_font_create( game->gl, "jpn16.ttf", 16 ); +} + +U8 assets_on_frame( struct GAME_DATA *game ) { + thread_mutex_lock( &font_mutex ); + if( !game->assets.fonts_init ) { + assets_create_fonts( game ); + game->assets.fonts_init = 1; + thread_mutex_unlock( &font_mutex ); + return 0; + } + + thread_mutex_unlock( &font_mutex ); + return 1; +} + +LIST<FILE_ENTRY> assets_get_files_by_ext_dir( const char** extensions, const char* dirname ) { + LIST<FILE_ENTRY> ret; + LIST<FILE_ENTRY> dir = dir_get_entries( dirname ); + + dir.each( fn( FILE_ENTRY* e ) { + char fullpath[256]; + sprintf( fullpath, "%s/%s", dirname, e->name ); + if( e->dir ) { + LIST<FILE_ENTRY> subdir = assets_get_files_by_ext_dir( extensions, fullpath ); + subdir.each( fn( FILE_ENTRY* se ) { ret.push( *se ); } ); + return; + } + + U32 len = strlen( e->name ); + for( U32 i = 0; !!extensions[i]; ++i ) { + const char* ext = extensions[i]; + U32 extlen = strlen( ext ); + + if( extlen < len && e->name[len - extlen - 1] == '.' ) { + if( !strcmp( e->name + len - extlen, ext ) ) { + FILE_ENTRY ne{ .dir = 0 }; + strcpy( ne.name, fullpath ); + ret.push( ne ); + } + } + } + } ); + + return ret; +} + +LIST<FILE_ENTRY> assets_get_files_by_ext( const char** extensions ) { + return assets_get_files_by_ext_dir( extensions, "../assets" ); +} + +const char* assets_abspath( const char* filename ) { + while( !strncmp( filename, "../assets/", 10 ) ) + filename += 10; + + return filename; +} diff --git a/src/game/assets.h b/src/game/assets.h new file mode 100644 index 0000000..d0fe270 --- /dev/null +++ b/src/game/assets.h @@ -0,0 +1,23 @@ +#pragma once +#include "../util/typedef.h" +#include "../util/file.h" + + +typedef struct { + struct GL_FONT* font; + struct GL_FONT* jpn12; + struct GL_FONT* jpn16; + struct GL_TEX2D* test; + + U8 fonts_init; +} GAME_ASSETS; + +extern void assets_init( struct GAME_DATA* game ); +extern void assets_create_fonts( struct GAME_DATA* game ); +// extensions is a null-terminated array of null-terminated strings. +extern LIST<FILE_ENTRY> assets_get_files_by_ext_dir( const char** extensions, const char* dir ); +// extensions is a null-terminated array of null-terminated strings. +extern LIST<FILE_ENTRY> assets_get_files_by_ext( const char** extensions ); +extern const char* assets_abspath( const char* filepath ); + +extern U8 assets_on_frame( struct GAME_DATA* game ); diff --git a/src/game/object.cpp b/src/game/object.cpp new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/game/object.cpp diff --git a/src/game/object.h b/src/game/object.h new file mode 100644 index 0000000..8191ab7 --- /dev/null +++ b/src/game/object.h @@ -0,0 +1,73 @@ +#pragma once +#include "../util/allocator.h" +#include "../util/vector.h" +#include "../util/fnv.h" + +typedef void ( *OBJECT_DEINIT_FN )( struct OBJECT* obj ); +inline const char* obj_classid_to_name( U32 classid ); + +enum ObjectClass_t { + OBJCLASS_NONE = 0, + OBJCLASS_WORLD, + OBJCLASS_TRIGGER, + OBJCLASS_PLAYER, + OBJCLASS_BASENPC, +}; + +static const char* OBJCLASS_NAMES[] = { + "none (invalid - FIXME)", + "world", + "trigger", + "player", + "base_npc", +}; + +// todo: come up with a good way to make these static +struct OBJECT_PROP { + OBJECT_PROP( const char* name, void* value, U32 size, LIST<OBJECT_PROP*>* list, void* parent ) { + this->name = name; + this->size = size; + this->hash = fnv1a( name ); + this->offset = (U32)( (U8*)value - (U8*)parent ); + list->push( this ); + } + + const char* name; + FNV1A hash; + U32 offset; + U32 size; +}; + +#define OBJVAR( type, name ) \ + type name; \ + OBJECT_PROP __##name##_props{ #name, (void*)&name, sizeof(type), &this->props, this } + +#define OBJVAR_STR( name, len ) \ + char name##[len]; \ + OBJECT_PROP __##name##_props{ #name, (void*)name, len, &this->props, this } + +struct OBJECT { + static const U32 CLASSID = OBJCLASS_NONE; + + U32 classid; + U8 keeponlevel{}; + char name[64]; + + OBJECT* parent; + LIST<OBJECT*> children{}; + LIST<OBJECT_PROP*> props{}; + OBJECT_DEINIT_FN deinit_fn{}; + + OBJVAR( VEC3, pos ); + OBJVAR( VEC3, rot ); + OBJVAR( I32, idx ); +}; + +inline const char* obj_classid_to_name( U32 classid ) { + if( classid > (sizeof(OBJCLASS_NAMES) / sizeof(OBJCLASS_NAMES[0])) ) { + dlog( "obj_classid_to_name() : invalid classid passed (%d)", classid ); + return "INVALIDCLASSID"; + } + + return OBJCLASS_NAMES[classid]; +} diff --git a/src/game/objlist.cpp b/src/game/objlist.cpp new file mode 100644 index 0000000..01ce49b --- /dev/null +++ b/src/game/objlist.cpp @@ -0,0 +1,44 @@ +#include "objlist.h" +#include "world/world.h" +#include "world/map.h" + +OBJECT_LIST* objl; + +OBJECT_LIST* objl_init() { + OBJECT_LIST* ret = new OBJECT_LIST; + ret->world = 0; + ret->pl = 0; + + return ret; +} + +void objl_clear( U8 delete_player ) { + objl->objects.each( fn( OBJECT** ptr ) { + OBJECT* obj = *ptr; + + if( !delete_player && obj == objl->pl ) + return; + + if( obj->deinit_fn ) + obj->deinit_fn( obj ); + delete obj; + } ); + + objl->objects.clear(); + objl->world = 0; + if( delete_player ) + objl->pl = 0; +} + +void objl_load_world( GAME_DATA* game, WORLD_MAP* map ) { + objl_clear(); + + objl->world = world_create( map ); + if( !objl->pl ) + objl->pl = player_create( map->startpos, map->startang ); + + if( !OK( world_populate_entities( objl->world ) ) ) { + objl_clear(); + dlog( "objl_load_world() : error populating entities\n" ); + } +} diff --git a/src/game/objlist.h b/src/game/objlist.h new file mode 100644 index 0000000..f8a46d7 --- /dev/null +++ b/src/game/objlist.h @@ -0,0 +1,122 @@ +#pragma once + +#include "player.h" +#include "world/world.h" + +const U32 OBJ_LIST_MAX = 8192; + +struct OBJECT_LIST { + template <typename T> + T* add( const char* name ) { + if( objects.size >= OBJ_LIST_MAX ) { + dlog( "OBJECT_LIST::add() : object list is full" ); + return 0; + } + + for( I32 i = 0; i < objects.size; ++i ) { + if( !strcmp( name, objects[i]->name ) ) { + dlog( "OBJECT_LIST::add() : object %s already exists", name ); + return 0; + } + } + + T* newt = new T; + newt->classid = T::CLASSID; + strcpy( newt->name, name ); + objects.push( newt ); + return newt; + } + + template < typename T > + T* add( const T& obj ) { + for( I32 i = 0; i < objects.size; ++i ) { + if( !strcmp( obj.name, objects[i]->name ) ) { + dlog( "OBJECT_LIST::add() : object %s already exists", obj.name ); + return 0; + } + } + + T* newt = new T( obj ); + newt->classid = T::CLASSID; + objects.push( newt ); + return newt; + } + + void remove( OBJECT* obj ) { + I32 idx = objects.idx_of( obj ); + if( idx != -1 ) + free( objects[idx] ); + } + + void remove( const char* obj ) { + for( I32 i = 0; i < objects.size; ++i ) { + if( !strcmp( obj, objects[i]->name ) ) { + free( objects[i] ); + return; + } + } + } + + template < typename T = OBJECT > + T* find( const char* name ) { + for( I32 i = 0; i < objects.size; ++i ) { + if( !strcmp( name, objects[i]->name ) ) + return objects[i]; + } + + return 0; + } + + void each( LIST<OBJECT*>::ON_EACH_FN func ) { + objects.each( func ); + } + + WORLD* world; + PLAYER* pl; + LIST<OBJECT*> objects; +}; + +extern OBJECT_LIST* objl; + +extern OBJECT_LIST* objl_init(); +extern void objl_clear( U8 delete_player = 0 ); +extern void objl_load_world( GAME_DATA* game, WORLD_MAP* map ); + + +template < typename T > +inline T* obj_add( const char* name ) { + if( !objl ) { + dlog( "obj_add() : objl null\n" ); + return 0; + } + + return objl->add<T>( name ); +} + +template < typename T > +inline T* obj_add( T obj ) { + T* newt = new T( obj ); + newt = objl->add( newt ); + return newt; +} + +template < typename T > +inline T* obj_add( OBJECT* obj, const char* name ) { + T* newt = objl->add<T>( name ); + if( !newt ) + return 0; + + newt->parent = obj; + obj->children.push( newt ); + return newt; +} + +template < typename T > +inline T* obj_add( OBJECT* obj, T newobj ) { + T* newt = objl->add( newobj ); + if( !newt ) + return 0; + + newt->parent = obj; + obj->children.push( newt ); +} diff --git a/src/game/physics/movement.h b/src/game/physics/movement.h new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/game/physics/movement.h diff --git a/src/game/player.cpp b/src/game/player.cpp new file mode 100644 index 0000000..87786df --- /dev/null +++ b/src/game/player.cpp @@ -0,0 +1,82 @@ +#include "../game.h" +#include "SDL_scancode.h" +#include "objlist.h" +#include "world/trace.h" +#include "player.h" + +F32 PLAYER_HULL_HEIGHT = 50.f; +F32 PLAYER_HULL_WIDTH = 32.f; + +PLAYER* player_create( VEC3 origin, F32 yaw ) { + PLAYER* p = obj_add<PLAYER>( "localplayer" ); + p->pos = origin; + p->rot = { 0, yaw, 0 }; + p->health = 100; + p->keeponlevel = 1; + p->velocity = {}; + + p->mins = { + -PLAYER_HULL_WIDTH * .5f, + -PLAYER_HULL_WIDTH * .5f, + 0 + }; + + p->maxs = { + PLAYER_HULL_WIDTH * .5f, + PLAYER_HULL_WIDTH * .5f, + PLAYER_HULL_HEIGHT + }; + + return p; +} + +void player_input( GAME_DATA* game, PLAYER* p ) { + F32* yaw = &p->rot.y; + VEC3* pos = &p->pos; + + if( input.keys[(U8)'a'] ) + *yaw -= 360.f * TICK_INTERVAL * 0.5f; + if( input.keys[(U8)'d'] ) + *yaw += 360.f * TICK_INTERVAL * 0.5f; + + if( input.keys[SDL_SCANCODE_UP] ) { + if( p->rot.x < 89.f ) + p->rot.x += 360.f * TICK_INTERVAL * 0.5f; + } + if( input.keys[SDL_SCANCODE_DOWN] ) { + if( p->rot.x > -89.f ) + p->rot.x -= 360.f * TICK_INTERVAL * 0.5f; + } + + *yaw = remainderf( *yaw, 360.f ); + + + VEC3 wishmove; + VEC3 dir = m_radial_offset( *yaw, 70.f ); + if( input.keys[(U8)'w'] ) { + wishmove = dir * TICK_INTERVAL; + } + if( input.keys[(U8)'s'] ) { + wishmove = dir * -TICK_INTERVAL; + } + + // todo : should never be false + if( vec_len( wishmove ) > BSP_TRACE_EPSILON && game->state.map->bsp ) { + BSP_TRACE tr{}; + AABB aabb{}; + tr.in_start = *pos; + tr.in_end = *pos + wishmove * 2.f; + aabb.min = p->mins; + aabb.max = p->maxs; + + bsp_trace( &tr, game->state.map->bsp, aabb ); + if( !tr.hit ) + *pos += wishmove; + else + *pos += wishmove * tr.frac; + } +} + +VEC3 player_get_view_pos( PLAYER* p ) { + return VEC3( p->pos.x, p->pos.y, p->pos.z + p->eyeoffset ); +} diff --git a/src/game/player.h b/src/game/player.h new file mode 100644 index 0000000..71b1c02 --- /dev/null +++ b/src/game/player.h @@ -0,0 +1,22 @@ +#pragma once +#include "object.h" + +struct PLAYER_INPUT { + +}; + +struct PLAYER : OBJECT { + static const U32 CLASSID = OBJCLASS_PLAYER; + U32 health; + F32 fov{72.f}; + F32 eyeoffset{40.f}; + + VEC3 mins; + VEC3 maxs; + + VEC3 velocity; +}; + +extern PLAYER* player_create( VEC3 origin, F32 yaw ); +extern void player_input( struct GAME_DATA* game, PLAYER* player ); +extern VEC3 player_get_view_pos( PLAYER* player ); diff --git a/src/game/raycast.cpp b/src/game/raycast.cpp new file mode 100644 index 0000000..c82a3ed --- /dev/null +++ b/src/game/raycast.cpp @@ -0,0 +1,162 @@ +#include <cmath> +#include <math.h> + +#include "raycast.h" +#include "../game.h" + +#include "../render/gl_2d.h" + +#include "objlist.h" +#include "player.h" +#include "world/world.h" +#include "world/map.h" + +const U32 RAY_DEPTH_MAX = 8; + +U8 line_intersects( VEC3 ray_start, VEC2 ray_dir, MAP_WALL* s, VEC3* p, F32* t ) { + VEC2 v1 = VEC2{ ray_start.x, ray_start.y } - VEC2{ s->start.x, s->start.y }; + VEC2 v2 = VEC2{ s->end.x, s->end.y } - VEC2{ s->start.x, s->start.y }; + VEC2 v3 = { -ray_dir.y, ray_dir.x }; + + F32 dot = vec_dot( v2, v3 ); + if( fabsf(dot) < 0.001f ) + return 0; + + F32 t1 = vec_cross( v2, v1 ) / dot; + F32 t2 = vec_dot( v1, v3 ) / dot; + + if( t1 >= 0.0f && ( t2 >= 0.0f && t2 <= 1.0f ) ) { + *t = t1; + *p = ray_start + VEC3{ ray_dir } * (*t); + return 1; + } + + return 0; +} + +F32 ray_calc_wall_hit_dir( VEC3 start, VEC3 end, F32 ang ) { + VEC3 walld = start - end; + vec_normalize( &walld ); + F32 wallang = atan2f( walld.y, walld.x ); + wallang += ang; + if( wallang < 0.f ) wallang += 2.f * M_PI; + if( wallang > 2.f * M_PI ) wallang -= 2.f * M_PI; + return wallang; +} + +LIST<RAY_HITDATA> ray_trace_list( VEC3 start, F32 ang, U32 max_iter ) { + WORLD* w = objl->world; + if( !w ) return {}; + LIST<RAY_HITDATA> hits; + + F32 rayang = m_deg2rad( ang ); + VEC2 raydir = { cos( rayang ), sin( rayang ) }; + VEC3 minp; + + for( U32 i = 0; i < w->map->walls.size; ++i ) { + VEC3 p; + F32 dist; + if( line_intersects( start, raydir, &w->map->walls[i], &p, &dist ) ) { + dist = vec_dist( start, p ); + + VEC3 start = w->map->walls[i].start; + VEC3 end = w->map->walls[i].end; + F32 hitang = ray_calc_wall_hit_dir( start, end, ang ); + hits.push( RAY_HITDATA{ p, dist, hitang, TH_WALL, i } ); + if( hits.size > max_iter ) + break; + } + } + + return hits; +} + +U32 ray_trace( TRACE* tr, F32 ang ) { + F32 rayang = m_deg2rad( ang ); + VEC3 start = tr->startpos; + VEC3 raydir = { cos( rayang ), sin( rayang ), 0.f }; + raydir *= RAY_DEPTH_MAX; + + return ray_trace( tr, raydir ); +} + +U32 ray_trace( TRACE* tr, VEC3 end ) { + WORLD* w = objl->world; + if( !w ) return TRACE_HIT_NONE; + WORLD_MAP* map = w->map; + + VEC3 start = tr->startpos; + VEC3 diff = end - start; + F32 trdist = vec_lensq( diff ); + vec_normalize( &diff ); + VEC2 raydir = { diff.x, diff.y }; + F32 ang = atan2f( raydir.y, raydir.x ); + + F32 mindist = INFINITY; + VEC3 minp; + U32 hitwall = TRACE_HIT_NONE; + + for( U32 i = 0; i < map->walls.size; ++i ) { + VEC3 p; + F32 dist; + + if( line_intersects( start, raydir, &map->walls[i], &p, &dist ) ) { + if( dist <= trdist && dist < mindist ) { + mindist = dist; + minp = p; + hitwall = i; + } + } + } + + if( hitwall != -1 ) { + tr->endpos = { minp.x, minp.y, 0.0f }; + tr->hitwall = hitwall; + + VEC3 start = map->walls[hitwall].start; + VEC3 end = map->walls[hitwall].end; + tr->hitang = ray_calc_wall_hit_dir( start, end, ang ); + return hitwall; + } + + tr->hitwall = -1; + tr->fract = -1.f; + tr->endpos = { start.x + RAY_DEPTH_MAX * raydir.x, start.y + RAY_DEPTH_MAX * raydir.y, 0.0f }; + return TRACE_HIT_NONE; +} + +void draw_player( GAME_DATA* game, PLAYER* pl ) { + GL_PROGRAM* gl2d = game->shaders.gl2d; + VEC2 xy = { pl->pos.x, pl->pos.y }; + + gl_2d_frect( gl2d, xy - VEC2{ 5.f, 5.f }, { 10.f, 10.f }, CLR::RED() ); +} + +void ray_draw_world2d( GAME_DATA* game, WORLD* world ) { + for( U32 i = 0; i < world->map->polygons.size; ++i ) { + MAP_POLYGON* p = &world->map->polygons[i]; + + LIST<VERTEX> verts{}; + SURF_PROPS* props = polygon_get_props( world->map, p ); + p->vertices.each( fn( MAP_VERTEX* mv ) { + VERTEX v{}; + v.pos = VEC2{ mv->pos.x, mv->pos.y }; + v.uv = mv->uv; + v.clr = props->clr; + verts.push( v ); + } ); + + if( props->tex ) + gl_textured_polygon( game->shaders.gl2d_texcoord, verts.data, verts.size, props->tex ); + else + gl_polygon( game->shaders.gl2d, verts.data, verts.size ); + } + + for( U32 i = 0; i < world->map->walls.size; ++i ) { + MAP_WALL* s = &world->map->walls[i]; + SURF_PROPS* p = &world->map->props[s->propid]; + gl_2d_line( game->shaders.gl2d, VEC2( s->start.x, s->start.y ), VEC2( s->end.x, s->end.y ), p->clr ); + } + + draw_player( game, objl->pl ); +} diff --git a/src/game/raycast.h b/src/game/raycast.h new file mode 100644 index 0000000..5920e09 --- /dev/null +++ b/src/game/raycast.h @@ -0,0 +1,65 @@ +#pragma once + +#include "../util.h" + +const static U32 RAY_ITER_MAX = 10; +const static U32 TRACE_HIT_NONE = (U32)-1; + +enum TraceHitType_t { + TH_NONE, + TH_WALL, + TH_CEILING, + TH_FLOOR +}; + +enum TraceFilter_t { + TF_NONE = 0x0, + TF_WALLS = 0x1, + TF_SPRITES = 0x2, + TF_ENTITIES = 0x4, + + TF_WORLD = TF_WALLS | TF_SPRITES, + TF_ALL = TF_WALLS | TF_SPRITES | TF_ENTITIES, +}; + +struct TRACE { + /* input */ + VEC3 startpos; + /* output*/ + VEC3 endpos; + /* output */ + F32 fract; + /* output */ + F32 hitang; + /* output */ + U32 hitwall; + /* input */ + U32 filter; +}; + +struct RAY_HITDATA { + VEC3 point; + F32 dist; + F32 ang; + + U8 type; + U32 obj_idx; +}; + +extern LIST<RAY_HITDATA> ray_trace_list( VEC3 start, F32 ang, U32 max_iter = RAY_ITER_MAX ); +extern LIST<RAY_HITDATA> ray_trace_list( VEC3 start, VEC3 end, U32 max_iter = RAY_ITER_MAX ); + +extern U32 ray_trace( TRACE* tr, F32 ang ); +extern U32 ray_trace( TRACE* tr, VEC3 end ); +extern U32 ray_trace( TRACE* tr, VEC3 start, F32 ang ); +extern U32 ray_trace( TRACE* tr, VEC3 start, VEC3 end ); + +extern VEC2 project_vertex( + VEC3 vertex_pos, + VEC3 player_pos, + F32 player_angle_deg, + F32 fov_deg, + VEC2 window, + VEC2 winsize, + U8* in_view = 0 +); diff --git a/src/game/vars.cpp b/src/game/vars.cpp new file mode 100644 index 0000000..d7b1f9b --- /dev/null +++ b/src/game/vars.cpp @@ -0,0 +1,115 @@ +#include "vars.h" +#include "../util.h" +#include <string.h> + +VAR_LIST* varl; + +VAR_LIST* vars_init() { + VAR_LIST* ret = new VAR_LIST; + return ret; +} + +CVAR* var_new( const char* name, F32 v ) { + if( !varl ) { + dlog( "var_new() : error: varl null\n" ); + return 0; + } + + CVAR* ret = new CVAR; + memset( ret, 0 , sizeof( CVAR ) ); + ret->type = CVAR_TYPE_FLOAT; + strcpy( ret->name, name ); + ret->fl_v = v; + + varl->vars.push( ret ); + return ret; +} + +CVAR* var_new( const char* name, I32 i ) { + if( !varl ) { + dlog( "var_new() : error: varl null\n" ); + return 0; + } + + CVAR* ret = new CVAR; + memset( ret, 0 , sizeof( CVAR ) ); + ret->type = CVAR_TYPE_INT; + strcpy( ret->name, name ); + ret->i_v = i; + + varl->vars.push( ret ); + return ret; +} + +CVAR* var_new( const char* name, CLR c ) { + if( !varl ) { + dlog( "var_new() : error: varl null\n" ); + return 0; + } + + CVAR* ret = new CVAR; + memset( ret, 0 , sizeof( CVAR ) ); + ret->type = CVAR_TYPE_COLOR; + strcpy( ret->name, name ); + ret->clr_v = c; + + varl->vars.push( ret ); + return ret; +} + +CVAR* var_new( const char* name, CVAR_FUNC func ) { + if( !varl ) { + dlog( "var_new() : error: varl null\n" ); + return 0; + } + + CVAR* ret = new CVAR; + memset( ret, 0 , sizeof( CVAR ) ); + ret->type = CVAR_TYPE_FN; + strcpy( ret->name, name ); + ret->fn_v = func; + + varl->vars.push( ret ); + return ret; +} + +CVAR* var_new( const char* name, const char* v ) { + if( !varl ) { + dlog( "var_new() : error: varl null\n" ); + return 0; + } + + CVAR* ret = new CVAR; + memset( ret, 0, sizeof( CVAR ) ); + ret->type = CVAR_TYPE_STRING; + strcpy( ret->name, name ); + + varl->vars.push( ret ); + return ret; +} + +CVAR* var_find( const char* name ) { + if( !varl ) { + dlog( "var_new() : error: varl null\n" ); + return 0; + } + + U32 len = strlen( name ); + for( U32 i = 0; i < varl->vars.size; ++i ) { + U32 len2 = strlen( varl->vars[i]->name ); + if( strncmp( varl->vars[i]->name, name, min( len, len2 ) ) == 0 ) { + return varl->vars[i]; + } + } + + return 0; +} + +I32 var_call( CVAR *cmdvar, const char *cmdline ) { + if( cmdvar->type != CVAR_TYPE_FN ) { + dlog( "var_call() : error: not a function\n" ); + return -1; + } + + return cmdvar->fn_v( cmdvar, cmdline ); +} diff --git a/src/game/vars.h b/src/game/vars.h new file mode 100644 index 0000000..395c470 --- /dev/null +++ b/src/game/vars.h @@ -0,0 +1,50 @@ +#pragma once +#include "../util.h" + +enum CvarTypes_t { + CVAR_TYPE_INT, + CVAR_TYPE_FLOAT, + CVAR_TYPE_COLOR, + CVAR_TYPE_STRING, + CVAR_TYPE_FN, + CVAR_TYPE_LAST +}; + +typedef I32(*CVAR_FUNC)( struct CVAR* self, const char* cmdline ); + +struct CVAR { + char name[100]; + U8 type; + union { + F32 fl_v; + I32 i_v; + CLR clr_v; + CVAR_FUNC fn_v; + char str_v[64]; + }; + + union { + F32 fl_v; + I32 i_v; + CLR clr_v; + CVAR_FUNC fn_v; + char str_v[64]; + } default_v; +}; + +struct VAR_LIST { + LIST<CVAR*> vars; +}; + +extern VAR_LIST* vars_init(); + +extern CVAR* var_new( const char* name, F32 v ); +extern CVAR* var_new( const char* name, I32 v ); +extern CVAR* var_new( const char* name, CLR v ); +extern CVAR* var_new( const char* name, const char* v ); +extern CVAR* var_new( const char* name, CVAR_FUNC fn ); + +extern CVAR* var_find( const char* name ); +extern I32 var_call( CVAR* cmdvar, const char* cmdline ); + +extern VAR_LIST* varl; diff --git a/src/game/world/bsp.cpp b/src/game/world/bsp.cpp new file mode 100644 index 0000000..9aa94d7 --- /dev/null +++ b/src/game/world/bsp.cpp @@ -0,0 +1,662 @@ +#include "bsp.h" +#include "../../util/math.h" +#include "map.h" +#include <climits> + +const F32 BSP_PORTAL_PLANE_SIZE = 64000.f; + +struct MAP_TRI { + MAP_VERTEX p0, p1, p2; +}; + +void bsp_face_gen_render_verts( BSP* bsp, BSP_FACE* f ) { + LIST<VERTEX3D> vertices; + vertices.reserve( f->verts.size ); + + SURF_PROPS* props = map_get_props( bsp->map, f->propid ); + + f->verts.each( fn( MAP_VERTEX* v ) { + VERTEX3D v3d; + v3d.pos = { v->pos.x, v->pos.z, v->pos.y }; + v3d.uv = v->uv; + v3d.clr = v->clr * props->clr; + v3d.sampler = 0; + vertices.push( v3d ); + } ); + + f->render_verts = triangle_fan_to_list( vertices.data, vertices.size ); +} + +void bsp_gen_render_vertices( BSP* bsp ) { + bsp->faces.each( fn( BSP_FACE* f ) { + bsp_face_gen_render_verts( bsp, f ); + } ); +} + +LIST<BSP_PLANE> bsp_map_get_wall_planes( WORLD_MAP* map ) { + LIST<BSP_PLANE> ret{}; + + auto calc_wall_plane = fn( MAP_WALL* w ) { + VEC3 dir = w->end - w->start; + VEC3 normal = vec_normalize( { -dir.y, dir.x, 0.0f } ); + F32 dist = vec_dot( normal, w->start ); + + BSP_PLANE p = { normal, dist }; + for( U32 i = 0; i < ret.size; ++i ) { + if( ret[i] == p ) + return; + } + + ret.push( p ); + }; + + map->walls.each( calc_wall_plane ); + for( U32 i = 0; i < 4; ++i ) + calc_wall_plane( &map->skybox.walls[i] ); + + return ret; +} + + +LIST<MAP_TRI> bsp_map_triangulate_planes( WORLD_MAP* map ) { + LIST<MAP_TRI> ret; + + auto triangulate_plane = fn( MAP_POLYGON* p ) { + MAP_VERTEX* start = &p->vertices[0]; + MAP_VERTEX* last = &p->vertices[1]; + for( U32 i = 2; i < p->vertices.size; ++i ) { + MAP_TRI tri = MAP_TRI{ + *start, + *last, + p->vertices[i] + }; + + last = &p->vertices[i]; + ret.push( tri ); + } + }; + + map->polygons.each( triangulate_plane ); + triangulate_plane( &map->skybox.polygons[0] ); + triangulate_plane( &map->skybox.polygons[1] ); + + return ret; +} + +LIST<BSP_PLANE> bsp_map_get_tri_planes( WORLD_MAP* map ) { + LIST<BSP_PLANE> ret; + LIST<MAP_TRI> tris = bsp_map_triangulate_planes( map ); + + tris.each( fn( MAP_TRI* tri ) { + VEC3 cross = vec_cross( tri->p1.pos - tri->p0.pos, tri->p2.pos - tri->p0.pos ); + VEC3 normal = vec_normalize( cross ); + F32 dist = vec_dot( normal, tri->p0.pos ); + + BSP_PLANE p = { normal, dist }; + for( U32 i = 0; i < ret.size; ++i ) { + if( ret[i] == p ) + return; + } + + ret.push( p ); + } ); + + return ret; +} + +LIST<BSP_PLANE> bsp_map_get_planes( WORLD_MAP* map ) { + LIST<BSP_PLANE> ret; + + LIST<BSP_PLANE> walls = bsp_map_get_wall_planes( map ); + LIST<BSP_PLANE> floors = bsp_map_get_tri_planes( map ); + + ret.push_list( walls ); + ret.push_list( floors ); + + // probably unnecessary, slow as fuck + // remove any dupes in case a polygon ends up the same as a wall + // unlikely but dont wanna run into the edge case + // this wont run that much anyway + // maybe remove later + for( U32 i = 0; i < ret.size; ++i ) { + BSP_PLANE* p = &ret[i]; + for( U32 i2 = 0; i2 < ret.size; ++i2 ) { + if( i == i2 ) + continue; + + if( ret.data[i2] == *p ) { + ret.erase( i ); + --i; + } + } + } + + return ret; +} + + +LIST<BSP_FACE> bsp_map_faces_from_walls( WORLD_MAP* map ) { + LIST<BSP_FACE> ret{}; + + auto triangulate_wall = fn( MAP_WALL* w ) { + SURF_PROPS* p = wall_get_props( map, w ); + VEC3 points[] = { + { w->start.x, w->start.y, w->start.z }, + { w->end.x, w->end.y, w->start.z }, + { w->end.x, w->end.y, w->start.z + w->end.z }, + { w->start.x, w->start.y, w->start.z + w->end.z }, + }; + + VEC2 uvs[] = { + { w->uvstart.x, 1.f - w->uvend.y }, + { 1.f - w->uvend.x, 1.f - w->uvend.y }, + { 1.f - w->uvend.x, w->uvstart.y }, + { w->uvstart.x, w->uvstart.y }, + }; + + MAP_VERTEX vertices[4]; + for( U32 i = 0; i < 4; ++i ) { + vertices[i] = { + .pos = points[i], + .uv = uvs[i], + .clr = p->clr, + }; + } + + BSP_FACE f1{ .propid = w->propid }; + f1.verts.push( vertices[2] ); + f1.verts.push( vertices[1] ); + f1.verts.push( vertices[0] ); + + BSP_FACE f2{ .propid = w->propid }; + f2.verts.push( vertices[3] ); + f2.verts.push( vertices[2] ); + f2.verts.push( vertices[0] ); + + ret.push( f1 ); + ret.push( f2 ); + }; + + map->walls.each( triangulate_wall ); + for( U32 i = 0; i < 4; ++i ) triangulate_wall( &map->skybox.walls[i] ); + + return ret; +} + +LIST<BSP_FACE> bsp_map_faces_from_planes( WORLD_MAP* map ) { + LIST<BSP_FACE> ret; + + auto triangulate_plane = fn( MAP_POLYGON* p ) { + MAP_VERTEX* start = &p->vertices[0]; + MAP_VERTEX* last = &p->vertices[1]; + for( U32 i = 2; i < p->vertices.size; ++i ) { + BSP_FACE f = { .propid = p->propid }; + f.verts.push( *start ); + f.verts.push( *last ); + f.verts.push( p->vertices[i] ); + + ret.push( f ); + last = &p->vertices[i]; + } + }; + + map->polygons.each( triangulate_plane ); + triangulate_plane( &map->skybox.polygons[0] ); + triangulate_plane( &map->skybox.polygons[1] ); + + return ret; +} + +LIST<BSP_FACE> bsp_map_get_faces( WORLD_MAP* map ) { + LIST<BSP_FACE> ret; + + LIST<BSP_FACE> walls = bsp_map_faces_from_walls( map ); + LIST<BSP_FACE> floors = bsp_map_faces_from_planes( map ); + + ret.emplace_list( walls ); + ret.emplace_list( floors ); + + return ret; +} + +inline I32 point_classify( const BSP_PLANE& plane, const VEC3& point ) { + F32 side = bsp_plane_dist( plane, point ); + if( side > BSP_NORM_EPSILON ) + return BSP_SIDE_FRONT; + else if( side < -BSP_NORM_EPSILON ) + return BSP_SIDE_BACK; + else + return BSP_SIDE_ON; +} + +inline I32 polygon_classify( + const BSP_PLANE& plane, + const BSP_FACE* face, + I32* out_fr = 0, + I32* out_bk = 0 +) { + I32 cf = 0, cb = 0; + for( U32 i = 0; i < face->verts.size; ++i ) { + I32 side = point_classify( plane, face->verts.data[i].pos ); + if( side & BSP_SIDE_FRONT ) + ++cf; + else if( side & BSP_SIDE_BACK ) + ++cb; + } + + if( out_fr ) + *out_fr = cf; + if( out_bk ) + *out_bk = cb; + + if( cf && cb ) return BSP_SIDE_SPAN; + if( cf ) return BSP_SIDE_FRONT; + if( cb ) return BSP_SIDE_BACK; + return BSP_SIDE_ON; +} + +STAT polygon_split( const BSP_PLANE& plane, const BSP_FACE* in, BSP_FACE* out_fr, BSP_FACE* out_bk ) { + LIST<MAP_VERTEX> front, back; + STAT stat = STAT_ERR; + + for( U32 i = 0; i < in->verts.size; ++i ) { + U32 i2 = (i+1) % in->verts.size; + MAP_VERTEX* a = &in->verts.data[i]; + MAP_VERTEX* c = &in->verts.data[i2]; + + F32 da = bsp_plane_dist( plane, a->pos ); + F32 dc = bsp_plane_dist( plane, c->pos ); + + U8 afront = da > BSP_NORM_EPSILON; + U8 aback = da < -BSP_NORM_EPSILON; + U8 cfront = dc > BSP_NORM_EPSILON; + U8 cback = dc < -BSP_NORM_EPSILON; + + if( !aback ) front.push( *a ); + if( !afront ) back.push( *a ); + + if( (afront && cback) || (aback && cfront) ) { + F32 t = m_clamp( da / (da - dc), 0.f, 1.f ); + VEC3 isec = a->pos + (c->pos - a->pos) * t; + VEC2 uv = a->uv + (c->uv - a->uv) * t; + CLR clr = a->clr + (c->clr - a->clr) * t; + MAP_VERTEX v = { .pos = isec, .uv = uv, .clr = clr }; + front.push( v ); + back.push( v ); + } + } + + if( front.size > 2 ) { + out_fr->propid = in->propid; + out_fr->verts = front; + stat = STAT_OK; + } + if( back.size > 2 ) { + out_bk->propid = in->propid; + out_bk->verts = back; + stat = STAT_OK; + } + + return stat; +} + +I32 bsp_splitter_cost( const BSP_PLANE& plane, const LIST<BSP_FACE>& faces ) { + I32 splits = 0, front = 0, back = 0; + for( U32 i = 0; i < faces.size; ++i ) { + I32 side = polygon_classify( plane, &faces.data[i], 0, 0 ); + if( side == BSP_SIDE_SPAN ) ++splits; + else if( side == BSP_SIDE_FRONT ) ++front; + else if( side == BSP_SIDE_BACK ) ++back; + else { ++front; ++back; } + } + + I32 diff = ( front > back ) ? front - back : back - front; + return splits * 8 + diff; +} + +I32 bsp_calc_splitter_index( const LIST<BSP_PLANE>& planes, const LIST<BSP_FACE>& faces ) { + I32 best = -1; + I32 bestc = INT_MAX; + + for( U32 i = 0; i < planes.size; ++i ) { + I32 cost = bsp_splitter_cost( planes.data[i], faces ); + if( cost < bestc ) { + best = i; + bestc = cost; + } + } + + return best; +} + +U8 bsp_is_convex_cluster( const LIST<BSP_FACE>& faces ) { + for( U32 i = 0; i < faces.size; ++i ) { + VEC3 e1 = faces.data[i].verts[1].pos - faces.data[i].verts[0].pos; + VEC3 e2 = faces.data[i].verts[2].pos - faces.data[i].verts[0].pos; + VEC3 norm = vec_normalize( vec_cross( e1, e2 ) ); + F32 dist = vec_dot( norm, faces.data[i].verts[0].pos ); + BSP_PLANE plane{ norm, dist }; + for( U32 i2 = 0; i2 < faces.size; ++i2 ) { + if( i == i2 ) + continue; + + if( polygon_classify( plane, &faces.data[i2], 0, 0 ) == BSP_SIDE_SPAN ) + return 0; + } + } + + return 1; +} + +I32 bsp_insert_leaf( BSP* bsp, LIST<BSP_FACE>& faces ) { + BSP_LEAF leaf{}; + leaf.first = bsp->faces.size; + leaf.count = faces.size; + leaf.cluster = -1; // todo: pvs + bsp->faces.push_list( faces ); + bsp->leaves.push( leaf ); + return ~( (I32)bsp->leaves.size - 1 ); +} + +I32 bsp_build_nodes( + BSP* bsp, + LIST<BSP_PLANE>& planes, + LIST<BSP_FACE>& faces, + I32 depth ) { + if( faces.size == 0 ) + return bsp_insert_leaf( bsp, faces ); + if( depth > BSP_DEPTH_MAX || bsp_is_convex_cluster( faces ) ) + return bsp_insert_leaf( bsp, faces ); + + I32 splitter_idx = bsp_calc_splitter_index( planes, faces ); + if( splitter_idx < 0 ) return bsp_insert_leaf( bsp, faces ); + + BSP_PLANE split = planes.data[splitter_idx]; + + LIST<BSP_FACE> front, back, coplane; + for( U32 i = 0; i < faces.size; ++i ) { + BSP_FACE* f = &faces.data[i]; + I32 frontc = 0, backc = 0; + I32 side = polygon_classify( split, f, &frontc, &backc ); + if( side == BSP_SIDE_FRONT ) + front.push( *f ); + else if( side == BSP_SIDE_BACK ) + back.push( *f ); + else if( side == BSP_SIDE_ON ) + coplane.push( *f ); + else { + BSP_FACE fr{}, bk{}; + if( OK( polygon_split( split, f, &fr, &bk ) ) ) { + if( fr.verts.size > 2 ) front.push( fr ); + if( bk.verts.size > 2 ) back.push( bk ); + } + } + } + + BSP_NODE node{}; + node.plane = split; + I32 nidx = bsp->nodes.size; + bsp->nodes.push( node ); + + LIST<BSP_PLANE> child_planes; + child_planes.reserve( planes.size > 0 ? planes.size - 1 : 0 ); + for( I32 i = 0; i < planes.size; ++i ) { + if( i != splitter_idx ) child_planes.push( planes.data[i] ); + } + + I32 front_child = bsp_build_nodes( bsp, child_planes, front, depth + 1 ); + if( coplane.size ) { + I32 coplane_leaf = bsp_insert_leaf( bsp, coplane ); + BSP_NODE glue{}; + glue.plane = split; + glue.front = front_child; + glue.back = coplane_leaf; + front_child = bsp->nodes.size; + bsp->nodes.push( glue ); + } + + I32 back_child = bsp_build_nodes( bsp, child_planes, back, depth + 1 ); + + bsp->nodes.data[nidx].front = front_child; + bsp->nodes.data[nidx].back = back_child; + return nidx; +} + +inline void bsp_plane_basis( const VEC3& norm, VEC3* u, VEC3* v ) { + VEC3 a = fabsf( norm.x ) > 0.9f ? VEC3{ 0, 1, 0 } : VEC3{ 1, 0, 0 }; + *u = vec_normalize( vec_cross( a, norm ) ); + *v = vec_normalize( vec_cross( norm, *v ) ); +} + +BSP_WINDING bsp_winding_create( BSP_PLANE* plane ) { + BSP_WINDING w{}; + VEC3 n = plane->normal, nu = n * -1.f; + VEC3 u, v; + bsp_plane_basis( n, &u, &v ); + + VEC3 c = n * plane->dist; + + F32 s = BSP_PORTAL_PLANE_SIZE; + w.points.reserve( 4 ); + w.points.push( c + ( u + v ) * s ); + w.points.push( c + ( nu + v ) * s ); + w.points.push( c + ( nu - v ) * s ); + w.points.push( c + ( u - v ) * s ); + + return w; +} + + +F32 signed_dist_point_to_plane( const BSP_PLANE& plane, const VEC3& point ) { + return vec_dot( plane.normal, point ) - plane.dist; +} + +U8 bsp_clip_winding( BSP_WINDING* w, const BSP_PLANE& plane, U8 keep_front ) { + if( w->points.size < 3 ) + return 0; + + LIST<VEC3> out; + out.reserve( w->points.size + 2 ); + + for( U32 i = 0; i < w->points.size; ++i ) { + VEC3* a = &w->points.data[i]; + VEC3* b = &w->points.data[(i + 1) % w->points.size]; + + F32 da = signed_dist_point_to_plane( plane, *a ); + F32 db = signed_dist_point_to_plane( plane, *b ); + + U8 ain = keep_front ? ( da >= -BSP_NORM_EPSILON ) : ( da <= BSP_NORM_EPSILON ); + U8 bin = keep_front ? ( db >= -BSP_NORM_EPSILON ) : ( db <= BSP_NORM_EPSILON ); + + if( ain ) + out.push( *a ); + + if( ain != bin ) { + F32 t = da / (da - db); + t = m_clamp( t, 0.f, 1.f ); + VEC3 p = *a + (*b - *a) * t; + out.push( p ); + } + } + + if( out.size < 3 ) { + w->points.clear(); + return 0; + } + + w->points = out; + return 1; +} + +U8 bsp_clip_winding_to_set( BSP_WINDING* w, LIST<BSP_PLANE>* planes ) { + for( U32 i = 0; i < planes->size; ++i ) { + if( !bsp_clip_winding( w, planes->data[i], 1 ) ) + return 0; + } + + return 1; +} + +U8 bsp_winding_intersect_plane( BSP_PLANE* plane, BSP_WINDING* wa, BSP_WINDING* wb, BSP_WINDING* out ) { + if( wa->points.size < 3 || wb->points.size < 3 ) + return 0; + + BSP_WINDING w = *wa; + + for( U32 i = 0; i < wb->points.size; ++i ) { + VEC3* b1 = &wb->points[i]; + VEC3* b2 = &wb->points[(i + 1) % wb->points.size]; + + VEC3 edge = *b2 - *b1; + + VEC3 inward_norm = vec_normalize( vec_cross( plane->normal, edge ) ); + F32 d = vec_dot( inward_norm, *b1 ); + + BSP_PLANE horip{ inward_norm, d }; + if( !bsp_clip_winding( &w, horip, 1 ) ) + return 0; + } + + *out = w; + return 1; +} + +void bsp_gather_volumes_for_node( + BSP* bsp, + I32 node_idx, + const LIST<BSP_PLANE>& parents, + U8 go_front, + LIST<BSP_PLANE>* out +) { + *out = parents; + BSP_NODE* n = &bsp->nodes.data[node_idx]; + BSP_PLANE newp = n->plane; + if( !go_front ) { + newp.normal = newp.normal * -1; + newp.dist = -newp.dist; + } + + out->push( newp ); +} + +void bsp_clip_portal_to_subtree( + BSP* bsp, + I32 idx, + BSP_WINDING w, + U8 in_front, + LIST<BSP_PORTAL_NODE>* nodes +) { + if( bsp_is_leaf( idx ) ) { + BSP_PORTAL_NODE newn{ .leaf = bsp_leaf_index( idx ), .w = w }; + nodes->push( newn ); + return; + } + + BSP_NODE* n = &bsp->nodes.data[idx]; + BSP_WINDING wf = w, wb = w; + + if( !bsp_clip_winding( in_front ? &wf : &wb, n->plane, in_front ) ) + return; + + if( in_front ) { + bsp_clip_portal_to_subtree( bsp, n->front, wf, 1, nodes ); + } else { + bsp_clip_portal_to_subtree( bsp, n->back, wb, 0, nodes ); + } +} + +void bsp_build_portals( BSP* bsp ) { + if( bsp->root < 0 ) + return; + + LIST<BSP_PORTAL_STACK_FRAME> stack; + stack.push( BSP_PORTAL_STACK_FRAME{ bsp->root, LIST<BSP_PLANE>{} } ); + + // todo: multithread this + while( stack.size ) { + BSP_PORTAL_STACK_FRAME frame = stack.data[stack.size - 1]; + stack.pop(); + + if( bsp_is_leaf( frame.idx ) ) + continue; + + BSP_NODE* n = &bsp->nodes.data[frame.idx]; + + BSP_WINDING wind = bsp_winding_create( &n->plane ); + BSP_WINDING bounded = wind; + if( bsp_clip_winding_to_set( &bounded, &frame.planes ) ) { + LIST<BSP_PORTAL_NODE> fr_nodes, bk_nodes; + + bsp_clip_portal_to_subtree( bsp, n->front, bounded, 1, &fr_nodes ); + bsp_clip_portal_to_subtree( bsp, n->back, bounded, 1, &bk_nodes ); + + for( U32 ifr = 0; ifr < fr_nodes.size; ++ifr ) { + for( U32 ibk = 0; ibk < bk_nodes.size; ++ibk ) { + BSP_WINDING intersect; + if( bsp_winding_intersect_plane( &n->plane, &fr_nodes.data[ifr].w, &bk_nodes.data[ibk].w, &intersect ) ) { + if( intersect.points.size > 3 ) { + BSP_PORTAL p; + p.w = intersect; + p.plane = n->plane; + p.front = bsp_leaf_index( fr_nodes.data[ifr].leaf ); + p.back = bsp_leaf_index( bk_nodes.data[ibk].leaf ); + bsp->portals.push( p ); + } + } + } + } + } + + LIST<BSP_PLANE> fr, bk; + bsp_gather_volumes_for_node( bsp, frame.idx, frame.planes, 1, &fr ); + bsp_gather_volumes_for_node( bsp, frame.idx, frame.planes, 0, &bk ); + + BSP_NODE* n1 = &bsp->nodes.data[frame.idx]; + if( !bsp_is_leaf( n1->front ) ) stack.push( BSP_PORTAL_STACK_FRAME{ n1->front, fr } ); + if( !bsp_is_leaf( n1->back ) ) stack.push( BSP_PORTAL_STACK_FRAME{ n1->back, bk } ); + } + + for( U32 i = 0; i < bsp->leaves.size; ++i ) { + bsp->leaves.data[i].cluster = (I32)i; + } +} + +void pvs_set( BSP_BITSET* pvs, U32 count, U32 cluster, U32 seen ) { + U32 wp = (count + 31) / 32; + U32 base = cluster * wp; + + U32 w = seen / 32, b = seen % 32; + while( base + w >= pvs->size ) pvs->push( 0 ); + pvs->data[base + w] |= (1u << b); +} + +U8 pvs_get( BSP_BITSET* pvs, U32 count, U32 cluster, U32 seen ) { + U32 wp = (count + 31) / 32; + U32 base = cluster * wp; + + U32 w = seen / 32, b = seen % 32; + if( base + w >= pvs->size ) return 0; + + return ( pvs->data[base + w] >> b) & 1u; +} + +BSP* bsp_build_map( WORLD_MAP* m ) { + BSP* bsp = new BSP; + bsp->map = m; + LIST<BSP_FACE> faces = bsp_map_get_faces( m ); + LIST<BSP_PLANE> planes = bsp_map_get_planes( m ); + + bsp->root = bsp_build_nodes( bsp, planes, faces, 0 ); + U32 vertc = 0; + m->polygons.each( fn( MAP_POLYGON* p ) { vertc += p->vertices.size; } ); + for( U32 i = 0; i < bsp->faces.size; ++i ) { + bsp->faces.data[i].id = i; + } + bsp_gen_render_vertices( bsp ); + return bsp; +} + +void bsp_free( BSP* bsp ) { + delete bsp; +} + diff --git a/src/game/world/bsp.h b/src/game/world/bsp.h new file mode 100644 index 0000000..439fce1 --- /dev/null +++ b/src/game/world/bsp.h @@ -0,0 +1,103 @@ +#pragma once + +#include "map.h" +#include "../../render/gl_3d.h" + +const F32 BSP_NORM_EPSILON = 0.0001f; +const F32 BSP_DIST_EPSILON = 0.01f; +const I32 BSP_DEPTH_MAX = 1024; +struct BSP_PLANE { + VEC3 normal; + F32 dist; + + const bool operator==( const BSP_PLANE& other ) const { + return fabsf( normal.x - other.normal.x ) < BSP_NORM_EPSILON && + fabsf( normal.y - other.normal.y ) < BSP_NORM_EPSILON && + fabsf( normal.z - other.normal.z ) < BSP_NORM_EPSILON && + fabsf( dist - other.dist ) < BSP_DIST_EPSILON; + } +}; + +enum BspSide_t { + BSP_SIDE_FRONT = 1, + BSP_SIDE_BACK = 2, + BSP_SIDE_ON = 4, + BSP_SIDE_SPAN = 8, +}; + +struct BSP_FACE { + I32 propid; + I32 id; + LIST<MAP_VERTEX> verts{}; + LIST<VERTEX3D> render_verts{}; +}; + +struct BSP_NODE { + BSP_PLANE plane; + I32 front; + I32 back; +}; + +struct BSP_LEAF { + U32 first; + U32 count; + I32 cluster; +}; + +struct BSP_WINDING { + LIST<VEC3> points; +}; + +struct BSP_PORTAL { + BSP_WINDING w; // winding + BSP_PLANE plane; + I32 front; + I32 back; +}; + +struct BSP_CLUSTER { + I32 first; + I32 count; + I32 pvs_off; +}; + +struct BSP_PORTAL_STACK_FRAME { + I32 idx; + LIST<BSP_PLANE> planes; +}; + +struct BSP_PORTAL_NODE { + I32 leaf; + BSP_WINDING w; +}; + +struct BSP_BITSET : public LIST<U32> {}; + +struct BSP { + LIST<BSP_NODE> nodes; + LIST<BSP_LEAF> leaves; + LIST<BSP_FACE> faces; + + // >= 0 = node, < 0 = leaf + I32 root; + WORLD_MAP* map; + + LIST<BSP_PORTAL> portals; + LIST<BSP_CLUSTER> clusters; + BSP_BITSET pvs_bits; +}; + +extern LIST<BSP_PLANE> bsp_map_get_planes( WORLD_MAP* map ); +extern LIST<BSP_FACE> bsp_map_get_faces( WORLD_MAP* map ); +extern BSP* bsp_build_map( WORLD_MAP* map ); +extern void bsp_free( BSP* bsp ); + + +inline U8 bsp_is_leaf( I32 child ) { return child < 0; } +inline I32 bsp_leaf_index( I32 child ) { return ~child; } +inline F32 bsp_plane_dist( const BSP_PLANE& p, const VEC3& v ) { return vec_dot( p.normal, v ) - p.dist; } +inline SURF_PROPS* bsp_face_get_props( WORLD_MAP* w, BSP_FACE* s ) { + return s->propid < 0 ? + map_props_get_special( w, s->propid ) : + &w->props[s->propid]; +} diff --git a/src/game/world/bsp_draw.cpp b/src/game/world/bsp_draw.cpp new file mode 100644 index 0000000..30e6731 --- /dev/null +++ b/src/game/world/bsp_draw.cpp @@ -0,0 +1,124 @@ +#include "bsp_draw.h" +#include "../../game.h" +#include "../../render/gl_3d.h" +#include "../objlist.h" +#include "map.h" +#include "trace.h" + + +void bsp_draw_leaf( BSP* bsp, GAME_DATA* game, I32 leafidx ) { + BSP_LEAF* leaf = &bsp->leaves.data[leafidx]; + // todo : handle transparency + + for( U32 i = 0; i < leaf->count; ++i ) { + BSP_FACE* f = &bsp->faces.data[leaf->first + i]; + U8 face_transparent = 0; + if( !face_transparent ) { + LIST<VERTEX3D> verts = f->render_verts; + SURF_PROPS* props = bsp_face_get_props( game->state.map, f ); + gl_3d_polygon( game->render.batch_3d, verts.data, verts.size, props->tex ); + } + } + + // todo : sort by depth + for( U32 i = 0; i < leaf->count; ++i ) { + BSP_FACE* f = &bsp->faces.data[leaf->first + i]; + U8 face_transparent = 0; + if( face_transparent ) { + LIST<VERTEX3D> verts = f->render_verts; + SURF_PROPS* props = bsp_face_get_props( game->state.map, f ); + gl_3d_polygon( game->render.batch_3d, verts.data, verts.size, props->tex ); + } + } +} + +U8 leaf_visible( I32 leaf_idx ) { + // todo : pvs + return 1; +} + +I32 bsp_point_leaf( BSP* bsp, const VEC3& p ) { + I32 n = bsp->root; + for( ;; ) { + if( bsp_is_leaf( n ) ) return bsp_leaf_index( n ); + BSP_NODE* node = &bsp->nodes.data[n]; + F32 dist = bsp_plane_dist( node->plane, p ); + n = (dist >= 0.f)? node->front : node->back; + } +} + +void bsp_traverse_draw( BSP* bsp, GAME_DATA* game, I32 node, VEC3 campos ) { + if( bsp_is_leaf( node ) ) { + I32 i = bsp_leaf_index( node ); + return bsp_draw_leaf( bsp, game, i ); + } + + BSP_NODE* n = &bsp->nodes.data[node]; + F32 dist = bsp_plane_dist( n->plane, campos ); + + I32 near = (dist >= 0.f)? n->front : n->back; + I32 far = (dist >= 0.f)? n->back : n->front; + + // todo : set up vertex buffer, draw all in one call + // todo todo : batch different shader calls separately + + bsp_traverse_draw( bsp, game, near, campos ); + bsp_traverse_draw( bsp, game, far, campos ); +} + +void bsp_draw_sprites( BSP* bsp, GAME_DATA* game ) { + LIST<MAP_SPRITE>* sprites = &game->state.map->sprites; + for( U32 i = 0; i < sprites->size; ++i ) { + MAP_SPRITE* sprite = &sprites->data[i]; + BSP_TRACE t; + t.in_start = player_get_view_pos( objl->pl ); + t.in_end = sprite->pos; + + bsp_trace( &t, bsp ); + if( !t.hit ) { + VEC3 angle = vec_angles( sprite->pos, t.in_start ); + gl_3d_plane( + game->shaders.gl3d, + sprite->pos, + sprite->size, + { -angle.x + 90.f, angle.y - 90.f }, + sprite->tex, + sprite->clr + ); + } + } +} + +void bsp_draw( BSP* bsp, GAME_DATA* game, VEC2 window, VEC2 winsize ) { + gl_set_viewport( game->gl, window, winsize ); + PLAYER* pl = objl->pl; + F32 fov = pl->fov; + gl_3d_projection_setup( + game->shaders.gl3d, + { pl->pos.x, pl->pos.y, pl->pos.z + pl->eyeoffset }, + fov, + pl->rot.y, + pl->rot.x, + 1.f, + 9999.f, + winsize + ); + + gl_set_clip( game->gl, window, winsize ); + gl_batch_empty( game->render.batch_3d ); + glEnable( GL_DEPTH_TEST ); + glEnable( GL_CULL_FACE ); + // ??? + glFrontFace( GL_CW ); + glDepthFunc( GL_LESS ); + bsp_traverse_draw( bsp, game, bsp->root, objl->pl->pos ); + gl_batch_draw( game->render.batch_3d ); + + glDisable( GL_DEPTH_TEST ); + glDisable( GL_CULL_FACE ); + bsp_draw_sprites( bsp, game ); + gl_reset_clip( game->gl ); + + VEC2 canvas = { (F32)game->gl->canvas_size[0], (F32)game->gl->canvas_size[1] }; + gl_set_viewport( game->gl, {}, canvas ); +} diff --git a/src/game/world/bsp_draw.h b/src/game/world/bsp_draw.h new file mode 100644 index 0000000..048fa61 --- /dev/null +++ b/src/game/world/bsp_draw.h @@ -0,0 +1,4 @@ +#include "bsp.h" + +extern void bsp_draw_leaf( BSP* bsp, I32 leafidx ); +extern void bsp_draw( BSP* bsp, GAME_DATA* game, VEC2 window, VEC2 winsize ); diff --git a/src/game/world/draw.cpp b/src/game/world/draw.cpp new file mode 100644 index 0000000..9269a21 --- /dev/null +++ b/src/game/world/draw.cpp @@ -0,0 +1,216 @@ +#include "draw.h" +#include "map.h" + +#include "../../game.h" +#include "../../render/gl_3d.h" + +#include "../objlist.h" +#include "../raycast.h" + +VEC2 world_project_pos( + GAME_DATA* game, + VEC3 pos +) { + GL_3D* gl3d = (GL_3D*)game->shaders.gl3d; + + VEC4 clip = { pos.x, pos.z, pos.y, 1.f }; + VEC4 ndc; + + mat4_mul_vec4( &gl3d->projmat, &clip, &ndc ); + + if( ndc.w <= 0 ) + return { -INFINITY, -INFINITY }; + + ndc.x /= ndc.w; + ndc.y /= ndc.w; + + VEC2 screen = { + ( ndc.x * 0.5f + 0.5f ) * gl3d->winsize.x, + ( -( ndc.y * 0.5f ) + 0.5f ) * gl3d->winsize.y + }; + + return screen; +} + +void world_draw_wall( + GAME_DATA* game, + WORLD* world, + VERTEX3D* verts, + MAP_WALL* s +) { + SURF_PROPS* p = wall_get_props( world->map, s ); + gl_3d_polygon( game->shaders.gl3d, verts, 4, p->tex ); +} + +void world_draw_walls( GAME_DATA* game, WORLD* world ) { + world->map->walls.each( fn( MAP_WALL* s ) { + SURF_PROPS* props = wall_get_props( world->map, s ); + VEC3 start = s->start; + VEC3 end = s->end; + + VERTEX3D verts[4]{}; + verts[0].pos = { start.x, start.z, start.y }; + verts[1].pos = { start.x, start.z + end.z, start.y }; + verts[2].pos = { end.x, start.z + end.z, end.y }; + verts[3].pos = { end.x, start.z, end.y }; + + verts[0].uv = { 0, 1 }; + verts[1].uv = { 0, 0 }; + verts[2].uv = { 1, 0 }; + verts[3].uv = { 1, 1 }; + + verts[0].clr = verts[1].clr = verts[2].clr = verts[3].clr = props->clr; + + world_draw_wall( game, world, verts, s ); + } ); +} + +void world_draw_polygon( + GAME_DATA* game, + WORLD* world, + MAP_POLYGON* p, + LIST<VERTEX3D>* verts +) { + SURF_PROPS* props = polygon_get_props( world->map, p ); + + gl_3d_polygon( + game->shaders.gl3d, + verts->data, + verts->size, + props->tex + ); +} + +void world_draw_polygons( GAME_DATA* game, WORLD* world ) { + for( U32 i = 0; i < world->map->polygons.size; ++i ) { + MAP_POLYGON* p = &world->map->polygons[i]; + SURF_PROPS* props = polygon_get_props( world->map, p ); + + LIST<VERTEX3D> proj_verts; + U8 visc = p->vertices.size; // todo: visc + p->vertices.each( fn( MAP_VERTEX* mv ) { + VERTEX3D v; + v.pos = { mv->pos.x, mv->pos.z, mv->pos.y }; + v.uv = mv->uv; + v.clr = mv->clr * props->clr; + v.sampler = 0; + proj_verts.push( v ); + } ); + + if( proj_verts.size > 2 && visc > 0 ) { + world_draw_polygon( game, world, p, &proj_verts ); + } + } +} + +U8 sprite_is_occluded( + VEC3 player_pos, + MAP_SPRITE* sprite, + WORLD* world +) { + VEC3 ray_dir = sprite->pos - player_pos; + F32 dist_to_sprite = vec_len2d( ray_dir ); + + vec_normalize( &ray_dir ); + + TRACE trace; + trace.startpos = player_pos; + U32 hit = ray_trace( &trace, sprite->pos ); + + F32 dist = vec_len( trace.endpos - player_pos ); + + return hit != TRACE_HIT_NONE && dist < dist_to_sprite; +} + +void world_draw_sprite( + GAME_DATA* game, + MAP_SPRITE* sprite, + VEC3 player_pos, + WORLD* world + ) { + if( sprite_is_occluded( player_pos, sprite, world ) ) + return; + + VEC3 angle = vec_angles( sprite->pos, player_pos ); + gl_3d_plane( + game->shaders.gl3d, + sprite->pos, + sprite->size, + { -angle.x + 90.f, angle.y - 90.f }, + sprite->tex, + sprite->clr + ); +} + +void world_draw_sprites( + GAME_DATA* game, + WORLD* world +) { + VEC3 player_pos = player_get_view_pos( objl->pl ); + for( U32 i = 0; i < world->map->sprites.size; ++i ) { + MAP_SPRITE* sprite = &world->map->sprites[i]; + world_draw_sprite( game, sprite, player_pos, world ); + } +} + +void world_draw( GAME_DATA* game, WORLD* world, VEC2 window, VEC2 winsize ) { + PLAYER* pl = objl->pl; + VEC2 start = { pl->pos.x, pl->pos.y }; + + F32 fov = pl->fov; + + gl_set_viewport( game->gl, window, winsize ); + gl_3d_projection_setup( + game->shaders.gl3d, + { pl->pos.x, pl->pos.y, pl->pos.z + pl->eyeoffset }, + fov, + pl->rot.y, + pl->rot.x, + 1.f, + 9999.f, + winsize + ); + + gl_set_clip( game->gl, window, winsize ); + glEnable( GL_DEPTH_TEST ); + glDepthFunc( GL_LESS ); + world_draw_polygons( game, world ); + world_draw_walls( game, world ); + glDisable( GL_DEPTH_TEST ); + world_draw_sprites( game, world ); + gl_reset_clip( game->gl ); + + VEC2 canvas = { (F32)game->gl->canvas_size[0], (F32)game->gl->canvas_size[1] }; + gl_set_viewport( game->gl, {}, canvas ); +} + +VEC2 world_draw_project_point( + VEC3 vertex_pos, + VEC3 player_pos, + F32 player_angle_deg, + F32 fov_deg, + VEC2 window, + VEC2 winsize, + U8* in_view +) { + F32 rx = vertex_pos.x; + F32 ry = vertex_pos.y; + + F32 dist = sqrtf( rx*rx + ry*ry ); + if( dist < 0.001f ) + return { -99999.0f, -99999.0f }; + + F32 ang = atan2f( ry, rx ); + F32 half_fov_rad = m_deg2rad( fov_deg * 0.5f ); + if( in_view ) *in_view = (ang >= -half_fov_rad && ang <= half_fov_rad); + + F32 cosang = cosf( ang ); + F32 normalized_x = (ang + half_fov_rad) / (2.f * half_fov_rad); + F32 screen_x = window.x + normalized_x * winsize.x; + + F32 screen_y_center = window.y + winsize.y * 0.5f; + F32 vz = ( vertex_pos.z - player_pos.z ); + F32 screen_y = screen_y_center + (vz * winsize.y) / (dist * -cosang); + + return { screen_x, screen_y }; +} diff --git a/src/game/world/draw.h b/src/game/world/draw.h new file mode 100644 index 0000000..6297a5e --- /dev/null +++ b/src/game/world/draw.h @@ -0,0 +1,19 @@ +#pragma once + +#include "../../util/vector.h" + +extern VEC2 world_project_pos( struct GAME_DATA* game, VEC3 pos ); + +extern void world_draw_walls( struct GAME_DATA* game, struct WORLD* world ); +extern void world_draw_sprites( struct GAME_DATA* game, struct WORLD* world ); +extern void world_draw_polygons( struct GAME_DATA* game, struct WORLD* world ); +extern void world_draw( struct GAME_DATA* game, struct WORLD* world, VEC2 window, VEC2 winsize ); +extern VEC2 world_draw_project_point( + VEC3 vertex_pos, + VEC3 player_pos, + F32 player_angle_deg, + F32 fov_deg, + VEC2 window, + VEC2 winsize, + U8* in_view = 0 +); diff --git a/src/game/world/map.cpp b/src/game/world/map.cpp new file mode 100644 index 0000000..aba283d --- /dev/null +++ b/src/game/world/map.cpp @@ -0,0 +1,640 @@ +#include <cfloat> + +#include "map.h" +#include "../../game.h" +#include "../assets.h" +#include "bsp.h" + +const F32 SKYBOX_OFFSET = 24.f; + +MAP_TEXTURE_ENTRY* map_load_texture( WORLD_MAP* m, GAME_DATA* game, const char* name ) { + FNV1A hash = fnv1a( name ); + MAP_TEXTURE_ENTRY** pentry = m->textures.where( fn( MAP_TEXTURE_ENTRY** t ) { return (*t)->hash == hash; } ); + if( pentry ) { + return *pentry; + } + + GL_TEX2D* tex = gl_texture_from_file( game->gl, name ); + if( !tex ) { + dlog( "map_load_texture() : failed to load texture %s\n", name ); + return 0; + } + + MAP_TEXTURE_ENTRY* entry = new MAP_TEXTURE_ENTRY; + entry->tex = tex; + entry->hash = fnv1a( name ); + strcpy( entry->name, name ); + + m->textures.push( entry ); + return entry; +} + +STAT map_add_texture_ref( WORLD_MAP* map, GL_TEX2D *tex ) { + const char* name = assets_abspath( tex->name ); + FNV1A hash = fnv1a( name ); + + I32 idx = map->textures.idx_where( fn( MAP_TEXTURE_ENTRY** e ) { + return (*e)->hash == hash; + } ); + + if( idx != -1 ) + return STAT_ALREADYEXISTS; + + MAP_TEXTURE_ENTRY* entry = new MAP_TEXTURE_ENTRY; + entry->tex = tex; + entry->hash = hash; + strcpy( entry->name, name ); + + return STAT_OK; +} + +GL_TEX2D* map_find_texture( WORLD_MAP* map, const char* name ) { + FNV1A hash = fnv1a( name ); + MAP_TEXTURE_ENTRY** entry = map->textures.where( fn( MAP_TEXTURE_ENTRY** e ) { + return (*e)->hash == hash; + } ); + + if( entry ) + return (*entry)->tex; + + return 0; +} + +void map_polygon_calc_vertex_normals( MAP_POLYGON* p ) { + for( U32 i = 0; i < p->vertices.size; i += 3 ) { + MAP_VERTEX* v1 = &p->vertices[i]; + MAP_VERTEX* v2 = &p->vertices[(i + 1) % p->vertices.size]; + MAP_VERTEX* v3 = &p->vertices[(i + 2) % p->vertices.size]; + + VEC3 d1 = v2->pos - v1->pos; + VEC3 d2 = v3->pos - v1->pos; + VEC3 n = vec_cross( d1, d2 ); + vec_normalize( &n ); + + v1->normal = v2->normal = v3->normal = n; + } +} + +STAT map_polygon_verts_from_section( WORLD_MAP* m, MAP_POLYGON* p, CFG_SECTION* vertices, U32 vertc ) { + char vertsec[256] = { 0 }; + for( U32 i = 0; i < vertc; ++i ) { + MAP_VERTEX vert; + + sprintf( vertsec, "%d", i ); + CFG_SECTION* v = cfg_section( vertices, vertsec ); + if( !v ) { + dlog( "map_polygon_verts_from_section() : missing vertex %d in polygon with propid %d in map %s\n", i, p->propid, m->name ); + continue; + } + + CFG_VEC3* pos = cfg_vec3( v, "pos" ); + CFG_VEC2* uv = cfg_vec2( v, "uv" ); + CFG_CLR* clr = cfg_clr( v, "clr" ); + + if( !pos ) { + dlog( "map_polygon_verts_from_section() : missing pos for vertex %d in polygon with propid %d in map %s\n", i, p->propid, m->name ); + return STAT_ERR; + } + + vert.pos = pos->value; + if( uv ) vert.uv = uv->value; + if( clr ) vert.clr = clr->value; + + p->vertices.push( vert ); + } + + if( !p->vertices.size ) { + dlog( "map_polygon_verts_from_section() : invalid vertex count for polygon with propid %d in map %s\n", p->propid, m->name ); + return STAT_ERR; + } + + map_polygon_calc_vertex_normals( p ); + return STAT_OK; +} + +void map_polygon_calc_bounds( MAP_POLYGON* p ) { + p->vertices.each( fn( MAP_VERTEX* v ) { + p->mins.x = min( v->pos.x, p->mins.x ); + p->mins.y = min( v->pos.y, p->mins.y ); + p->mins.z = min( v->pos.z, p->mins.z ); + + p->maxs.x = max( v->pos.x, p->maxs.x ); + p->maxs.y = max( v->pos.y, p->maxs.y ); + p->maxs.z = max( v->pos.z, p->maxs.z ); + } ); +} + +STAT map_polygons_from_section( WORLD_MAP* m, GAME_DATA* g, CFG_SECTION* polygons, U32 polyc ) { + if( !polyc ) { + dlog( "map_polygons_from_section() : no polygons in %s\n", m->name ); + return STAT_OK; + } + + U32 parsec = 0; + char polysec[256] = { 0 }; + for( U32 i = 0; i < polyc; ++i ) { + MAP_POLYGON poly; + + sprintf( polysec, "%d", i ); + CFG_SECTION* p = cfg_section( polygons, polysec ); + if( !p ) { + dlog( "map_polygons_from_section() : missing polygon %d in %s\n", i, m->name ); + continue; + } + + CFG_INT* propid = cfg_int( p, "propid" ); + if( !propid ) { + dlog( "map_polygons_from_section() : missing propid for polygon %d in map %s\n", i, m->name ); + continue; + } + + poly.propid = propid->value; + CFG_INT* polytype = cfg_int( p, "type" ); + if( !polytype ) { + dlog( "map_polygons_from_section() : missing polygon type for polygon %d in %s\n", i, m->name ); + continue; + } + + poly.type = (U8)polytype->value; + + CFG_INT* vertc = cfg_int( p, "vertcount" ); + if( !vertc || !vertc->value ) { + dlog( "map_polygons_from_section() : missing vertex count for polygon %d in %s\n", i, m->name ); + continue; + } + + CFG_SECTION* vertices = cfg_section( p, "vertices" ); + if( !vertices ) { + dlog( "map_polygons_from_section() : missing vertices for polygon %d in %s", i, m->name ); + continue; + } + + if( !OK( map_polygon_verts_from_section( m, &poly, vertices, vertc->value ) ) ) + continue; + + map_polygon_calc_bounds( &poly ); + m->polygons.push( poly ); + ++parsec; + } + + return parsec > 0 ? STAT_OK : STAT_ERR; +} + +STAT map_walls_from_section( WORLD_MAP* m, GAME_DATA* g, CFG_SECTION* walls, U32 wallc ) { + if( !wallc ) { + dlog( "map_walls_from_section() : no walls in %s\n", m->name ); + return STAT_OK; + } + + U32 parsec = 0; + char wallsec[256] = { 0 }; + for( U32 i = 0; i < wallc; i++ ) { + MAP_WALL seg; + + sprintf( wallsec, "%d", i ); + CFG_SECTION* w = cfg_section( walls, wallsec ); + if( !w ) { + dlog( "map_walls_from_section() : missing wall %d in %s\n", i, m->name ); + continue; + } + + CFG_VEC3* start = cfg_vec3( w, "start" ); + CFG_VEC3* end = cfg_vec3( w, "end" ); + CFG_INT* prop = cfg_int( w, "propid" ); + + if( !start || !end || !prop ) { + dlog( "map_walls_from_section() : bad wall definition in %s idx %d [%p %p %p]\n", m->name, i, start, end, prop ); + continue; + } + + seg.start = start->value; + seg.end = end->value; + seg.propid = prop->value; + m->walls.push( seg ); + + ++parsec; + } + + return parsec > 0 ? STAT_OK : STAT_ERR; +} + + +STAT map_props_from_section( WORLD_MAP* m, GAME_DATA* g, CFG_SECTION* props, U32 propc ) { + if( !propc ) { + dlog( "map_props_from_section() : no props in %s\n", m->name ); + return STAT_ERR; + } + + U32 parsec = 0; + char propsec[256] = { 0 }; + for( U32 i = 0; i < propc; ++i ) { + SURF_PROPS prop; + + sprintf( propsec, "%d", i ); + CFG_SECTION* p = cfg_section( props, propsec ); + if( !p ) { + dlog( "map_props_from_section() : missing prop %d in %s\n", i, m->name ); + continue; + } + + CFG_CLR* clr = cfg_clr( p, "clr" ); + CFG_STR* tex = cfg_str( p, "tex" ); + + CLR propclr = (!!clr)? clr->value : CLR{ 1.f, 1.f, 1.f, 1.f }; + GL_TEX2D* proptex = 0; + if( tex ) { + MAP_TEXTURE_ENTRY* entry = map_load_texture( m, g, tex->str ); + if( !entry ) continue; + + proptex = entry->tex; + } + + prop.clr = propclr; + prop.tex = proptex; + m->props.push( prop ); + + ++parsec; + } + + return parsec > 0 ? STAT_OK : STAT_ERR; +} + +STAT map_sprites_from_section( WORLD_MAP* m, GAME_DATA* g, CFG_SECTION* sprites, U32 spritec ) { + if( !spritec ) { + dlog( "map_sprites_from_section() : no sprites in %s\n", m->name ); + return STAT_OK; + } + + U32 parsec = 0; + char spritesec[256] = { 0 }; + for( U32 i = 0; i < spritec; ++i ) { + MAP_SPRITE sprite; + + sprintf( spritesec, "%d", i ); + CFG_SECTION* s = cfg_section( sprites, spritesec ); + if( !s ) { + dlog( "map_sprites_from_section() : missing sprite %d in %s\n", i, m->name ); + continue; + } + + CFG_CLR* clr = cfg_clr( s, "clr" ); + CFG_STR* tex = cfg_str( s, "tex" ); + CFG_VEC3* pos = cfg_vec3( s, "pos" ); + CFG_VEC2* size = cfg_vec2( s, "size" ); + + if( !tex || !pos || !size ) { + dlog( "map_sprites_from_section() : invalid sprite %d in %s\n", i, m->name ); + continue; + } + + CLR spriteclr = (!!clr)? clr->value : CLR{ 1.f, 1.f, 1.f, 1.f }; + MAP_TEXTURE_ENTRY* texentry = map_load_texture( m, g, tex->str ); + GL_TEX2D* spritetex = texentry->tex; + + sprite.tex = spritetex; + sprite.pos = pos->value; + sprite.size = size->value; + sprite.clr = spriteclr; + m->sprites.push( sprite ); + + ++parsec; + } + + return parsec > 0 ? STAT_OK : STAT_ERR; +} + +STAT map_skybox_from_section( CFG_SECTION* s, GAME_DATA* g, WORLD_MAP* m ) { + CFG_SECTION* props = cfg_section( s, "props" ); + if( !props ) + return STAT_ERR; + + CFG_CLR* clr = cfg_clr( props, "clr" ); + CFG_STR* tex = cfg_str( props, "tex" ); + + CLR skyclr; + GL_TEX2D* skytex; + if( !tex ) { + skyclr = (!!clr)? clr->value : CLR::BLACK(); + skytex = 0; + } + else { + MAP_TEXTURE_ENTRY* texentry = map_load_texture( m, g, tex->str ); + skyclr = (!!clr)? clr->value : CLR::WHITE(); + if( !texentry || !texentry->tex ) { + dlog( "map_skybox_from_section() : could not load skybox texture '%s'", tex->str ); + return STAT_ERR; + } + else { + skytex = texentry->tex; + } + } + + + m->skybox.props = SURF_PROPS{ .tex = skytex, .clr = skyclr }; + return STAT_OK; +} + +MAP_SKYBOX map_default_skybox() { + return { + .props = { .tex = 0, .clr = CLR::BLACK() } + }; +} + +// generates 4 walls and 2 polygons to enclose the map. +void map_gen_skybox( WORLD_MAP* m ) { + VEC3 mins = m->mins - VEC3( SKYBOX_OFFSET, SKYBOX_OFFSET, SKYBOX_OFFSET ); + VEC3 maxs = m->maxs + VEC3( SKYBOX_OFFSET, SKYBOX_OFFSET, SKYBOX_OFFSET ); + + m->skybox.walls[0] = { + .start = { mins.x, mins.y, mins.z }, + .end = { maxs.x, mins.y, maxs.z - mins.z }, + .propid = MAPPROP_SKYBOX + }; + m->skybox.walls[1] = { + .start = { maxs.x, mins.y, mins.z }, + .end = { maxs.x, maxs.y, maxs.z - mins.z }, + .propid = MAPPROP_SKYBOX + }; + m->skybox.walls[2] = { + .start = { maxs.x, maxs.y, mins.z }, + .end = { mins.x, maxs.y, maxs.z - mins.z }, + .propid = MAPPROP_SKYBOX + }; + m->skybox.walls[3] = { + .start = { mins.x, maxs.y, mins.z }, + .end = { mins.x, mins.y, maxs.z - mins.z }, + .propid = MAPPROP_SKYBOX + }; + + VEC2 floor[] = { + { mins.x, mins.y }, + { maxs.x, mins.y }, + { maxs.x, maxs.y }, + { mins.x, maxs.y } + }; + VEC2 floor_uv[] = { + { 0, 0 }, + { 1, 0 }, + { 1, 1 }, + { 0, 1 } + }; + + m->skybox.polygons[0] = m->skybox.polygons[1] = { + .propid = MAPPROP_SKYBOX + }; + + for( U32 i = 0; i < 4; ++i ) { + m->skybox.polygons[0].vertices.push( { + .pos = { floor[i].x, floor[i].y, mins.z }, + .uv = floor_uv[i], // todo: fix + .clr = CLR::WHITE() + } ); + + m->skybox.polygons[1].vertices.push( { + .pos = { floor[i].x, floor[i].y, maxs.z }, + .uv = floor_uv[i], // todo: fix + .clr = CLR::WHITE() + } ); + } +} + +void map_calc_bounds( WORLD_MAP* m ) { + m->mins = { FLT_MAX, FLT_MAX, FLT_MAX }; + m->maxs = { -FLT_MAX, -FLT_MAX, -FLT_MAX }; + m->walls.each( fn( MAP_WALL* l ) { + m->mins.x = min( min( l->start.x, l->end.x ), m->mins.x ); + m->mins.y = min( min( l->start.y, l->end.y ), m->mins.y ); + m->mins.z = min( min( l->start.z, l->start.x + l->end.z ), m->mins.z ); + + m->maxs.x = max( max( l->start.x, l->end.x ), m->maxs.x ); + m->maxs.y = max( max( l->start.y, l->end.y ), m->maxs.y ); + m->maxs.z = max( max( l->start.z, l->start.x + l->end.z ), m->maxs.z ); + } ); + + m->polygons.each( fn( MAP_POLYGON* p ) { + m->mins.x = min( p->mins.x, m->mins.x ); + m->mins.y = min( p->mins.y, m->mins.y ); + m->mins.z = min( p->mins.z, m->mins.z ); + + m->maxs.x = max( p->maxs.x, m->maxs.x ); + m->maxs.y = max( p->maxs.y, m->maxs.y ); + m->maxs.z = max( p->maxs.z, m->maxs.z ); + } ); +} + +void map_check_bounds( WORLD_MAP* m ) { + VEC3 mins = { FLT_MAX, FLT_MAX, FLT_MAX }; + VEC3 maxs = { -FLT_MAX, -FLT_MAX, -FLT_MAX }; + m->walls.each( fn( MAP_WALL* l ) { + mins.x = min( min( l->start.x, l->end.x ), mins.x ); + mins.y = min( min( l->start.y, l->end.y ), mins.y ); + mins.z = min( min( l->start.z, l->start.x + l->end.z ), mins.z ); + + maxs.x = max( max( l->start.x, l->end.x ), maxs.x ); + maxs.y = max( max( l->start.y, l->end.y ), maxs.y ); + maxs.z = max( max( l->start.z, l->start.x + l->end.z ), maxs.z ); + } ); + + m->polygons.each( fn( MAP_POLYGON* p ) { + mins.x = min( p->mins.x, mins.x ); + mins.y = min( p->mins.y, mins.y ); + mins.z = min( p->mins.z, mins.z ); + + maxs.x = max( p->maxs.x, maxs.x ); + maxs.y = max( p->maxs.y, maxs.y ); + maxs.z = max( p->maxs.z, maxs.z ); + } ); + + if( vec_dist( mins, m->mins ) > 0.01f || vec_dist( maxs, m->maxs ) > 0.01f ) { + m->mins = mins; + m->maxs = maxs; + map_gen_skybox( m ); + } +} + +// todo: too long -- extract parts into funcs +WORLD_MAP* map_from_file( GAME_DATA* game, const char* path ) { + CFG_SECTION* root = cfg_load( path ); + + ddef( const char* errstr = "map_from_file() : %s - missing %s section\n" ); + if( !root ) { dlog( "map_from_file() : error opening file %s\n", path ); return 0; } + defer( cfg_free( root ); ); + + WORLD_MAP* m = new WORLD_MAP; + const char* name = file_path_last_of( path ); + strcpy( m->name, name ); + + CFG_SECTION* s = cfg_section( root, "map" ); + if( !s ) { dlog( errstr, "map", path ); delete m; return 0; } + + CFG_SECTION* props = cfg_section( s, "props" ); + if( !props ) { dlog( errstr, path, "props" ); delete m; return 0; } + + CFG_SECTION* walls = cfg_section( s, "walls" ); + if( !walls ) { dlog( errstr, path, "walls" ); delete m; return 0; } + + CFG_SECTION* polys = cfg_section( s, "polygons" ); + if( !polys ) { dlog( errstr, path, "polygons" ); delete m; return 0; } + + CFG_INT* propc = cfg_int( s, "propcount" ); + if( !propc ) { dlog( errstr, path, "propcount" ); delete m; return 0; } + + CFG_INT* wallc = cfg_int( s, "wallcount" ); + if( !wallc ) { dlog( errstr, path, "wallcount" ); delete m; return 0; } + + CFG_INT* polyc = cfg_int( s, "polycount" ); + if( !polyc ) { dlog( errstr, path, "polycount" ); delete m; return 0; } + + if( !OK( map_props_from_section( m, game, props, propc->value ) ) ) { delete m; return 0; } + if( !OK( map_walls_from_section( m, game, walls, wallc->value ) ) ) { delete m; return 0; } + if( !OK( map_polygons_from_section( m, game, polys, polyc->value ) ) ) { delete m; return 0; } + + CFG_INT* spritec = cfg_int( s, "spritecount" ); + if( spritec ) { + CFG_SECTION* sprites = cfg_section( s, "sprites" ); + if( !sprites ) { dlog( errstr, path, "sprites" ); delete m; return 0; } + if( !OK( map_sprites_from_section( m, game, sprites, spritec->value ) ) ) { delete m; return 0; } + } + + CFG_VEC3* startpos = cfg_vec3( s, "startpos" ); + if( !startpos ) { + dlog( errstr, path, "startpos" ); + m->startpos = { 0, 0, 0 }; + } + else { + m->startpos = startpos->value; + } + + CFG_FLOAT* startang = cfg_float( s, "startang" ); + if( !startang ) { + dlog( errstr, path, "startang" ); + m->startang = 0; + } + else { + m->startang = startang->value; + } + + map_calc_bounds( m ); + CFG_SECTION* skybox = cfg_section( s, "skybox" ); + if( !skybox ) { + dlog( errstr, path, "skybox" ); + dlog( "using default skybox" ); + m->skybox = map_default_skybox(); + } + else { + if( !OK( map_skybox_from_section( skybox, game, m ) ) ) { + dlog( errstr, path, "skybox" ); + dlog( "using default skybox" ); + m->skybox = map_default_skybox(); + } + } + map_gen_skybox( m ); + return m; +} + +void map_serialize_info( CFG_SECTION* s, WORLD_MAP* map ) { + cfg_int( "wallcount", s, map->walls.size ); + cfg_int( "polycount", s, map->polygons.size ); + cfg_int( "propcount", s, map->props.size ); + cfg_int( "spritecount", s, map->sprites.size ); + cfg_vec3( "startpos", s, map->startpos ); + cfg_float( "startang", s, map->startang ); +} + +void map_serialize_walls( CFG_SECTION* s, WORLD_MAP* map ) { + CFG_SECTION* walls = cfg_section_new( "walls", s ); + char name[64]; + + U32 i = 0; + map->walls.each( fn( MAP_WALL* seg ) { + sprintf( name, "%d", i++ ); + CFG_SECTION* wallsec = cfg_section_new( name, walls ); + cfg_vec3( "start", wallsec, seg->start ); + cfg_vec3( "end", wallsec, seg->end ); + cfg_int( "propid", wallsec, seg->propid ); + } ); +} + +void map_serialize_polygons( CFG_SECTION* s, WORLD_MAP* map ) { + CFG_SECTION* polygons = cfg_section_new( "polygons", s ); + char name[64]; + + U32 i = 0; + map->polygons.each( fn( MAP_POLYGON* p ) { + sprintf( name, "%d", i++ ); + CFG_SECTION* polygonsec = cfg_section_new( name, polygons ); + cfg_int( "vertcount", polygonsec, p->vertices.size ); + cfg_int( "propid", polygonsec, p->propid ); + cfg_int( "type", polygonsec, p->type ); + + CFG_SECTION* vertices = cfg_section_new( "vertices", polygonsec ); + U32 i2 = 0; + p->vertices.each( fn( MAP_VERTEX* v ) { + sprintf( name, "%d", i2++ ); + CFG_SECTION* vertsec = cfg_section_new( name, vertices ); + cfg_vec3( "pos", vertsec, v->pos ); + cfg_vec2( "uv", vertsec, v->uv ); + cfg_clr( "clr", vertsec, v->clr ); + } ); + } ); +} + +void map_serialize_sprites( CFG_SECTION* s, WORLD_MAP* map ) { + CFG_SECTION* sprites = cfg_section_new( "sprites", s ); + char name[64]; + + U32 i = 0; + map->sprites.each( fn( MAP_SPRITE* s ) { + sprintf( name, "%d", i++ ); + CFG_SECTION* spritesec = cfg_section_new( name, sprites ); + cfg_vec3( "pos", spritesec, s->pos ); + cfg_vec2( "size", spritesec, s->size ); + cfg_clr( "clr", spritesec, s->clr ); + if( s->tex ) { + const char* endp = assets_abspath( s->tex->name ); + cfg_str( "tex", spritesec, endp, 256 ); + } + } ); +} + +void map_serialize_props( CFG_SECTION* s, WORLD_MAP* map ) { + CFG_SECTION* props = cfg_section_new( "props", s ); + char name[64]; + + U32 i = 0; + map->props.each( fn( SURF_PROPS* p ) { + sprintf( name, "%d", i++ ); + CFG_SECTION* propsec = cfg_section_new( name, props ); + cfg_clr( "clr", propsec, p->clr ); + if( p->tex ) { + const char* endp = assets_abspath( p->tex->name ); + cfg_str( "tex", propsec, endp, 256 ); + } + } ); +} + + +CFG_SECTION* map_serialize( WORLD_MAP* map ) { + CFG_SECTION* root = cfg_section_new( "root", 0 ); + CFG_SECTION* s = cfg_section_new( "map", root ); + + map_serialize_info( s, map ); + map_serialize_props( s, map ); + map_serialize_walls( s, map ); + map_serialize_polygons( s, map ); + map_serialize_sprites( s, map ); + + return s; +} + +void map_free( GAME_DATA* game, WORLD_MAP* m ) { + if( !m ) return; + if( m->bsp ) bsp_free( m->bsp ); + + m->textures.each( fn( MAP_TEXTURE_ENTRY** e ) { + gl_texture_destroy( game->gl, (*e)->tex ); + delete *e; + } ); + + delete m; +} diff --git a/src/game/world/map.h b/src/game/world/map.h new file mode 100644 index 0000000..29022db --- /dev/null +++ b/src/game/world/map.h @@ -0,0 +1,137 @@ +#pragma once + +#include "../../util/allocator.h" +#include "../../util/vector.h" +#include "../../util/color.h" +#include "../../util/fnv.h" + +#include <string.h> + +enum MapPropId_t { + MAPPROP_SKYBOX = -1, +}; + +struct SURF_PROPS { + struct GL_TEX2D* tex; + CLR clr; +}; + +struct MAP_VERTEX { + VEC3 pos; + VEC3 normal; + VEC2 uv; + CLR clr; +}; + +enum MapPolygonType_t { + MPT_FLOOR, + MPT_CEILING +}; + +struct MAP_POLYGON { + MAP_POLYGON& operator=( const MAP_POLYGON& other ) { + memcpy( this, &other, sizeof(*this) ); + vertices = other.vertices; + + return *this; + } + + LIST<MAP_VERTEX> vertices; + + VEC3 mins; + VEC3 maxs; + U8 type; + I32 propid; +}; + +struct MAP_WALL { + VEC3 start; + VEC3 end; + + VEC2 uvstart; + VEC2 uvend; + + I32 propid; +}; + +struct MAP_TEXTURE_ENTRY { + char name[256]; + struct GL_TEX2D* tex; + FNV1A hash; +}; + +struct MAP_SPRITE { + VEC3 pos; + VEC2 size; + CLR clr; + GL_TEX2D* tex; +}; + +struct MAP_ENTITY { + U32 classid; + LIST<struct OBJECT_PROP*> props; +}; + +struct MAP_SKYBOX { + SURF_PROPS props; + MAP_WALL walls[4]; + MAP_POLYGON polygons[2]; +}; + +struct WORLD_MAP { + LIST<MAP_WALL> walls; + LIST<MAP_POLYGON> polygons; + LIST<MAP_SPRITE> sprites; + LIST<SURF_PROPS> props; + MAP_SKYBOX skybox; + F32 w; + F32 h; + + VEC3 mins; + VEC3 maxs; + char name[256]; + + VEC3 startpos; + F32 startang; + + struct BSP* bsp{}; + + LIST<MAP_TEXTURE_ENTRY*> textures; +}; + +extern WORLD_MAP* map_from_file( struct GAME_DATA* game, const char* filename ); +extern struct CFG_SECTION* map_serialize( WORLD_MAP* map ); +extern STAT map_add_texture_ref( WORLD_MAP* map, GL_TEX2D* tex ); +extern GL_TEX2D* map_find_texture( WORLD_MAP* map, const char* name ); +extern void map_free( struct GAME_DATA* game, WORLD_MAP* map ); + +// checks mins/maxs and recreates skybox if needed +extern void map_check_bounds( WORLD_MAP* map ); +extern void map_calc_bounds( WORLD_MAP* map ); +extern void map_polygon_calc_bounds( MAP_POLYGON* p ); + + +inline SURF_PROPS* map_props_get_special( WORLD_MAP* w, I32 prop ) { + switch( prop ) { + case MAPPROP_SKYBOX: return &w->skybox.props; + }; + dlog( "map_props_get_special(): unknown special prop %d\n", prop ); + return 0; +} + +inline SURF_PROPS* map_get_props( WORLD_MAP* m, I32 idx ) { + return idx < 0 ? + map_props_get_special( m, idx ) : + &m->props[idx]; +} + +inline SURF_PROPS* wall_get_props( WORLD_MAP* w, MAP_WALL* s ) { + return s->propid < 0 ? + map_props_get_special( w, s->propid ) : + &w->props[s->propid]; +} +inline SURF_PROPS* polygon_get_props( WORLD_MAP* w, MAP_POLYGON* s ) { + return s->propid < 0 ? + map_props_get_special( w, s->propid ) : + &w->props[s->propid]; +} diff --git a/src/game/world/trace.cpp b/src/game/world/trace.cpp new file mode 100644 index 0000000..b3a0041 --- /dev/null +++ b/src/game/world/trace.cpp @@ -0,0 +1,393 @@ +#include <cfloat> + +#include "trace.h" +#include "../../util/math.h" +#include "bsp.h" + +// i have no idea how this works tbh i pasted this +// todo : backface culling (return 0 if d < BSP_TRACE_EPSILON) +U8 bsp_segment_intersects_tri( + const VEC3& va, const VEC3& vb, + const VEC3& v0, const VEC3& v1, const VEC3& v2, + F32* fract, + VEC2* out_uv = 0 +) { + VEC3 dir = vb - va; + VEC3 e1 = v1 - v0, + e2 = v2 - v0; + + VEC3 p = vec_cross( dir, e2 ); + F32 d = vec_dot( e1, p ); + + if( fabsf( d ) < BSP_TRACE_EPSILON ) { + // parallel + return 0; + } + + F32 inv = 1.0f / d; + VEC3 ivec = va - v0; + F32 u = vec_dot( ivec, p ) * inv; + if( u < -BSP_TRACE_EPSILON || u > 1.0f + BSP_TRACE_EPSILON ) { + // outside + return 0; + } + + VEC3 q = vec_cross( ivec, e1 ); + F32 v = vec_dot( dir, q ) * inv; + if( v < -BSP_TRACE_EPSILON || u + v > 1.0f + BSP_TRACE_EPSILON ) { + // outside + return 0; + } + + F32 t = vec_dot( e2, q ) * inv; + if( t < -BSP_TRACE_EPSILON || t > 1.0f + BSP_TRACE_EPSILON ) { + // outside + return 0; + } + + if( fract ) *fract = m_clamp( t, 0.f, 1.f ); + if( out_uv ) { + *out_uv = { u, v }; + } + + return 1; +} + +U8 bsp_trace_leaf_segment( BSP_TRACE* trace, BSP* bsp, I32 leafidx, const VEC3& va, const VEC3& vb ) { + BSP_LEAF* leaf = &bsp->leaves.data[leafidx]; + U8 didhit = 0; + F32 best = FLT_MAX; + + for( U32 i = 0; i < leaf->count; ++i ) { + BSP_FACE* face = &bsp->faces.data[leaf->first + i]; + + // shouldnt happen + if( face->verts.size < 3 ) + continue; + + for( U32 t0 = 1; t0 + 1 < face->verts.size; ++t0 ) { + VEC3 v0 = face->verts[0].pos; + VEC3 v1 = face->verts[t0].pos; + VEC3 v2 = face->verts[t0 + 1].pos; + + // todo : skip func for props + + F32 t; + VEC2 uv; + + if( !bsp_segment_intersects_tri( va, vb, v0, v1, v2, &t, &uv ) ) + continue; + + if( t < best ) { + best = t; + trace->hit = didhit = 1; + trace->frac = t; + trace->point = va + (vb - va) * t; + trace->normal = vec_normalize( vec_cross( v1 - v0, v2 - v0 ) ); + trace->propid = face->propid; + trace->leafid = leafidx; + trace->faceid = leaf->first + i; + } + } + } + + return didhit; +} + +U8 bsp_trace_segment( BSP_TRACE* trace, BSP* bsp, I32 nodeidx, const VEC3& va, const VEC3& vb ) { + if( bsp_is_leaf( nodeidx ) ) { + I32 i = bsp_leaf_index( nodeidx ); + return bsp_trace_leaf_segment( trace, bsp, i, va, vb ); + } + + BSP_NODE* node = &bsp->nodes.data[nodeidx]; + F32 da = bsp_plane_dist( node->plane, va ); + F32 db = bsp_plane_dist( node->plane, vb ); + + if( da > BSP_TRACE_EPSILON && db > BSP_TRACE_EPSILON ) + return bsp_trace_segment( trace, bsp, node->front, va, vb ); + if( da < -BSP_TRACE_EPSILON && db < -BSP_TRACE_EPSILON ) + return bsp_trace_segment( trace, bsp, node->back, va, vb ); + + VEC3 dir = vb - va; + F32 denom = vec_dot( node->plane.normal, dir ); + I32 near = (da >= 0.f) ? node->front : node->back; + I32 far = (da >= 0.f) ? node->back : node->front; + if( fabsf( denom ) < BSP_TRACE_EPSILON ) { + if( bsp_trace_segment( trace, bsp, near, va, vb ) ) + return 1; + return bsp_trace_segment( trace, bsp, far, va, vb ); + } + + F32 t = (node->plane.dist - vec_dot( node->plane.normal, va ) ) / denom; + t = m_clamp( t, 0.f, 1.f ); + + VEC3 iplane = va + dir * t; + if( bsp_trace_segment( trace, bsp, near, va, iplane ) ) + return 1; + + if( t <= BSP_TRACE_EPSILON || t >= 1.0f - BSP_TRACE_EPSILON ) { + return bsp_trace_segment( trace, bsp, far, iplane, vb ); + } + + return bsp_trace_segment( trace, bsp, far, iplane, vb ); +} + +// nudge the trace ends slightly to prevent exact-coplanar infinite recursion. +// quake does this every recursion divided up by the length of the trace +// should not matter in the end +void bsp_trace_nudge( BSP_TRACE* trace ) { + VEC3 dir = trace->in_end - trace->in_start; + VEC3 nudge = dir * BSP_TRACE_EPSILON; + trace->in_start += nudge; + trace->in_end -= nudge; +} + +U8 bsp_trace( BSP_TRACE* trace, BSP* bsp, VEC3 start, VEC3 end ) { + if( !trace ) { + dlog( "bsp_trace() : null trace\n" ); + abort(); + } + + trace->in_start = start; + trace->in_end = end; + + return bsp_trace( trace, bsp ); +} + +U8 bsp_trace( BSP_TRACE* trace, BSP* bsp ) { + if( !trace ) { + dlog( "bsp_trace() : null trace\n" ); + abort(); + } + + trace->hit = 0; + + bsp_trace_nudge( trace ); + VEC3 start = trace->in_start; + VEC3 end = trace->in_end; + + return bsp_trace_segment( trace, bsp, bsp->root, start, end ); +} + +inline VEC3 bsp_face_get_normal( const BSP_FACE* f ){ + const VEC3 v0 = f->verts.data[0].pos; + const VEC3 v1 = f->verts.data[1].pos; + const VEC3 v2 = f->verts.data[2].pos; + return vec_normalize( vec_cross( v1 - v0, v2 - v0 ) ); +} + +inline U8 point_in_inflated_poly( + const VEC3& point, + const BSP_FACE* face, + const AABB& hull, + const VEC3& norm +){ + VEC3 center{}; + U32 c = face->verts.size; + for( U32 i = 0; i < c; ++i ) + center = center + face->verts.data[i].pos; + if( c ) center = center * (1.0f / c); + + for( U32 i = 0; i < c; ++i ) { + VEC3 a = face->verts.data[i].pos; + VEC3 b = face->verts.data[(i+1)%c].pos; + VEC3 in = vec_cross( norm, b - a ); + F32 len = vec_len( in ); + if( len <= 0.00001f ) continue; + in *= (1.0f/ len); + if( vec_dot( center - a, in ) < 0.0f ) + in = in * -1.0f; + + F32 dist = vec_dot( point - a, in ); + VEC2 feet = aabb_extend_at_feet( hull, in ); + F32 pos_r = feet.x, neg_r = feet.y; + F32 expand = norm.z >= 0 ? neg_r : pos_r; + if( dist < -expand - BSP_TRACE_EPSILON ) + return 0; + } + return 1; +} + + +// big fat ass +U8 bsp_trace_sweep_aabb_to_face( + BSP_TRACE* trace, + const AABB& hull, + BSP_FACE* face, + const VEC3& start, + const VEC3& end, + U32 face_idx +) { + if( face->verts.size < 3 ) + return 0; + + VEC3 norm = bsp_face_get_normal( face ); + F32 d = vec_dot( norm, face->verts[0].pos ); + VEC2 feet = aabb_extend_at_feet( hull, norm ); + F32 pos_r = feet.x, neg_r = feet.y; + F32 radius = norm.z >= 0 ? neg_r : pos_r; + + VEC3 dir = end - start; + F32 s0 = vec_dot( norm, trace->in_start ) - d; + F32 ndir = vec_dot( norm, trace->in_end - trace->in_start ); + + if( fabsf( s0 ) <= radius + BSP_TRACE_EPSILON ) { + if( point_in_inflated_poly( start, face, hull, norm ) ) { + trace->hit = 1; + trace->frac = 0.f; + trace->point = start; + trace->normal = norm; + trace->propid = face->propid; + trace->faceid = face_idx; + return 1; + } + } + + if( fabsf( ndir ) < BSP_TRACE_EPSILON ) + return 0; + + F32 t_enter = (-radius - s0) / ndir; + F32 t_exit = ( radius - s0) / ndir; + if( t_enter > t_exit ) { F32 tmp=t_enter; t_enter=t_exit; t_exit=tmp; } + + if( t_exit < 0.f - BSP_TRACE_EPSILON || t_enter > 1.f + BSP_TRACE_EPSILON ) + return 0; + + F32 t = m_clamp( t_enter, 0.f, 1.f ); + VEC3 step = start + dir * t; + + if( !point_in_inflated_poly( step, face, hull, norm ) ) + return 0; + + trace->hit = 1; + trace->frac = t; + trace->point = step; + trace->normal = norm; + trace->propid = face->propid; + trace->faceid = face_idx; + return 1; +} + +U8 bsp_trace_sweep_aabb_to_leaf( + BSP* bsp, + BSP_TRACE* trace, + const AABB& hull, + const VEC3& start, + const VEC3& end, + I32 leaf_idx +) { + BSP_LEAF* leaf = &bsp->leaves.data[leaf_idx]; + U8 hit = 0; + F32 best = FLT_MAX; + + BSP_TRACE besthit = *trace; + + for( U32 i = 0; i < leaf->count; ++i ) { + U32 face_idx = leaf->first + i; + BSP_FACE* f = &bsp->faces.data[face_idx]; + + if( f->propid == MAPPROP_SKYBOX ) + continue; + + BSP_TRACE tr = *trace; + if( !bsp_trace_sweep_aabb_to_face( &tr, hull, f, start, end, face_idx ) ) + continue; + + if( tr.frac < best ) { + best = tr.frac; + besthit = tr; + besthit.leafid = leaf_idx; + hit = 1; + } + } + + if( hit ) { + *trace = besthit; + trace->hit = 1; + } + return hit; +} + +U8 bsp_trace_sweep_aabb( + BSP_TRACE* trace, + BSP* bsp, + const AABB& hull, + const VEC3& start, + const VEC3& end, + I32 node_idx +) { + if( bsp_is_leaf( node_idx ) ) { + I32 i = bsp_leaf_index( node_idx ); + return bsp_trace_sweep_aabb_to_leaf( bsp, trace, hull, start, end, i ); + } + + BSP_NODE* node = &bsp->nodes.data[node_idx]; + VEC3 n = node->plane.normal; + F32 d = node->plane.dist; + + F32 s_dist = vec_dot( n, start ) - d; + F32 e_dist = vec_dot( n, end ) - d; + F32 offset = aabb_support_radius( hull, n ); + + if( s_dist > offset && e_dist > offset ) + return bsp_trace_sweep_aabb( trace, bsp, hull, start, end, node->front ); + + if( s_dist < -offset && e_dist < -offset ) + return bsp_trace_sweep_aabb( trace, bsp, hull, start, end, node->back ); + + VEC3 dir = end - start; + F32 denom = ( e_dist - s_dist ); + + if( fabsf(denom) < BSP_TRACE_EPSILON ) { + I32 near = (s_dist >= 0.f) ? node->front : node->back; + I32 far = (s_dist >= 0.f) ? node->back : node->front; + if( bsp_trace_sweep_aabb( trace, bsp, hull, start, end, near ) ) + return 1; + return bsp_trace_sweep_aabb( trace, bsp, hull, start, end, far ); + } + + F32 tin = ( offset - s_dist) / denom; + F32 tout = (-offset - s_dist) / denom; + if( tin > tout ) { F32 tmp = tin; tin = tout; tout = tmp; } + + tin = m_clamp( tin, 0.f, 1.f ); + tout = m_clamp( tout , 0.f, 1.f ); + + VEC3 mid = start + dir * tin; + I32 near = (s_dist > 0.f) ? node->front : node->back; + I32 far = (s_dist > 0.f) ? node->back : node->front; + + if( bsp_trace_sweep_aabb( trace, bsp, hull, start, mid, near ) ) + return 1; + + if( tin <= BSP_TRACE_EPSILON || tin >= 1.f - BSP_TRACE_EPSILON ) { + F32 nudge = (denom >= 0.f ? 1.f : -1.f) * BSP_TRACE_EPSILON * 4.f; + VEC3 nudged = mid + dir * nudge; + return bsp_trace_sweep_aabb( trace, bsp, hull, nudged, end, far ); + } + + return bsp_trace_sweep_aabb(trace, bsp, hull, mid, end, far ); +} + +U8 bsp_trace( BSP_TRACE* trace, BSP* bsp, AABB hull ) { + if( !trace || !bsp ) + return 0; + + trace->hit = 0; + bsp_trace_nudge( trace ); + VEC3 start = trace->in_start; + VEC3 end = trace->in_end; + + U8 ret = bsp_trace_sweep_aabb( trace, bsp, hull, start, end, bsp->root ); + return ret; +} + +U8 bsp_trace( BSP_TRACE* trace, BSP* bsp, AABB hull, VEC3 start, VEC3 end ) { + if( !trace || !bsp ) + return 0; + + trace->in_start = start; + trace->in_end = end; + return bsp_trace( trace, bsp, hull ); +} diff --git a/src/game/world/trace.h b/src/game/world/trace.h new file mode 100644 index 0000000..8a0f4eb --- /dev/null +++ b/src/game/world/trace.h @@ -0,0 +1,29 @@ +#pragma once + +#include "bsp.h" +#include "../../util/aabb.h" + +// quake uses 1 / 32 +const F32 BSP_TRACE_EPSILON = 1.f / 64.f; + +struct BSP_TRACE { + VEC3 in_start; + VEC3 in_end; + + U8 hit; + F32 frac; + VEC3 point; + VEC3 normal; + U32 propid; + I32 leafid; + U32 faceid; +}; + +extern U8 bsp_trace( BSP_TRACE* trace, BSP* bsp ); +extern U8 bsp_trace( BSP_TRACE* trace, BSP* bsp, VEC3 start, VEC3 end ); + +extern U8 bsp_trace( BSP_TRACE* trace, BSP* bsp, AABB aabb ); +extern U8 bsp_trace( BSP_TRACE* trace, BSP* bsp, AABB aabb, VEC3 start, VEC3 end ); + +extern U8 bsp_trace( BSP_TRACE* trace ); +extern U8 bsp_trace( BSP_TRACE* trace, VEC3 start, VEC3 end ); diff --git a/src/game/world/world.cpp b/src/game/world/world.cpp new file mode 100644 index 0000000..337b496 --- /dev/null +++ b/src/game/world/world.cpp @@ -0,0 +1,14 @@ +#include "world.h" +#include "../objlist.h" + +WORLD* world_create( WORLD_MAP* map ) { + WORLD* w = obj_add<WORLD>( "root" ); + w->map = map; + + return w; +} + +STAT world_populate_entities( WORLD *world ) { + + return STAT_OK; +} diff --git a/src/game/world/world.h b/src/game/world/world.h new file mode 100644 index 0000000..229f235 --- /dev/null +++ b/src/game/world/world.h @@ -0,0 +1,11 @@ +#pragma once +#include "../object.h" + +struct WORLD : OBJECT { + static const U32 CLASSID = OBJCLASS_WORLD; + + struct WORLD_MAP* map; +}; + +extern WORLD* world_create( WORLD_MAP* map ); +extern STAT world_populate_entities( WORLD* world ); |
