diff options
| author | navewindre <boneyaard@gmail.com> | 2025-09-03 20:10:09 +0200 |
|---|---|---|
| committer | navewindre <boneyaard@gmail.com> | 2025-09-03 20:10:09 +0200 |
| commit | f8b92ce3aa08b1445c9f956d8166830946562d12 (patch) | |
| tree | 94e63a5aec9f8f52b577f56799e0c9201fd976a5 /src/game/world | |
a
Diffstat (limited to 'src/game/world')
| -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 |
12 files changed, 2352 insertions, 0 deletions
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 ); |
