clipmap.cc (35529B)
1 #include "intrinsics.h" 2 #include "int_conversions.h" 3 #include "linear_algebra.h" 4 #include "aabb.h" 5 #include "game.h" 6 #include "renderer.h" 7 #include "shaders.h" 8 #include "immediate.h" 9 #include "text_renderer.h" 10 #include "decompress_bc.h" 11 #include "gl.h" 12 #include "obj.h" 13 #include "skybox.h" 14 #include "stream.h" 15 #include "hashtable.h" 16 #include "pool.h" 17 #include "profiler.h" 18 #include "platform_network.h" 19 20 #include "libs/lz4/lz4.h" 21 22 struct ClipmapTerrain { 23 struct { 24 Mesh tile; 25 Mesh empty_tile; 26 Mesh filler; 27 Mesh trim; 28 Mesh cross; 29 Mesh seam; 30 Mesh skirt; 31 Texture heightmap; 32 Texture normalmap; 33 Texture horizonmap; 34 } gpu; 35 36 array2d< u16 > heightmap; 37 QuadTree quadtree; 38 }; 39 40 static const float EYE_HEIGHT = 1.8f; 41 42 static ClipmapTerrain clipmap; 43 44 struct Fireball { 45 v3 pos; 46 v3 velocity; 47 }; 48 49 struct Explosion { 50 v3 pos; 51 double created_at; 52 }; 53 54 struct Player { 55 u64 sid; 56 v3 pos; 57 }; 58 59 static FB msaa_fb; 60 61 static UDPSocket sock; 62 static NetAddress server_addr; 63 static u64 sid; 64 static double last_connection_attempt = -5.0; // TODO 65 static bool connected = false; 66 67 static Pool< Fireball, 1024 > fireballs; 68 static Pool< Explosion, 1024 > explosions; 69 70 static Pool< Player, 1024 > players; 71 static HashTable< Player *, 1024 * 2 > sid_to_player; 72 73 static VB trees_instance_data; 74 static u32 num_trees; 75 static Mesh tree_mesh; 76 77 static constexpr u32 TILE_RESOLUTION = 48; 78 static constexpr u32 PATCH_VERT_RESOLUTION = TILE_RESOLUTION + 1; 79 static constexpr u32 CLIPMAP_RESOLUTION = TILE_RESOLUTION * 4 + 1; 80 static constexpr u32 CLIPMAP_VERT_RESOLUTION = CLIPMAP_RESOLUTION + 1; 81 static constexpr u32 NUM_CLIPMAP_LEVELS = 7; 82 83 static void load_trees( MemoryArena * arena ) { 84 MEMARENA_SCOPED_CHECKPOINT( arena ); 85 86 size_t compressed_len; 87 u8 * compressed_trees = file_get_contents( "terrains/gta16.png.parts/trees.lz4", &compressed_len ); 88 89 // TODO: don't hardcode this! 90 array< v3 > trees = memarena_push_array( arena, v3, 300000 ); 91 int decompressed_len = LZ4_decompress_safe( ( char * ) compressed_trees, ( char * ) trees.ptr(), compressed_len, trees.num_bytes() ); 92 ASSERT( decompressed_len > 0 ); 93 94 free( compressed_trees ); 95 96 num_trees = decompressed_len / sizeof( v3 ); 97 array< m4 > tree_matrices = memarena_push_array( arena, m4, num_trees ); 98 99 m4 rot = m4_rotx( deg_to_rad( 90 ) ); 100 for( size_t i = 0; i < num_trees; i++ ) { 101 tree_matrices[ i ] = m4_translation( trees[ i ] ) * rot; 102 } 103 104 trees_instance_data = renderer_new_vb( tree_matrices ); 105 106 tree_mesh = load_obj( "models/trees/PineTree.obj", arena ); 107 } 108 109 static size_t patch2d( size_t x, size_t y ) { 110 return y * PATCH_VERT_RESOLUTION + x; 111 } 112 113 static void file_get_contents_and_decompress( const char * path, u8 * decompressed, size_t decompressed_size ) { 114 size_t compressed_size; 115 u8 * compressed = file_get_contents( path, &compressed_size ); 116 int ok = LZ4_decompress_safe( ( const char * ) compressed, ( char * ) decompressed, compressed_size, decompressed_size ); 117 ASSERT( ok > 0 && size_t( ok ) == decompressed_size ); 118 free( compressed ); 119 } 120 121 struct RGBA { 122 u8 r, g, b, a; 123 }; 124 125 GAME_INIT( game_init ) { 126 net_init(); 127 128 msaa_fb = { }; 129 130 // load quadtree 131 { 132 clipmap.heightmap = alloc_array2d< u16 >( &mem->persistent_arena, 4096, 4096 ); 133 134 array< QuadTreeNode > nodes = alloc_array< QuadTreeNode >( &mem->persistent_arena, 4096 * 4096 / 3 ); 135 file_get_contents_and_decompress( "terrains/gta16.png.parts/quadtree.lz4", ( u8 * ) nodes.ptr(), nodes.num_bytes() ); 136 137 clipmap.quadtree.nodes = nodes; 138 clipmap.quadtree.heightmap = clipmap.heightmap; 139 } 140 141 // load heightmap 142 { 143 MEMARENA_SCOPED_CHECKPOINT( &mem->persistent_arena ); 144 145 u8 * heightmap_data = memarena_push_size( &mem->persistent_arena, 4096 * 4096 ); 146 file_get_contents_and_decompress( "terrains/gta16.png.parts/heightmap.bc5.lz4", heightmap_data, 4096 * 4096 ); 147 148 TextureConfig texture_config; 149 texture_config.width = 4096; 150 texture_config.height = 4096; 151 texture_config.data = heightmap_data; 152 texture_config.format = TEXFMT_BC5; 153 texture_config.wrap = TEXWRAP_BORDER; 154 texture_config.border_colour = v4( 0 ); 155 clipmap.gpu.heightmap = renderer_new_texture( texture_config ); 156 157 bc5_to_heightmap( &mem->persistent_arena, clipmap.heightmap, heightmap_data ); 158 } 159 160 // load normalmap 161 { 162 MEMARENA_SCOPED_CHECKPOINT( &mem->persistent_arena ); 163 164 u8 * normalmap_data = memarena_push_size( &mem->persistent_arena, 4096 * 4096 ); 165 file_get_contents_and_decompress( "terrains/gta16.png.parts/normalmap.bc5.lz4", normalmap_data, 4096 * 4096 ); 166 167 TextureConfig texture_config; 168 texture_config.width = 4096; 169 texture_config.height = 4096; 170 texture_config.data = normalmap_data; 171 texture_config.format = TEXFMT_BC5; 172 texture_config.wrap = TEXWRAP_BORDER; 173 texture_config.border_colour = v4( 0.5f ); 174 clipmap.gpu.normalmap = renderer_new_texture( texture_config ); 175 } 176 177 // load horizonmap 178 { 179 MEMARENA_SCOPED_CHECKPOINT( &mem->persistent_arena ); 180 181 u8 * horizonmap_data = memarena_push_size( &mem->persistent_arena, 4096 * 4096 / 2 ); 182 file_get_contents_and_decompress( "terrains/gta16.png.parts/horizonmap.bc4.lz4", horizonmap_data, 4096 * 4096 / 2 ); 183 184 TextureConfig texture_config; 185 texture_config.width = 4096; 186 texture_config.height = 4096; 187 texture_config.data = horizonmap_data; 188 texture_config.format = TEXFMT_BC4; 189 texture_config.wrap = TEXWRAP_BORDER; 190 texture_config.border_colour = v4( 0 ); 191 clipmap.gpu.horizonmap = renderer_new_texture( texture_config ); 192 } 193 194 // generate tile mesh 195 { 196 MEMARENA_SCOPED_CHECKPOINT( &mem->persistent_arena ); 197 198 array< v3 > vertices = alloc_array< v3 >( &mem->persistent_arena, PATCH_VERT_RESOLUTION * PATCH_VERT_RESOLUTION ); 199 size_t n = 0; 200 201 for( u32 y = 0; y < PATCH_VERT_RESOLUTION; y++ ) { 202 for( u32 x = 0; x < PATCH_VERT_RESOLUTION; x++ ) { 203 vertices[ n++ ] = v3( x, y, 0 ); 204 } 205 } 206 207 ASSERT( n == vertices.n ); 208 209 array< u32 > indices = alloc_array< u32 >( &mem->persistent_arena, TILE_RESOLUTION * TILE_RESOLUTION * 6 ); 210 n = 0; 211 212 for( u32 y = 0; y < TILE_RESOLUTION; y++ ) { 213 for( u32 x = 0; x < TILE_RESOLUTION; x++ ) { 214 indices[ n++ ] = patch2d( x, y ); 215 indices[ n++ ] = patch2d( x + 1, y + 1 ); 216 indices[ n++ ] = patch2d( x, y + 1 ); 217 218 indices[ n++ ] = patch2d( x, y ); 219 indices[ n++ ] = patch2d( x + 1, y ); 220 indices[ n++ ] = patch2d( x + 1, y + 1 ); 221 } 222 } 223 224 ASSERT( n == indices.n ); 225 226 MeshConfig mesh_config; 227 mesh_config.positions = renderer_new_vb( vertices ); 228 mesh_config.indices = renderer_new_ib( indices ); 229 mesh_config.num_vertices = indices.n; 230 clipmap.gpu.tile = renderer_new_mesh( mesh_config ); 231 } 232 233 // generate empty tile mesh 234 { 235 MEMARENA_SCOPED_CHECKPOINT( &mem->persistent_arena ); 236 237 array< v3 > vertices = alloc_array< v3 >( &mem->persistent_arena, TILE_RESOLUTION * 4 + 1 ); 238 239 // vertices[ 0 ] is the centre of the fan 240 vertices[ 0 ] = v3( v2( TILE_RESOLUTION / 2.0f ), 0 ); 241 242 for( u32 i = 0; i < TILE_RESOLUTION; i++ ) { 243 vertices[ i + 1 + 0 * TILE_RESOLUTION ] = v3( i, 0, 0 ); 244 vertices[ i + 1 + 1 * TILE_RESOLUTION ] = v3( TILE_RESOLUTION, i, 0 ); 245 vertices[ i + 1 + 2 * TILE_RESOLUTION ] = v3( TILE_RESOLUTION - i, TILE_RESOLUTION, 0 ); 246 vertices[ i + 1 + 3 * TILE_RESOLUTION ] = v3( 0, TILE_RESOLUTION - i, 0 ); 247 } 248 249 array< u32 > indices = alloc_array< u32 >( &mem->persistent_arena, TILE_RESOLUTION * 12 ); 250 251 for( u32 i = 0; i < TILE_RESOLUTION * 4; i++ ) { 252 indices[ i * 3 + 0 ] = 0; 253 indices[ i * 3 + 1 ] = i + 1; 254 indices[ i * 3 + 2 ] = i + 2; 255 } 256 257 // make the last triangle wrap around 258 indices[ indices.n - 1 ] = 1; 259 260 MeshConfig mesh_config; 261 mesh_config.positions = renderer_new_vb( vertices ); 262 mesh_config.indices = renderer_new_ib( indices ); 263 mesh_config.num_vertices = indices.n; 264 clipmap.gpu.empty_tile = renderer_new_mesh( mesh_config ); 265 } 266 267 // generate filler mesh 268 { 269 MEMARENA_SCOPED_CHECKPOINT( &mem->persistent_arena ); 270 271 array< v3 > vertices = alloc_array< v3 >( &mem->persistent_arena, PATCH_VERT_RESOLUTION * 8 ); 272 size_t n = 0; 273 u32 offset = TILE_RESOLUTION; 274 275 for( u32 i = 0; i < PATCH_VERT_RESOLUTION; i++ ) { 276 vertices[ n++ ] = v3( offset + i + 1, 0, 0 ); 277 vertices[ n++ ] = v3( offset + i + 1, 1, 0 ); 278 } 279 280 for( u32 i = 0; i < PATCH_VERT_RESOLUTION; i++ ) { 281 vertices[ n++ ] = v3( 1, offset + i + 1, 0 ); 282 vertices[ n++ ] = v3( 0, offset + i + 1, 0 ); 283 } 284 285 for( u32 i = 0; i < PATCH_VERT_RESOLUTION; i++ ) { 286 vertices[ n++ ] = v3( -float( offset + i ), 1, 0 ); 287 vertices[ n++ ] = v3( -float( offset + i ), 0, 0 ); 288 } 289 290 for( u32 i = 0; i < PATCH_VERT_RESOLUTION; i++ ) { 291 vertices[ n++ ] = v3( 0, -float( offset + i ), 0 ); 292 vertices[ n++ ] = v3( 1, -float( offset + i ), 0 ); 293 } 294 295 ASSERT( n == vertices.n ); 296 297 array< u32 > indices = alloc_array< u32 >( &mem->persistent_arena, TILE_RESOLUTION * 24 ); 298 n = 0; 299 300 for( u32 i = 0; i < TILE_RESOLUTION * 4; i++ ) { 301 // the arms shouldn't be connected to each other 302 u32 arm = i / TILE_RESOLUTION; 303 304 u32 bl = ( arm + i ) * 2 + 0; 305 u32 br = ( arm + i ) * 2 + 1; 306 u32 tl = ( arm + i ) * 2 + 2; 307 u32 tr = ( arm + i ) * 2 + 3; 308 309 if( arm % 2 == 0 ) { 310 indices[ n++ ] = br; 311 indices[ n++ ] = bl; 312 indices[ n++ ] = tr; 313 indices[ n++ ] = bl; 314 indices[ n++ ] = tl; 315 indices[ n++ ] = tr; 316 } 317 else { 318 indices[ n++ ] = br; 319 indices[ n++ ] = bl; 320 indices[ n++ ] = tl; 321 indices[ n++ ] = br; 322 indices[ n++ ] = tl; 323 indices[ n++ ] = tr; 324 } 325 } 326 327 ASSERT( n == indices.n ); 328 329 MeshConfig mesh_config; 330 mesh_config.positions = renderer_new_vb( vertices ); 331 mesh_config.indices = renderer_new_ib( indices ); 332 mesh_config.num_vertices = indices.n; 333 clipmap.gpu.filler = renderer_new_mesh( mesh_config ); 334 } 335 336 // generate trim mesh 337 { 338 MEMARENA_SCOPED_CHECKPOINT( &mem->persistent_arena ); 339 340 array< v3 > vertices = alloc_array< v3 >( &mem->persistent_arena, ( CLIPMAP_VERT_RESOLUTION * 2 + 1 ) * 2 ); 341 size_t n = 0; 342 343 // vertical part of L 344 for( u32 i = 0; i < CLIPMAP_VERT_RESOLUTION + 1; i++ ) { 345 vertices[ n++ ] = v3( 0, CLIPMAP_VERT_RESOLUTION - i, 0 ); 346 vertices[ n++ ] = v3( 1, CLIPMAP_VERT_RESOLUTION - i, 0 ); 347 } 348 349 size_t start_of_horizontal = n; 350 351 // horizontal part of L 352 for( u32 i = 0; i < CLIPMAP_VERT_RESOLUTION; i++ ) { 353 vertices[ n++ ] = v3( i + 1, 0, 0 ); 354 vertices[ n++ ] = v3( i + 1, 1, 0 ); 355 } 356 357 ASSERT( n == vertices.n ); 358 359 for( v3 & v : vertices ) { 360 v -= v3( 0.5f * v2( CLIPMAP_VERT_RESOLUTION + 1 ), 0 ); 361 } 362 363 array< u32 > indices = alloc_array< u32 >( &mem->persistent_arena, ( CLIPMAP_VERT_RESOLUTION * 2 - 1 ) * 6 ); 364 n = 0; 365 366 for( u32 i = 0; i < CLIPMAP_VERT_RESOLUTION; i++ ) { 367 indices[ n++ ] = ( i + 0 ) * 2 + 1; 368 indices[ n++ ] = ( i + 0 ) * 2 + 0; 369 indices[ n++ ] = ( i + 1 ) * 2 + 0; 370 371 indices[ n++ ] = ( i + 1 ) * 2 + 1; 372 indices[ n++ ] = ( i + 0 ) * 2 + 1; 373 indices[ n++ ] = ( i + 1 ) * 2 + 0; 374 } 375 376 for( u32 i = 0; i < CLIPMAP_VERT_RESOLUTION - 1; i++ ) { 377 indices[ n++ ] = start_of_horizontal + ( i + 0 ) * 2 + 1; 378 indices[ n++ ] = start_of_horizontal + ( i + 0 ) * 2 + 0; 379 indices[ n++ ] = start_of_horizontal + ( i + 1 ) * 2 + 0; 380 381 indices[ n++ ] = start_of_horizontal + ( i + 1 ) * 2 + 1; 382 indices[ n++ ] = start_of_horizontal + ( i + 0 ) * 2 + 1; 383 indices[ n++ ] = start_of_horizontal + ( i + 1 ) * 2 + 0; 384 } 385 386 ASSERT( n == indices.n ); 387 388 MeshConfig mesh_config; 389 mesh_config.positions = renderer_new_vb( vertices ); 390 mesh_config.indices = renderer_new_ib( indices ); 391 mesh_config.num_vertices = indices.n; 392 clipmap.gpu.trim = renderer_new_mesh( mesh_config ); 393 } 394 395 // generate cross mesh 396 { 397 MEMARENA_SCOPED_CHECKPOINT( &mem->persistent_arena ); 398 399 array< v3 > vertices = alloc_array< v3 >( &mem->persistent_arena, PATCH_VERT_RESOLUTION * 8 ); 400 size_t n = 0; 401 402 // horizontal vertices 403 for( u32 i = 0; i < PATCH_VERT_RESOLUTION * 2; i++ ) { 404 vertices[ n++ ] = v3( i - float( TILE_RESOLUTION ), 0, 0 ); 405 vertices[ n++ ] = v3( i - float( TILE_RESOLUTION ), 1, 0 ); 406 } 407 408 size_t start_of_vertical = n; 409 410 // vertical vertices 411 for( u32 i = 0; i < PATCH_VERT_RESOLUTION * 2; i++ ) { 412 vertices[ n++ ] = v3( 0, i - float( TILE_RESOLUTION ), 0 ); 413 vertices[ n++ ] = v3( 1, i - float( TILE_RESOLUTION ), 0 ); 414 } 415 416 ASSERT( n == vertices.n ); 417 418 array< u32 > indices = alloc_array< u32 >( &mem->persistent_arena, TILE_RESOLUTION * 24 + 6 ); 419 n = 0; 420 421 // horizontal indices 422 for( u32 i = 0; i < TILE_RESOLUTION * 2 + 1; i++ ) { 423 u32 bl = i * 2 + 0; 424 u32 br = i * 2 + 1; 425 u32 tl = i * 2 + 2; 426 u32 tr = i * 2 + 3; 427 428 indices[ n++ ] = br; 429 indices[ n++ ] = bl; 430 indices[ n++ ] = tr; 431 indices[ n++ ] = bl; 432 indices[ n++ ] = tl; 433 indices[ n++ ] = tr; 434 } 435 436 // vertical indices 437 for( u32 i = 0; i < TILE_RESOLUTION * 2 + 1; i++ ) { 438 if( i == TILE_RESOLUTION ) 439 continue; 440 441 u32 bl = i * 2 + 0; 442 u32 br = i * 2 + 1; 443 u32 tl = i * 2 + 2; 444 u32 tr = i * 2 + 3; 445 446 indices[ n++ ] = start_of_vertical + br; 447 indices[ n++ ] = start_of_vertical + tr; 448 indices[ n++ ] = start_of_vertical + bl; 449 indices[ n++ ] = start_of_vertical + bl; 450 indices[ n++ ] = start_of_vertical + tr; 451 indices[ n++ ] = start_of_vertical + tl; 452 } 453 454 ASSERT( n == indices.n ); 455 456 MeshConfig mesh_config; 457 mesh_config.positions = renderer_new_vb( vertices ); 458 mesh_config.indices = renderer_new_ib( indices ); 459 mesh_config.num_vertices = indices.n; 460 clipmap.gpu.cross = renderer_new_mesh( mesh_config ); 461 } 462 463 // generate seam mesh 464 { 465 MEMARENA_SCOPED_CHECKPOINT( &mem->persistent_arena ); 466 467 array< v3 > vertices = alloc_array< v3 >( &mem->persistent_arena, CLIPMAP_VERT_RESOLUTION * 4 ); 468 469 for( u32 i = 0; i < CLIPMAP_VERT_RESOLUTION; i++ ) { 470 vertices[ CLIPMAP_VERT_RESOLUTION * 0 + i ] = v3( i, 0, 0 ); 471 vertices[ CLIPMAP_VERT_RESOLUTION * 1 + i ] = v3( CLIPMAP_VERT_RESOLUTION, i, 0 ); 472 vertices[ CLIPMAP_VERT_RESOLUTION * 2 + i ] = v3( CLIPMAP_VERT_RESOLUTION - i, CLIPMAP_VERT_RESOLUTION, 0 ); 473 vertices[ CLIPMAP_VERT_RESOLUTION * 3 + i ] = v3( 0, CLIPMAP_VERT_RESOLUTION - i, 0 ); 474 } 475 476 array< u32 > indices = alloc_array< u32 >( &mem->persistent_arena, CLIPMAP_VERT_RESOLUTION * 6 ); 477 size_t n = 0; 478 479 for( u32 i = 0; i < CLIPMAP_VERT_RESOLUTION * 4; i += 2 ) { 480 indices[ n++ ] = i + 1; 481 indices[ n++ ] = i; 482 indices[ n++ ] = i + 2; 483 } 484 485 // make the last triangle wrap around 486 indices[ indices.n - 1 ] = 0; 487 488 ASSERT( n == indices.n ); 489 490 MeshConfig mesh_config; 491 mesh_config.positions = renderer_new_vb( vertices ); 492 mesh_config.indices = renderer_new_ib( indices ); 493 mesh_config.num_vertices = indices.n; 494 clipmap.gpu.seam = renderer_new_mesh( mesh_config ); 495 } 496 497 // generate skirt mesh 498 { 499 float scale = checked_cast< float >( u32( 1 ) << ( NUM_CLIPMAP_LEVELS - 1 ) ); 500 v2 base = -v2( checked_cast< float >( TILE_RESOLUTION << NUM_CLIPMAP_LEVELS ) ); 501 502 v2 clipmap_tl = base; 503 v2 clipmap_br = clipmap_tl + v2( CLIPMAP_RESOLUTION * scale ); 504 505 StaticArray< v3, 8 > vertices; 506 float big = 10000000.0; 507 vertices[ 0 ] = v3( -1, -1, 0 ) * big; 508 vertices[ 1 ] = v3( +1, -1, 0 ) * big; 509 vertices[ 2 ] = v3( -1, +1, 0 ) * big; 510 vertices[ 3 ] = v3( +1, +1, 0 ) * big; 511 vertices[ 4 ] = v3( clipmap_tl.x, clipmap_tl.y, 0 ); 512 vertices[ 5 ] = v3( clipmap_br.x, clipmap_tl.y, 0 ); 513 vertices[ 6 ] = v3( clipmap_tl.x, clipmap_br.y, 0 ); 514 vertices[ 7 ] = v3( clipmap_br.x, clipmap_br.y, 0 ); 515 516 u32 indices[] = { 517 0, 1, 4, 4, 1, 5, 518 1, 3, 5, 5, 3, 7, 519 3, 2, 7, 7, 2, 6, 520 4, 6, 0, 0, 6, 2, 521 }; 522 523 MeshConfig mesh_config; 524 mesh_config.positions = renderer_new_vb( vertices.ptr(), vertices.num_bytes() ); 525 mesh_config.indices = renderer_new_ib( indices, sizeof( indices ) ); 526 mesh_config.num_vertices = ARRAY_COUNT( indices ); 527 clipmap.gpu.skirt = renderer_new_mesh( mesh_config ); 528 } 529 530 game->pos = v3( 2056, 2056, 70 ); 531 game->pitch = 0; 532 game->yaw = 250; 533 game->sun_angle = 0.3f; 534 535 skybox_init( &game->skybox ); 536 537 load_trees( &mem->persistent_arena ); 538 539 sock = net_new_udp( NET_NONBLOCKING ); 540 541 if( !dns_first( "localhost", &server_addr ) ) { 542 FATAL( "dns_first" ); 543 } 544 server_addr.port = 13337; 545 } 546 547 static void draw_qt( MinMaxu32 aabb, const array< QuadTreeNode > nodes, size_t node_idx ) { 548 if( aabb.maxs.x - aabb.mins.x < 64 ) { 549 return; 550 } 551 552 v3 mins = v3( aabb.mins.x, aabb.mins.y, aabb.mins.z / 256.0f ); 553 v3 maxs = v3( aabb.maxs.x, aabb.maxs.y, aabb.maxs.z / 256.0f ); 554 immediate_aabb( mins, maxs, v4( 0, 0, 1, 1 ) ); 555 556 for( size_t i = 0; i < 4; i++ ) { 557 MinMaxu32 child_aabb = aabb.quadrant( i ).clamp_z( nodes[ node_idx * 4 + i + 1 ].min_z, nodes[ node_idx * 4 + i + 1 ].max_z ); 558 draw_qt( child_aabb, nodes, node_idx * 4 + i + 1 ); 559 } 560 } 561 562 static bool intervals_overlap( float a0, float a1, float b0, float b1 ) { 563 ASSERT( a0 <= a1 ); 564 ASSERT( b0 <= b1 ); 565 return a0 <= b1 && b0 <= a1; 566 } 567 568 static void recreate_framebuffers() { 569 renderer_delete_fb( msaa_fb ); 570 571 v2u32 window_size = get_window_size(); 572 573 TextureConfig texture_config; 574 texture_config.width = window_size.x; 575 texture_config.height = window_size.y; 576 texture_config.wrap = TEXWRAP_CLAMP; 577 578 FramebufferConfig fb_config; 579 580 texture_config.format = TEXFMT_RGB_U8_SRGB; 581 fb_config.textures[ OUTPUT_ALBEDO ].config = texture_config; 582 fb_config.textures[ OUTPUT_ALBEDO ].attachment = FB_COLOUR; 583 584 texture_config.format = TEXFMT_DEPTH; 585 fb_config.textures[ OUTPUT_DEPTH ].config = texture_config; 586 fb_config.textures[ OUTPUT_DEPTH ].attachment = FB_DEPTH; 587 588 fb_config.msaa_samples = 0; 589 590 msaa_fb = renderer_new_fb( fb_config ); 591 } 592 593 GAME_FRAME( game_frame ) { 594 if( input->resized ) { 595 recreate_framebuffers(); 596 } 597 598 float fb = float( input->keys[ KEY_W ] - input->keys[ KEY_S ] ); 599 float lr = float( input->keys[ KEY_D ] - input->keys[ KEY_A ] ); 600 float dz = float( input->keys[ KEY_SPACE ] - input->keys[ KEY_LEFTSHIFT ] ); 601 602 float dpitch = input->keys[ KEY_DOWNARROW ] - input->keys[ KEY_UPARROW ]; 603 float dyaw = input->keys[ KEY_LEFTARROW ] - input->keys[ KEY_RIGHTARROW ]; 604 605 dpitch = input->keys[ KEY_K ] - input->keys[ KEY_I ]; 606 dyaw += input->keys[ KEY_J ] - input->keys[ KEY_L ]; 607 608 if( input->keys[ KEY_M ] && input->key_edges[ KEY_M ] ) 609 game->draw_wireframe = !game->draw_wireframe; 610 if( input->keys[ KEY_T ] && input->key_edges[ KEY_T ] ) 611 game->draw_quadtree = !game->draw_quadtree; 612 if( input->keys[ KEY_N ] && input->key_edges[ KEY_N ] ) { 613 game->noclip = !game->noclip; 614 game->velocity = v3( 0 ); 615 game->on_ground = false; 616 } 617 if( input->keys[ KEY_C ] && input->key_edges[ KEY_C ] ) { 618 game->frozen = !game->frozen; 619 game->frozen_pos = game->pos; 620 } 621 622 dpitch -= float( input->mouse_dy * 0.25 ); 623 dyaw -= float( input->mouse_dx * 0.25 ); 624 625 game->pitch = clamp( game->pitch + dpitch * dt * 100, -89.9f, 89.9f ); 626 game->yaw += dyaw * dt * 100; 627 628 const float dsun = ( input->keys[ KEY_EQUALS ] - input->keys[ KEY_MINUS ] ) * dt * 0.25f; 629 game->sun_angle += dsun; 630 631 const v3 world_up = v3( 0, 0, 1 ); 632 v3 forward = v3_forward( game->pitch, game->yaw ); 633 v3 right = normalize( cross( forward, world_up ) ); 634 v3 up = normalize( cross( right, forward ) ); 635 636 // player movement 637 if( game->noclip ) { 638 const float speed = 100.0f; 639 game->pos += forward * dt * fb * speed; 640 game->pos += right * dt * lr * speed; 641 game->pos.z += dt * dz * speed; 642 } 643 else { 644 if( game->on_ground ) { 645 if( input->keys[ KEY_SPACE ] ) { 646 game->velocity.z = 9.8; 647 game->on_ground = false; 648 } 649 } 650 651 const float acceleration = 20.0f; 652 const float deceleration = 1.2f; 653 if( game->on_ground ) { 654 // apply friction 655 if( dot( game->velocity, game->velocity ) < 0.01f ) { 656 game->velocity = v3( 0 ); 657 } 658 else { 659 float current_speed = length( game->velocity ); 660 float drop = max( acceleration, current_speed ) * deceleration * dt; 661 float new_speed = max( 0.0f, current_speed - drop ); 662 game->velocity = new_speed * ( game->velocity / current_speed ); 663 } 664 665 if( fb != 0 || lr != 0 ) { 666 // apply acceleration 667 float max_speed = 5; 668 669 v3 forward_xy = v3_forward_xy( game->yaw ); 670 v3 right_xy = normalize( cross( forward_xy, world_up ) ); 671 v3 desired_direction = fb * forward_xy + lr * right_xy; 672 673 desired_direction = normalize( desired_direction ); 674 675 float speed_in_desired_direction = dot( game->velocity, desired_direction ); 676 677 if( speed_in_desired_direction < max_speed ) { 678 float accel_speed = min( acceleration * dt * max_speed, max_speed - speed_in_desired_direction ); 679 game->velocity += accel_speed * desired_direction; 680 } 681 } 682 } 683 684 int spins = 0; 685 float remaining_t = dt; 686 while( remaining_t > 0 && spins < 5 ) { 687 spins++; 688 if( game->on_ground ) { 689 const float step_height = 0.1f; 690 691 v3 low_start = game->pos; 692 v3 low_end = low_start + game->velocity * remaining_t; 693 694 v3 high_start = game->pos + v3( 0, 0, step_height ); 695 v3 high_end = high_start + game->velocity * remaining_t; 696 697 float low_t; 698 // initialise high_t to -1 so we never pick the high trace 699 // if we couldn't step up (fails the high_t > low_t test) 700 float high_t = -1; 701 v3 low_normal, high_normal; 702 703 bool low_hit = segment_vs_terrain( clipmap.quadtree, low_start, low_end, &low_t, &low_normal ); 704 bool high_hit = false; 705 706 bool step_up_hit = segment_vs_terrain( clipmap.quadtree, low_start, high_start, NULL, NULL ); 707 if( !step_up_hit ) { 708 high_hit = segment_vs_terrain( clipmap.quadtree, high_start, high_end, &high_t, &high_normal ); 709 } 710 711 v3 step_down_start; 712 float step_down_distance; 713 bool should_step_down = false; 714 715 if( high_t > low_t ) { 716 if( high_hit ) { 717 game->pos = high_start + game->velocity * high_t * remaining_t; 718 remaining_t = ( 1.0f - high_t ) * remaining_t; 719 } 720 else { 721 step_down_start = high_end; 722 step_down_distance = step_height * 2; 723 should_step_down = true; 724 remaining_t = 0; 725 } 726 } 727 else { 728 if( low_hit ) { 729 game->pos = low_start + game->velocity * low_t * remaining_t; 730 remaining_t = ( 1.0f - low_t ) * remaining_t; 731 } 732 else { 733 step_down_start = low_end; 734 step_down_distance = step_height; 735 should_step_down = true; 736 remaining_t = 0; 737 } 738 } 739 740 if( should_step_down ) { 741 float step_down_t; 742 v3 step_down_end = step_down_start - v3( 0, 0, step_down_distance ); 743 744 bool step_down_hit = segment_vs_terrain( clipmap.quadtree, step_down_start, step_down_end, &step_down_t, NULL ); 745 746 game->pos = lerp( step_down_start, step_down_t, step_down_end ); 747 game->on_ground = step_down_hit; 748 } 749 } 750 751 // TODO: this might be quite harmful 752 if( !game->on_ground ) { 753 const float gravity = -30.0f; 754 game->velocity += v3( 0, 0, gravity ) * remaining_t; 755 756 v3 start = game->pos; 757 v3 end = start + game->velocity * remaining_t; 758 float t; 759 v3 normal; 760 bool hit = segment_vs_terrain( clipmap.quadtree, start, end, &t, &normal ); 761 if( hit ) { 762 // TODO: check normal is flat enough, otherwise stay in the air 763 game->on_ground = true; 764 game->pos += game->velocity * t * remaining_t; 765 // TODO: truncate velocity 766 game->velocity.z = 0; 767 768 remaining_t = ( 1.0f - t ) * remaining_t; 769 } 770 else { 771 game->pos = end; 772 remaining_t = 0; 773 } 774 } 775 776 if( game->on_ground ) { 777 // move up 1mm so float imprecision doesn't make us fall through the world 778 game->pos.z += 0.001f; 779 } 780 } 781 } 782 783 v3 eye_pos = game->pos + v3( 0, 0, EYE_HEIGHT ); 784 m4 P = m4_perspective( VERTICAL_FOV, get_aspect_ratio(), NEAR_PLANE_DEPTH, FAR_PLANE_DEPTH ); 785 m4 Pinf = m4_perspective_inf( VERTICAL_FOV, get_aspect_ratio(), NEAR_PLANE_DEPTH ); 786 m4 V = m4_view( forward, right, up, eye_pos ); 787 m4 Vsky = m4_view( forward, right, up, v3( 0 ) ); 788 v3 sun_dir = v3( -cosf( game->sun_angle ), 0, sinf( game->sun_angle ) ); 789 790 renderer_begin_frame(); 791 792 u8 world_pass = renderer_add_pass( "Render world", msaa_fb, RENDERER_CLEAR_COLOUR_DO, RENDERER_CLEAR_DEPTH_DO ); 793 u8 ents_pass = renderer_add_pass( "Render entities", msaa_fb ); 794 u8 sky_pass = renderer_add_pass( "Render sky", msaa_fb ); 795 renderer_resolve_msaa( msaa_fb ); 796 u8 ui_pass = renderer_add_pass( "Render UI" ); 797 798 UniformBinding view_uniforms = renderer_uniforms( V, P, game->pos ); 799 UniformBinding inf_view_uniforms = renderer_uniforms( V, Pinf, game->pos ); 800 UniformBinding sun_uniforms = renderer_uniforms( sun_dir, game->sun_angle ); 801 802 StaticArray< UniformBinding, 4 > rotation_uniforms; 803 rotation_uniforms[ 0 ] = renderer_uniforms( m4_identity() ); 804 rotation_uniforms[ 1 ] = renderer_uniforms( m4_rotz270() ); 805 rotation_uniforms[ 2 ] = renderer_uniforms( m4_rotz90() ); 806 rotation_uniforms[ 3 ] = renderer_uniforms( m4_rotz180() ); 807 808 if( !game->frozen ) { 809 game->frozen_pos = game->pos; 810 } 811 812 // draw terrain 813 { 814 RenderState render_state; 815 render_state.pass = world_pass; 816 render_state.shader = get_shader( SHADER_CLIPMAP ); 817 render_state.set_uniform( "view", view_uniforms ); 818 render_state.set_uniform( "sun", sun_uniforms ); 819 render_state.set_texture( "heightmap", clipmap.gpu.heightmap ); 820 render_state.set_texture( "normalmap", clipmap.gpu.normalmap ); 821 render_state.set_texture( "horizonmap", clipmap.gpu.horizonmap ); 822 render_state.set_texture( "blue_noise", renderer_blue_noise() ); 823 render_state.wireframe = game->draw_wireframe; 824 825 // draw cross 826 { 827 v2 snapped_pos = floorf( game->frozen_pos.xy() ); 828 render_state.set_uniform( "model", rotation_uniforms[ 0 ] ); 829 render_state.set_uniform( "clipmap", renderer_uniforms( snapped_pos, 1.0f ) ); 830 renderer_draw_mesh( clipmap.gpu.cross, render_state ); 831 } 832 833 for( u32 l = 0; l < NUM_CLIPMAP_LEVELS; l++ ) { 834 float scale = checked_cast< float >( u32( 1 ) << l ); 835 v2 snapped_pos = floorf( game->frozen_pos.xy() / scale ) * scale; 836 837 // draw tiles 838 v2 tile_size = v2( checked_cast< float >( TILE_RESOLUTION << l ) ); 839 v2 base = snapped_pos - v2( checked_cast< float >( TILE_RESOLUTION << ( l + 1 ) ) ); 840 841 for( int x = 0; x < 4; x++ ) { 842 for( int y = 0; y < 4; y++ ) { 843 // draw a 4x4 set of tiles. cut out the middle 2x2 unless we're at the finest level 844 if( l != 0 && ( x == 1 || x == 2 ) && ( y == 1 || y == 2 ) ) { 845 continue; 846 } 847 848 v2 fill = v2( x >= 2 ? 1 : 0, y >= 2 ? 1 : 0 ) * scale; 849 v2 tile_tl = base + v2( x, y ) * tile_size + fill; 850 851 // draw a low poly tile if the tile is entirely outside the world 852 v2 tile_br = tile_tl + tile_size; 853 bool inside = true; 854 if( !intervals_overlap( tile_tl.x, tile_br.x, 0, clipmap.heightmap.w ) ) 855 inside = false; 856 if( !intervals_overlap( tile_tl.y, tile_br.y, 0, clipmap.heightmap.h ) ) 857 inside = false; 858 859 Mesh mesh = inside ? clipmap.gpu.tile : clipmap.gpu.empty_tile; 860 render_state.set_uniform( "model", rotation_uniforms[ 0 ] ); 861 render_state.set_uniform( "clipmap", renderer_uniforms( tile_tl, scale ) ); 862 renderer_draw_mesh( mesh, render_state ); 863 } 864 } 865 866 // draw filler 867 { 868 render_state.set_uniform( "model", rotation_uniforms[ 0 ] ); 869 render_state.set_uniform( "clipmap", renderer_uniforms( snapped_pos, scale ) ); 870 renderer_draw_mesh( clipmap.gpu.filler, render_state ); 871 } 872 873 if( l != NUM_CLIPMAP_LEVELS - 1 ) { 874 float next_scale = scale * 2.0f; 875 v2 next_snapped_pos = floorf( game->frozen_pos.xy() / next_scale ) * next_scale; 876 877 // draw trim 878 { 879 v2 tile_centre = snapped_pos + v2( scale * 0.5f ); 880 881 v2 d = game->frozen_pos.xy() - next_snapped_pos; 882 u32 r = 0; 883 r |= d.x >= scale ? 0 : 2; 884 r |= d.y >= scale ? 0 : 1; 885 886 render_state.set_uniform( "model", rotation_uniforms[ r ] ); 887 render_state.set_uniform( "clipmap", renderer_uniforms( tile_centre, scale ) ); 888 renderer_draw_mesh( clipmap.gpu.trim, render_state ); 889 } 890 891 // draw seam 892 { 893 v2 next_base = next_snapped_pos - v2( checked_cast< float >( TILE_RESOLUTION << ( l + 1 ) ) ); 894 895 render_state.set_uniform( "model", rotation_uniforms[ 0 ] ); 896 render_state.set_uniform( "clipmap", renderer_uniforms( next_base, scale ) ); 897 renderer_draw_mesh( clipmap.gpu.seam, render_state ); 898 } 899 } 900 } 901 } 902 903 // draw skirts 904 { 905 float scale = checked_cast< float >( u32( 1 ) << ( NUM_CLIPMAP_LEVELS - 1 ) ); 906 907 RenderState skirt_render_state; 908 skirt_render_state.pass = world_pass; 909 skirt_render_state.shader = get_shader( SHADER_CLIPMAP_SKIRT ); 910 skirt_render_state.set_uniform( "view", inf_view_uniforms ); 911 skirt_render_state.set_uniform( "sun", sun_uniforms ); 912 skirt_render_state.set_uniform( "clipmap_skirt", renderer_uniforms( scale ) ); 913 skirt_render_state.set_texture( "blue_noise", renderer_blue_noise() ); 914 skirt_render_state.wireframe = game->draw_wireframe; 915 renderer_draw_mesh( clipmap.gpu.skirt, skirt_render_state ); 916 } 917 918 // draw trees 919 { 920 RenderState render_state; 921 render_state.pass = ents_pass; 922 render_state.shader = get_shader( SHADER_TREE ); 923 render_state.set_uniform( "view", view_uniforms ); 924 render_state.set_uniform( "sun", renderer_uniforms( sun_dir ) ); 925 render_state.set_texture( "blue_noise", renderer_blue_noise() ); 926 927 renderer_draw_instances( tree_mesh, render_state, num_trees, trees_instance_data ); 928 } 929 930 // draw crosshair 931 { 932 const float aspect = get_aspect_ratio(); 933 const float crosshair_thickness = 0.0025f; 934 const float crosshair_length = 0.01f; 935 936 const v4 red( 1, 0, 0, 1 ); 937 immediate_triangle( 938 v3( -crosshair_length, crosshair_thickness, 0 ), 939 v3( -crosshair_length, -crosshair_thickness, 0 ), 940 v3( crosshair_length, crosshair_thickness, 0 ), 941 red 942 ); 943 immediate_triangle( 944 v3( crosshair_length, -crosshair_thickness, 0 ), 945 v3( crosshair_length, crosshair_thickness, 0 ), 946 v3( -crosshair_length, -crosshair_thickness, 0 ), 947 red 948 ); 949 immediate_triangle( 950 v3( crosshair_thickness / aspect, crosshair_length * aspect, 0 ), 951 v3( -crosshair_thickness / aspect, crosshair_length * aspect, 0 ), 952 v3( crosshair_thickness / aspect, -crosshair_length * aspect, 0 ), 953 red 954 ); 955 immediate_triangle( 956 v3( -crosshair_thickness / aspect, -crosshair_length * aspect, 0 ), 957 v3( crosshair_thickness / aspect, -crosshair_length * aspect, 0 ), 958 v3( -crosshair_thickness / aspect, crosshair_length * aspect, 0 ), 959 red 960 ); 961 RenderState render_state; 962 render_state.pass = ui_pass; 963 render_state.shader = get_shader( SHADER_UI ); 964 render_state.depth_func = DEPTHFUNC_ALWAYS; 965 immediate_render( render_state ); 966 } 967 968 // fireballs 969 if( input->keys[ KEY_F ] ) { 970 Fireball * fireball = fireballs.acquire(); 971 fireball->pos = game->pos + v3( 0.0f, 0.0f, EYE_HEIGHT ); 972 fireball->velocity = 128.0f * forward; 973 } 974 975 { 976 { 977 PROFILE_BLOCK( "Update fireballs" ); 978 for( size_t i = 0; i < fireballs.elems.n; i++ ) { 979 Fireball * fireball = &fireballs.elems[ i ]; 980 981 const float gravity = 30; 982 fireball->velocity.z -= dt * gravity; 983 984 Ray3 ray( fireball->pos, fireball->velocity ); 985 v3 end = fireball->pos + fireball->velocity * dt; 986 987 float t; 988 v3 normal; 989 if( segment_vs_terrain( clipmap.quadtree, fireball->pos, end, &t, &normal ) ) { 990 Explosion * explosion = explosions.acquire(); 991 explosion->pos = lerp( fireball->pos, t, end ); 992 explosion->created_at = current_time; 993 994 fireballs.release( fireball ); 995 i--; 996 } 997 else { 998 fireball->pos = end; 999 immediate_sphere( fireball->pos, 4, v4( 1, 0, 0, 1 ), 4 ); 1000 } 1001 } 1002 } 1003 1004 RenderState fireball_render_state; 1005 fireball_render_state.pass = ents_pass; 1006 fireball_render_state.shader = get_shader( SHADER_FLAT_VERTEX_COLOURS ); 1007 fireball_render_state.set_uniform( "view", view_uniforms ); 1008 immediate_render( fireball_render_state ); 1009 1010 for( size_t i = 0; i < explosions.elems.n; i++ ) { 1011 Explosion * explosion = &explosions.elems[ i ]; 1012 float t = float( current_time - explosion->created_at ); 1013 if( t < 0.5f ) { 1014 immediate_sphere( explosion->pos, t * 32.0f, v4( 1, 0.5, 0, 1 ), 4 ); 1015 } 1016 else { 1017 explosions.release( explosion ); 1018 i--; 1019 } 1020 } 1021 1022 RenderState explosion_render_state; 1023 explosion_render_state.pass = ents_pass; 1024 explosion_render_state.shader = get_shader( SHADER_FLAT_VERTEX_COLOURS ); 1025 explosion_render_state.set_uniform( "view", view_uniforms ); 1026 immediate_render( explosion_render_state ); 1027 } 1028 1029 skybox_render( &game->skybox, sky_pass, Vsky, Pinf, game->sun_angle, sun_dir ); 1030 1031 // networking 1032 { 1033 char buf[ 1400 ]; 1034 NetAddress recv_addr; 1035 size_t len; 1036 if( !net_tryrecv( sock, buf, sizeof( buf ), &recv_addr, &len ) ) { 1037 len = 0; 1038 } 1039 if( recv_addr != server_addr ) { 1040 len = 0; 1041 } 1042 1043 ReadStream rs( buf, len ); 1044 1045 if( connected && len > 0 ) { 1046 char write_buf[ 1400 ]; 1047 WriteStream ws( write_buf ); 1048 write( &ws, sid ); 1049 write( &ws, game->pos ); 1050 1051 net_send( sock, ws.start, ws.len(), server_addr ); 1052 1053 u16 player_count = read_u16( &rs ); 1054 ReadStreamCheckpoint rsc = rs.checkpoint(); 1055 1056 for( u16 i = 0; i < player_count; i++ ) { 1057 read_u64( &rs ); 1058 u8 player_state = read_u8( &rs ); 1059 if( player_state == 0 ) { 1060 v3 pos; 1061 read( &rs, &pos ); 1062 } 1063 } 1064 1065 if( rs.done() ) { 1066 rs.reset( &rsc ); 1067 for( u16 i = 0; i < player_count; i++ ) { 1068 u64 player_sid = read_u64( &rs ); 1069 u8 player_state = read_u8( &rs ); 1070 v3 pos; 1071 if( player_state == 0 ) { 1072 read( &rs, &pos ); 1073 } 1074 1075 Player * player = NULL; 1076 if( !sid_to_player.get( player_sid, &player ) && player_state == 0 ) { 1077 player = players.acquire(); 1078 ASSERT( player != NULL ); // TODO 1079 sid_to_player.add( player_sid, player ); 1080 player->sid = player_sid; 1081 } 1082 1083 if( player_state == 0 ) { 1084 player->pos = pos; 1085 } 1086 else if( player_state == 1 ) { 1087 ggprint( "{08x} disconnected\n", player->sid ); 1088 players.release( player ); 1089 } 1090 } 1091 1092 ASSERT( rs.done() ); 1093 } 1094 else { 1095 if( len > 0 ) { 1096 FATAL( "bad message from the server" ); 1097 } 1098 } 1099 } 1100 else if( len > 0 ) { 1101 sid = read_u64( &rs ); 1102 if( rs.ok ) { 1103 connected = true; 1104 } 1105 } 1106 1107 if( !connected && current_time - last_connection_attempt > 1.0 ) { 1108 char write_buf[ 1400 ]; 1109 WriteStream ws( write_buf ); 1110 1111 write_u64( &ws, ~u64( 0 ) ); 1112 write( &ws, game->pos ); 1113 1114 net_send( sock, ws.start, ws.len(), server_addr ); 1115 1116 last_connection_attempt = current_time; 1117 } 1118 1119 for( const Player & player : players ) { 1120 if( player.sid != sid ) { 1121 v3 mins = player.pos - v3( 0.4f, 0.4f, 0.0f ); 1122 v3 maxs = player.pos + v3( 0.4f, 0.4f, EYE_HEIGHT + 0.1f ); 1123 immediate_aabb( mins, maxs, v4( 1, 1, 0, 1 ) ); 1124 } 1125 } 1126 1127 RenderState impact_render_state; 1128 impact_render_state.pass = world_pass; 1129 impact_render_state.shader = get_shader( SHADER_FLAT_VERTEX_COLOURS ); 1130 impact_render_state.set_uniform( "view", view_uniforms ); 1131 immediate_render( impact_render_state ); 1132 } 1133 1134 if( game->draw_quadtree ) { 1135 v3u32 mins = v3u32( 0, 0, clipmap.quadtree.nodes[ 0 ].min_z ); 1136 v3u32 maxs = v3u32( clipmap.heightmap.w, clipmap.heightmap.h, clipmap.quadtree.nodes[ 0 ].max_z ); 1137 draw_qt( MinMaxu32( mins, maxs ), clipmap.quadtree.nodes, 0 ); 1138 1139 RenderState render_state; 1140 render_state.pass = world_pass; 1141 render_state.shader = get_shader( SHADER_WIREFRAME ); 1142 render_state.set_uniform( "view", view_uniforms ); 1143 render_state.wireframe = true; 1144 immediate_render( render_state ); 1145 } 1146 1147 // info text 1148 { 1149 const str< 128 > status( "Frame time: {.1}ms FPS: {.1} {}", dt * 1000.0f, 1.0f / dt, 1150 connected ? "connected!" : "connecting" ); 1151 draw_text( ui_pass, status.c_str(), 2, 2, 16.0f ); 1152 draw_text( ui_pass, str< 128 >( "pos = {.1} vel = {.1}", game->pos, game->velocity ).c_str(), 2, 20, 16 ); 1153 draw_text( ui_pass, str< 128 >( "drawcalls = {}, tris = {}", renderer_num_draw_calls(), renderer_num_vertices() / 3 ).c_str(), 2, 38, 16 ); 1154 } 1155 1156 renderer_end_frame(); 1157 }