1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789
mod sprite;
mod watcher;
pub use self::watcher::{BlocksOfInterest, FireplaceType, Interaction};
use sprite::{FilteredSpriteData, SpriteData, SpriteModelData, SpriteSpec};
use crate::{
mesh::{
greedy::{GreedyMesh, SpriteAtlasAllocator},
segment::generate_mesh_base_vol_sprite,
terrain::{generate_mesh, SUNLIGHT, SUNLIGHT_INV},
},
render::{
pipelines::{self, AtlasData, AtlasTextures},
AltIndices, CullingMode, FigureSpriteAtlasData, FirstPassDrawer, FluidVertex, GlobalModel,
Instances, LodData, Mesh, Model, RenderError, Renderer, SpriteDrawer,
SpriteGlobalsBindGroup, SpriteInstance, SpriteVertex, SpriteVerts, TerrainAtlasData,
TerrainLocals, TerrainShadowDrawer, TerrainVertex, SPRITE_VERT_PAGE_SIZE,
},
scene::terrain::sprite::SpriteModelConfig,
};
use super::{
camera::{self, Camera},
math, SceneData, RAIN_THRESHOLD,
};
use common::{
assets::{AssetExt, DotVoxAsset},
figure::Segment,
spiral::Spiral2d,
terrain::{Block, SpriteKind, TerrainChunk},
vol::{BaseVol, ReadVol, RectRasterableVol, SampleVol},
volumes::vol_grid_2d::{VolGrid2d, VolGrid2dError},
};
use common_base::{prof_span, span};
use core::{f32, fmt::Debug, marker::PhantomData, time::Duration};
use crossbeam_channel as channel;
use guillotiere::AtlasAllocator;
use hashbrown::HashMap;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use tracing::warn;
use treeculler::{BVol, Frustum, AABB};
use vek::*;
const SPRITE_SCALE: Vec3<f32> = Vec3::new(1.0 / 11.0, 1.0 / 11.0, 1.0 / 11.0);
pub const SPRITE_LOD_LEVELS: usize = 5;
// For rain occlusion we only need to render the closest chunks.
/// How many chunks are maximally rendered for rain occlusion.
pub const RAIN_OCCLUSION_CHUNKS: usize = 25;
#[derive(Clone, Copy, Debug)]
struct Visibility {
in_range: bool,
in_frustum: bool,
}
impl Visibility {
/// Should the chunk actually get rendered?
fn is_visible(&self) -> bool {
// Currently, we don't take into account in_range to allow all chunks to do
// pop-in. This isn't really a problem because we no longer have VD mist
// or anything like that. Also, we don't load chunks outside of the VD
// anyway so this literally just controls which chunks get actually
// rendered.
/* self.in_range && */
self.in_frustum
}
}
/// Type of closure used for light mapping.
type LightMapFn = Arc<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>;
pub struct TerrainChunkData {
// GPU data
load_time: f32,
opaque_model: Option<Model<TerrainVertex>>,
fluid_model: Option<Model<FluidVertex>>,
/// If this is `None`, this texture is not allocated in the current atlas,
/// and therefore there is no need to free its allocation.
atlas_alloc: Option<guillotiere::AllocId>,
/// The actual backing texture for this chunk. Use this for rendering
/// purposes. The texture is reference-counted, so it will be
/// automatically freed when no chunks are left that need it (though
/// shadow chunks will still keep it alive; we could deal with this by
/// making this an `Option`, but it probably isn't worth it since they
/// shouldn't be that much more nonlocal than regular chunks).
atlas_textures: Arc<AtlasTextures<pipelines::terrain::Locals, TerrainAtlasData>>,
light_map: LightMapFn,
glow_map: LightMapFn,
sprite_instances: [(Instances<SpriteInstance>, AltIndices); SPRITE_LOD_LEVELS],
locals: pipelines::terrain::BoundLocals,
pub blocks_of_interest: BlocksOfInterest,
visible: Visibility,
can_shadow_point: bool,
can_shadow_sun: bool,
z_bounds: (f32, f32),
sun_occluder_z_bounds: (f32, f32),
frustum_last_plane_index: u8,
alt_indices: AltIndices,
}
/// The depth at which the intermediate zone between underground and surface
/// begins
pub const SHALLOW_ALT: f32 = 24.0;
/// The depth at which the intermediate zone between underground and surface
/// ends
pub const DEEP_ALT: f32 = 96.0;
/// The depth below the surface altitude at which the camera switches from
/// displaying surface elements to underground elements
pub const UNDERGROUND_ALT: f32 = (SHALLOW_ALT + DEEP_ALT) * 0.5;
// The distance (in chunks) within which all levels of the chunks will be drawn
// to minimise cull-related popping.
const NEVER_CULL_DIST: i32 = 3;
#[derive(Copy, Clone)]
struct ChunkMeshState {
pos: Vec2<i32>,
started_tick: u64,
is_worker_active: bool,
// If this is set, we skip the actual meshing part of the update.
skip_remesh: bool,
}
/// Just the mesh part of a mesh worker response.
pub struct MeshWorkerResponseMesh {
z_bounds: (f32, f32),
sun_occluder_z_bounds: (f32, f32),
opaque_mesh: Mesh<TerrainVertex>,
fluid_mesh: Mesh<FluidVertex>,
atlas_texture_data: TerrainAtlasData,
atlas_size: Vec2<u16>,
light_map: LightMapFn,
glow_map: LightMapFn,
alt_indices: AltIndices,
}
/// A type produced by mesh worker threads corresponding to the position and
/// mesh of a chunk.
struct MeshWorkerResponse {
pos: Vec2<i32>,
sprite_instances: [(Vec<SpriteInstance>, AltIndices); SPRITE_LOD_LEVELS],
/// If None, this update was requested without meshing.
mesh: Option<MeshWorkerResponseMesh>,
started_tick: u64,
blocks_of_interest: BlocksOfInterest,
}
pub(super) fn get_sprite_instances<'a, I: 'a>(
lod_levels: &'a mut [I; SPRITE_LOD_LEVELS],
set_instance: impl Fn(&mut I, SpriteInstance, Vec3<i32>),
blocks: impl Iterator<Item = (Vec3<f32>, Block)>,
mut to_wpos: impl FnMut(Vec3<f32>) -> Vec3<i32>,
mut light_map: impl FnMut(Vec3<i32>) -> f32,
mut glow_map: impl FnMut(Vec3<i32>) -> f32,
sprite_data: &HashMap<SpriteKind, FilteredSpriteData>,
missing_sprite_placeholder: &SpriteData,
) {
prof_span!("extract sprite_instances");
for (rel_pos, block) in blocks {
let Some(sprite) = block.get_sprite() else {
continue;
};
// Short-circuit and skip hashmap interaction since this is every fluid block
// (including air)
if matches!(sprite, SpriteKind::Empty) {
continue;
}
let data = sprite_data
.get(&sprite)
.and_then(|filtered| filtered.for_block(&block))
.unwrap_or(missing_sprite_placeholder);
if data.variations.is_empty() {
continue;
}
let wpos = to_wpos(rel_pos);
let seed = (wpos.x as u64)
.wrapping_mul(3)
.wrapping_add((wpos.y as u64).wrapping_mul(7))
.wrapping_add((wpos.x as u64).wrapping_mul(wpos.y as u64)); // Awful PRNG
// % 4 is non uniform, take 7 and combine two lesser probable outcomes
let ori = (block.get_ori().unwrap_or((((seed % 7) + 1) / 2) as u8 * 2)) & 0b111;
// try to make the variation more uniform as the PRNG is highly unfair
let variation = match data.variations.len() {
1 => 0,
2 => (seed as usize % 4) / 3,
3 => (seed as usize % 5) / 2,
// for four use a different seed than for ori to not have them match always
4 => (((seed.wrapping_add(wpos.x as u64)) as usize % 7) + 1) / 2,
_ => seed as usize % data.variations.len(),
};
let variant = &data.variations[variation];
let light = light_map(wpos);
let glow = glow_map(wpos);
for (lod_level, model_data) in lod_levels.iter_mut().zip(variant) {
// TODO: worth precomputing the constant parts of this?
let mat = Mat4::identity()
// Scaling for different LOD resolutions
.scaled_3d(model_data.scale)
// Offset
.translated_3d(model_data.offset)
.scaled_3d(SPRITE_SCALE)
.rotated_z(f32::consts::PI * 0.25 * ori as f32)
.translated_3d(
rel_pos + Vec3::new(0.5, 0.5, 0.0)
);
// Add an instance for each page in the sprite model
for page in model_data.vert_pages.clone() {
// TODO: could be more efficient to create once outside this loop and clone
// while modifying vert_page?
let instance = SpriteInstance::new(
mat,
data.wind_sway,
model_data.scale.z,
rel_pos.as_(),
ori,
light,
glow,
page,
sprite.is_door(),
);
set_instance(lod_level, instance, wpos);
}
}
}
}
/// Function executed by worker threads dedicated to chunk meshing.
/// skip_remesh is either None (do the full remesh, including recomputing the
/// light map), or Some((light_map, glow_map)).
fn mesh_worker(
pos: Vec2<i32>,
z_bounds: (f32, f32),
skip_remesh: Option<(LightMapFn, LightMapFn)>,
started_tick: u64,
volume: <VolGrid2d<TerrainChunk> as SampleVol<Aabr<i32>>>::Sample,
max_texture_size: u16,
chunk: Arc<TerrainChunk>,
range: Aabb<i32>,
sprite_render_state: &SpriteRenderState,
) -> MeshWorkerResponse {
span!(_guard, "mesh_worker");
let blocks_of_interest = BlocksOfInterest::from_blocks(
chunk.iter_changed().map(|(pos, block)| (pos, *block)),
chunk.meta().river_velocity(),
chunk.meta().temp(),
chunk.meta().humidity(),
&*chunk,
);
let mesh;
let (light_map, glow_map) = if let Some((light_map, glow_map)) = &skip_remesh {
mesh = None;
(&**light_map, &**glow_map)
} else {
let (
opaque_mesh,
fluid_mesh,
_shadow_mesh,
(
bounds,
atlas_texture_data,
atlas_size,
light_map,
glow_map,
alt_indices,
sun_occluder_z_bounds,
),
) = generate_mesh(
&volume,
(
range,
Vec2::new(max_texture_size, max_texture_size),
&blocks_of_interest,
),
);
mesh = Some(MeshWorkerResponseMesh {
// TODO: Take sprite bounds into account somehow?
z_bounds: (bounds.min.z, bounds.max.z),
sun_occluder_z_bounds,
opaque_mesh,
fluid_mesh,
atlas_texture_data,
atlas_size,
light_map,
glow_map,
alt_indices,
});
// Pointer juggling so borrows work out.
let mesh = mesh.as_ref().unwrap();
(&*mesh.light_map, &*mesh.glow_map)
};
let to_wpos = |rel_pos: Vec3<f32>| {
Vec3::from(pos * TerrainChunk::RECT_SIZE.map(|e: u32| e as i32)) + rel_pos.as_()
};
MeshWorkerResponse {
pos,
// Extract sprite locations from volume
sprite_instances: {
prof_span!("extract sprite_instances");
let mut instances = [(); SPRITE_LOD_LEVELS].map(|()| {
(
Vec::new(), // Deep
Vec::new(), // Shallow
Vec::new(), // Surface
)
});
let (underground_alt, deep_alt) = volume
.get_key(volume.pos_key((range.min + range.max) / 2))
.map_or((0.0, 0.0), |c| {
(c.meta().alt() - SHALLOW_ALT, c.meta().alt() - DEEP_ALT)
});
get_sprite_instances(
&mut instances,
|(deep_level, shallow_level, surface_level), instance, wpos| {
if (wpos.z as f32) < deep_alt {
deep_level.push(instance);
} else if wpos.z as f32 > underground_alt {
surface_level.push(instance);
} else {
shallow_level.push(instance);
}
},
(0..TerrainChunk::RECT_SIZE.x as i32)
.flat_map(|x| {
(0..TerrainChunk::RECT_SIZE.y as i32).flat_map(move |y| {
(z_bounds.0 as i32..z_bounds.1 as i32)
.map(move |z| Vec3::new(x, y, z).as_())
})
})
.filter_map(|rel_pos| Some((rel_pos, *volume.get(to_wpos(rel_pos)).ok()?))),
to_wpos,
light_map,
glow_map,
&sprite_render_state.sprite_data,
&sprite_render_state.missing_sprite_placeholder,
);
instances.map(|(deep_level, shallow_level, surface_level)| {
let deep_end = deep_level.len();
let alt_indices = AltIndices {
deep_end,
underground_end: deep_end + shallow_level.len(),
};
(
deep_level
.into_iter()
.chain(shallow_level)
.chain(surface_level)
.collect(),
alt_indices,
)
})
},
mesh,
blocks_of_interest,
started_tick,
}
}
pub struct Terrain<V: RectRasterableVol = TerrainChunk> {
/// This is always the *current* atlas into which data is being allocated.
/// Once an atlas is too full to allocate the next texture, we always
/// allocate a fresh texture and start allocating into that. Trying to
/// keep more than one texture available for allocation doesn't seem
/// worth it, because our allocation patterns are heavily spatial (so all
/// data allocated around the same time should have a very similar lifetime,
/// even in pathological cases). As a result, fragmentation effects
/// should be minimal.
///
/// TODO: Consider "moving GC" style allocation to deal with spatial
/// fragmentation effects due to odd texture sizes, which in some cases
/// might significantly reduce the number of textures we need for
/// particularly difficult locations.
atlas: AtlasAllocator,
chunks: HashMap<Vec2<i32>, TerrainChunkData>,
/// Temporary storage for dead chunks that might still be shadowing chunks
/// in view. We wait until either the chunk definitely cannot be
/// shadowing anything the player can see, the chunk comes back into
/// view, or for daylight to end, before removing it (whichever comes
/// first).
///
/// Note that these chunks are not complete; for example, they are missing
/// texture data (they still currently hold onto a reference to their
/// backing texture, but it generally can't be trusted for rendering
/// purposes).
shadow_chunks: Vec<(Vec2<i32>, TerrainChunkData)>,
/* /// Secondary index into the terrain chunk table, used to sort through chunks by z index from
/// the top down.
z_index_down: BTreeSet<Vec3<i32>>,
/// Secondary index into the terrain chunk table, used to sort through chunks by z index from
/// the bottom up.
z_index_up: BTreeSet<Vec3<i32>>, */
// The mpsc sender and receiver used for talking to meshing worker threads.
// We keep the sender component for no reason other than to clone it and send it to new
// workers.
mesh_send_tmp: channel::Sender<MeshWorkerResponse>,
mesh_recv: channel::Receiver<MeshWorkerResponse>,
mesh_todo: HashMap<Vec2<i32>, ChunkMeshState>,
mesh_todos_active: Arc<AtomicU64>,
mesh_recv_overflow: f32,
// GPU data
// Maps sprite kind + variant to data detailing how to render it
pub(super) sprite_render_state: Arc<SpriteRenderState>,
pub(super) sprite_globals: SpriteGlobalsBindGroup,
/// As stated previously, this is always the very latest texture into which
/// we allocate. Code cannot assume that this is the assigned texture
/// for any particular chunk; look at the `texture` field in
/// `TerrainChunkData` for that.
atlas_textures: Arc<AtlasTextures<pipelines::terrain::Locals, TerrainAtlasData>>,
phantom: PhantomData<V>,
}
impl TerrainChunkData {
pub fn can_shadow_sun(&self) -> bool { self.visible.is_visible() || self.can_shadow_sun }
}
pub(super) struct SpriteRenderState {
// TODO: This could be an `AssetHandle<SpriteSpec>`, to get hot-reloading. However, this would
// need to regenerate `sprite_data` and `sprite_atlas_textures`, and re-run
// `get_sprite_instances` for any meshed chunks.
//pub sprite_config: Arc<SpriteSpec>,
// Maps sprite kind + variant to data detailing how to render it
pub sprite_data: HashMap<SpriteKind, FilteredSpriteData>,
pub missing_sprite_placeholder: SpriteData,
pub sprite_atlas_textures: AtlasTextures<pipelines::sprite::Locals, FigureSpriteAtlasData>,
}
#[derive(Clone)]
pub struct SpriteRenderContext {
pub(super) state: Arc<SpriteRenderState>,
pub(super) sprite_verts_buffer: Arc<SpriteVerts>,
}
pub type SpriteRenderContextLazy = Box<dyn FnMut(&mut Renderer) -> SpriteRenderContext>;
impl SpriteRenderContext {
pub fn new(renderer: &mut Renderer) -> SpriteRenderContextLazy {
let max_texture_size = renderer.max_texture_size();
struct SpriteWorkerResponse {
//sprite_config: Arc<SpriteSpec>,
sprite_data: HashMap<SpriteKind, FilteredSpriteData>,
missing_sprite_placeholder: SpriteData,
sprite_atlas_texture_data: FigureSpriteAtlasData,
sprite_atlas_size: Vec2<u16>,
sprite_mesh: Mesh<SpriteVertex>,
}
let join_handle = std::thread::spawn(move || {
prof_span!("mesh all sprites");
// Load all the sprite config data.
let sprite_config =
Arc::<SpriteSpec>::load_expect("voxygen.voxel.sprite_manifest").cloned();
let max_size = Vec2::from(u16::try_from(max_texture_size).unwrap_or(u16::MAX));
let mut greedy = GreedyMesh::<FigureSpriteAtlasData, SpriteAtlasAllocator>::new(
max_size,
crate::mesh::greedy::sprite_config(),
);
let mut sprite_mesh = Mesh::new();
let mut config_to_data = |sprite_model_config: &_| {
let SpriteModelConfig {
model,
offset,
lod_axes,
} = sprite_model_config;
let scaled = [1.0, 0.8, 0.6, 0.4, 0.2];
let offset = Vec3::from(*offset);
let lod_axes = Vec3::from(*lod_axes);
let model = DotVoxAsset::load_expect(model);
let zero = Vec3::zero();
let model = &model.read().0;
let model_size = if let Some(model) = model.models.first() {
let dot_vox::Size { x, y, z } = model.size;
Vec3::new(x, y, z)
} else {
zero
};
let max_model_size = Vec3::new(31.0, 31.0, 63.0);
let model_scale = max_model_size.map2(model_size, |max_sz: f32, cur_sz| {
let scale = max_sz / max_sz.max(cur_sz as f32);
if scale < 1.0 && (cur_sz as f32 * scale).ceil() > max_sz {
scale - 0.001
} else {
scale
}
});
prof_span!(guard, "mesh sprite");
let lod_sprite_data = scaled.map(|lod_scale_orig| {
let lod_scale = model_scale
* if lod_scale_orig == 1.0 {
Vec3::broadcast(1.0)
} else {
lod_axes * lod_scale_orig
+ lod_axes.map(|e| if e == 0.0 { 1.0 } else { 0.0 })
};
// Get starting page count of opaque mesh
let start_page_num =
sprite_mesh.vertices().len() / SPRITE_VERT_PAGE_SIZE as usize;
// Mesh generation exclusively acts using side effects; it
// has no interesting return value, but updates the mesh.
generate_mesh_base_vol_sprite(
Segment::from_vox_model_index(model, 0).scaled_by(lod_scale),
(&mut greedy, &mut sprite_mesh, false),
offset.map(|e: f32| e.floor()) * lod_scale,
);
// Get the number of pages after the model was meshed
let end_page_num =
(sprite_mesh.vertices().len() + SPRITE_VERT_PAGE_SIZE as usize - 1)
/ SPRITE_VERT_PAGE_SIZE as usize;
// Fill the current last page up with degenerate verts
sprite_mesh.vertices_mut_vec().resize_with(
end_page_num * SPRITE_VERT_PAGE_SIZE as usize,
SpriteVertex::default,
);
let sprite_scale = Vec3::one() / lod_scale;
SpriteModelData {
vert_pages: start_page_num as u32..end_page_num as u32,
scale: sprite_scale,
offset: offset.map(|e| e.rem_euclid(1.0)),
}
});
drop(guard);
lod_sprite_data
};
let sprite_data = sprite_config.map_to_data(&mut config_to_data);
// TODO: test appearance of this
let missing_sprite_placeholder = SpriteData {
variations: vec![config_to_data(&SpriteModelConfig {
model: "voxygen.voxel.not_found".into(),
offset: (-5.5, -5.5, 0.0),
lod_axes: (1.0, 1.0, 1.0),
})]
.into(),
wind_sway: 1.0,
};
let (sprite_atlas_texture_data, sprite_atlas_size) = {
prof_span!("finalize");
greedy.finalize()
};
SpriteWorkerResponse {
//sprite_config,
sprite_data,
missing_sprite_placeholder,
sprite_atlas_texture_data,
sprite_atlas_size,
sprite_mesh,
}
});
let init = core::cell::OnceCell::new();
let mut join_handle = Some(join_handle);
let mut closure = move |renderer: &mut Renderer| {
// The second unwrap can only fail if the sprite meshing thread panics, which
// implies that our sprite assets either were not found or did not
// satisfy the size requirements for meshing, both of which are
// considered invariant violations.
let SpriteWorkerResponse {
//sprite_config,
sprite_data,
missing_sprite_placeholder,
sprite_atlas_texture_data,
sprite_atlas_size,
sprite_mesh,
} = join_handle
.take()
.expect(
"Closure should only be called once (in a `OnceCell::get_or_init`) in the \
absence of caught panics!",
)
.join()
.unwrap();
let [sprite_col_lights] =
sprite_atlas_texture_data.create_textures(renderer, sprite_atlas_size);
let sprite_atlas_textures = renderer.sprite_bind_atlas_textures(sprite_col_lights);
// Write sprite model to a 1D texture
let sprite_verts_buffer = renderer.create_sprite_verts(sprite_mesh);
Self {
state: Arc::new(SpriteRenderState {
// TODO: these are all Arcs, would it makes sense to factor out the Arc?
//sprite_config: Arc::clone(&sprite_config),
sprite_data,
missing_sprite_placeholder,
sprite_atlas_textures,
}),
sprite_verts_buffer: Arc::new(sprite_verts_buffer),
}
};
Box::new(move |renderer| init.get_or_init(|| closure(renderer)).clone())
}
}
impl<V: RectRasterableVol> Terrain<V> {
pub fn new(
renderer: &mut Renderer,
global_model: &GlobalModel,
lod_data: &LodData,
sprite_render_context: SpriteRenderContext,
) -> Self {
// Create a new mpsc (Multiple Produced, Single Consumer) pair for communicating
// with worker threads that are meshing chunks.
let (send, recv) = channel::unbounded();
let (atlas, atlas_textures) =
Self::make_atlas(renderer).expect("Failed to create atlas texture");
Self {
atlas,
chunks: HashMap::default(),
shadow_chunks: Vec::default(),
mesh_send_tmp: send,
mesh_recv: recv,
mesh_todo: HashMap::default(),
mesh_todos_active: Arc::new(AtomicU64::new(0)),
mesh_recv_overflow: 0.0,
sprite_render_state: sprite_render_context.state,
sprite_globals: renderer.bind_sprite_globals(
global_model,
lod_data,
&sprite_render_context.sprite_verts_buffer,
),
atlas_textures: Arc::new(atlas_textures),
phantom: PhantomData,
}
}
fn make_atlas(
renderer: &mut Renderer,
) -> Result<
(
AtlasAllocator,
AtlasTextures<pipelines::terrain::Locals, TerrainAtlasData>,
),
RenderError,
> {
span!(_guard, "make_atlas", "Terrain::make_atlas");
let max_texture_size = renderer.max_texture_size();
let atlas_size = guillotiere::Size::new(max_texture_size as i32, max_texture_size as i32);
let atlas = AtlasAllocator::with_options(atlas_size, &guillotiere::AllocatorOptions {
// TODO: Verify some good empirical constants.
small_size_threshold: 128,
large_size_threshold: 1024,
..guillotiere::AllocatorOptions::default()
});
let [col_lights, kinds] = [wgpu::TextureFormat::Rgba8Unorm, wgpu::TextureFormat::R8Uint]
.map(|fmt| {
renderer.create_texture_raw(
&wgpu::TextureDescriptor {
label: Some("Terrain atlas texture"),
size: wgpu::Extent3d {
width: max_texture_size,
height: max_texture_size,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: fmt,
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
&wgpu::TextureViewDescriptor {
label: Some("Terrain atlas texture view"),
format: Some(fmt),
dimension: Some(wgpu::TextureViewDimension::D2),
aspect: wgpu::TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
},
&wgpu::SamplerDescriptor {
label: Some("Terrain atlas sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
},
)
});
let textures = renderer.terrain_bind_atlas_textures(col_lights, kinds);
Ok((atlas, textures))
}
fn remove_chunk_meta(&mut self, _pos: Vec2<i32>, chunk: &TerrainChunkData) {
// No need to free the allocation if the chunk is not allocated in the current
// atlas, since we don't bother tracking it at that point.
if let Some(atlas_alloc) = chunk.atlas_alloc {
self.atlas.deallocate(atlas_alloc);
}
/* let (zmin, zmax) = chunk.z_bounds;
self.z_index_up.remove(Vec3::from(zmin, pos.x, pos.y));
self.z_index_down.remove(Vec3::from(zmax, pos.x, pos.y)); */
}
fn insert_chunk(&mut self, pos: Vec2<i32>, chunk: TerrainChunkData) {
if let Some(old) = self.chunks.insert(pos, chunk) {
self.remove_chunk_meta(pos, &old);
}
/* let (zmin, zmax) = chunk.z_bounds;
self.z_index_up.insert(Vec3::from(zmin, pos.x, pos.y));
self.z_index_down.insert(Vec3::from(zmax, pos.x, pos.y)); */
}
fn remove_chunk(&mut self, pos: Vec2<i32>) {
if let Some(chunk) = self.chunks.remove(&pos) {
self.remove_chunk_meta(pos, &chunk);
// Temporarily remember dead chunks for shadowing purposes.
self.shadow_chunks.push((pos, chunk));
}
if let Some(_todo) = self.mesh_todo.remove(&pos) {
//Do nothing on todo mesh removal.
}
}
/// Find the light level (sunlight) at the given world position.
pub fn light_at_wpos(&self, wpos: Vec3<i32>) -> f32 {
let chunk_pos = Vec2::from(wpos).map2(TerrainChunk::RECT_SIZE, |e: i32, sz| {
e.div_euclid(sz as i32)
});
self.chunks
.get(&chunk_pos)
.map(|c| (c.light_map)(wpos))
.unwrap_or(1.0)
}
/// Determine whether a given block change actually require remeshing.
///
/// Returns (skip_color, skip_lights) where
///
/// skip_color means no textures were recolored (i.e. this was a sprite only
/// change).
///
/// skip_lights means no remeshing or relighting was required
/// (i.e. the block opacity / lighting info / block kind didn't change).
fn skip_remesh(old_block: Block, new_block: Block) -> (bool, bool) {
let same_mesh =
// Both blocks are of the same opacity and same liquidity (since these are what we use
// to determine mesh boundaries).
new_block.is_liquid() == old_block.is_liquid() &&
new_block.is_opaque() == old_block.is_opaque();
let skip_lights = same_mesh &&
// Block glow and sunlight handling are the same (so we don't have to redo
// lighting).
new_block.get_glow() == old_block.get_glow() &&
new_block.get_max_sunlight() == old_block.get_max_sunlight();
let skip_color = same_mesh &&
// Both blocks are uncolored
!new_block.has_color() && !old_block.has_color();
(skip_color, skip_lights)
}
/// Find the glow level (light from lamps) at the given world position.
pub fn glow_at_wpos(&self, wpos: Vec3<i32>) -> f32 {
let chunk_pos = Vec2::from(wpos).map2(TerrainChunk::RECT_SIZE, |e: i32, sz| {
e.div_euclid(sz as i32)
});
self.chunks
.get(&chunk_pos)
.map(|c| (c.glow_map)(wpos))
.unwrap_or(0.0)
}
pub fn glow_normal_at_wpos(&self, wpos: Vec3<f32>) -> (Vec3<f32>, f32) {
let wpos_chunk = wpos.xy().map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
(e as i32).div_euclid(sz as i32)
});
const AMBIANCE: f32 = 0.15; // 0-1, the proportion of light that should illuminate the rear of an object
let (bias, total) = Spiral2d::new()
.take(9)
.flat_map(|rpos| {
let chunk_pos = wpos_chunk + rpos;
self.chunks
.get(&chunk_pos)
.into_iter()
.flat_map(|c| c.blocks_of_interest.lights.iter())
.filter_map(move |(lpos, level)| {
if (*lpos - wpos_chunk).map(|e| e.abs()).reduce_min() < SUNLIGHT as i32 + 2
{
Some((
Vec3::<i32>::from(
chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32),
) + *lpos,
level,
))
} else {
None
}
})
})
.fold(
(Vec3::broadcast(0.001), 0.0),
|(bias, total), (lpos, level)| {
let rpos = lpos.map(|e| e as f32 + 0.5) - wpos;
let level = (*level as f32 - rpos.magnitude()).max(0.0) * SUNLIGHT_INV;
(
bias + rpos.try_normalized().unwrap_or_else(Vec3::zero) * level,
total + level,
)
},
);
let bias_factor = bias.magnitude() * (1.0 - AMBIANCE) / total.max(0.001);
(
bias.try_normalized().unwrap_or_else(Vec3::zero) * bias_factor.powf(0.5),
self.glow_at_wpos(wpos.map(|e| e.floor() as i32)),
)
}
/// Maintain terrain data. To be called once per tick.
///
/// The returned visible bounding volumes take into account the current
/// camera position (i.e: when underground, surface structures will be
/// culled from the volume).
pub fn maintain(
&mut self,
renderer: &mut Renderer,
scene_data: &SceneData,
focus_pos: Vec3<f32>,
loaded_distance: f32,
camera: &Camera,
) -> (
Aabb<f32>,
Vec<math::Vec3<f32>>,
math::Aabr<f32>,
Vec<math::Vec3<f32>>,
math::Aabr<f32>,
) {
let camera::Dependents {
view_mat,
proj_mat_treeculler,
cam_pos,
..
} = camera.dependents();
// Remove any models for chunks that have been recently removed.
// Note: Does this before adding to todo list just in case removed chunks were
// replaced with new chunks (although this would probably be recorded as
// modified chunks)
for &pos in &scene_data.state.terrain_changes().removed_chunks {
self.remove_chunk(pos);
// Remove neighbors from meshing todo
for i in -1..2 {
for j in -1..2 {
if i != 0 || j != 0 {
self.mesh_todo.remove(&(pos + Vec2::new(i, j)));
}
}
}
}
span!(_guard, "maintain", "Terrain::maintain");
let current_tick = scene_data.tick;
let current_time = scene_data.state.get_time();
// The visible bounding box of all chunks, not including culled regions
let mut visible_bounding_box: Option<Aabb<f32>> = None;
// Add any recently created or changed chunks to the list of chunks to be
// meshed.
span!(guard, "Add new/modified chunks to mesh todo list");
for (modified, pos) in scene_data
.state
.terrain_changes()
.modified_chunks
.iter()
.map(|c| (true, c))
.chain(
scene_data
.state
.terrain_changes()
.new_chunks
.iter()
.map(|c| (false, c)),
)
{
// TODO: ANOTHER PROBLEM HERE!
// What happens if the block on the edge of a chunk gets modified? We need to
// spawn a mesh worker to remesh its neighbour(s) too since their
// ambient occlusion and face elision information changes too!
for i in -1..2 {
for j in -1..2 {
let pos = pos + Vec2::new(i, j);
if !(self.chunks.contains_key(&pos) || self.mesh_todo.contains_key(&pos))
|| modified
{
let mut neighbours = true;
for i in -1..2 {
for j in -1..2 {
neighbours &= scene_data
.state
.terrain()
.contains_key_real(pos + Vec2::new(i, j));
}
}
if neighbours {
self.mesh_todo.insert(pos, ChunkMeshState {
pos,
started_tick: current_tick,
is_worker_active: false,
skip_remesh: false,
});
}
}
}
}
}
drop(guard);
// Add the chunks belonging to recently changed blocks to the list of chunks to
// be meshed
span!(guard, "Add chunks with modified blocks to mesh todo list");
// TODO: would be useful if modified blocks were grouped by chunk
for (&pos, &old_block) in scene_data.state.terrain_changes().modified_blocks.iter() {
// terrain_changes() are both set and applied during the same tick on the
// client, so the current state is the new state and modified_blocks
// stores the old state.
let new_block = scene_data.state.get_block(pos);
let (skip_color, skip_lights) = if let Some(new_block) = new_block {
Self::skip_remesh(old_block, new_block)
} else {
// The block coordinates of a modified block should be in bounds, since they are
// only retained if setting the block was successful during the state tick in
// client. So this is definitely a bug, but we can recover safely by just
// conservatively doing a full remesh in this case, rather than crashing the
// game.
warn!(
"Invariant violation: pos={:?} should be a valid block position. This is a \
bug; please contact the developers if you see this error message!",
pos
);
(false, false)
};
// Currently, we can only skip remeshing if both lights and
// colors don't need to be reworked.
let skip_remesh = skip_color && skip_lights;
// TODO: Be cleverer about this to avoid remeshing all neighbours. There are a
// few things that can create an 'effect at a distance'. These are
// as follows:
// - A glowing block is added or removed, thereby causing a lighting
// recalculation proportional to its glow radius.
// - An opaque block that was blocking sunlight from entering a cavity is
// removed (or added) thereby
// changing the way that sunlight propagates into the cavity.
//
// We can and should be cleverer about this, but it's non-trivial. For now, we
// don't remesh if only a block color changed or a sprite was
// altered in a way that doesn't affect its glow, but we make no
// attempt to do smarter cavity checking (to see if altering the
// block changed the sunlight neighbors could get).
// let block_effect_radius = block.get_glow().unwrap_or(0).max(1);
let block_effect_radius = crate::mesh::terrain::MAX_LIGHT_DIST;
// Handle block changes on chunk borders
// Remesh all neighbours because we have complex lighting now
// TODO: if lighting is on the server this can be updated to only remesh when
// lighting changes in that neighbouring chunk or if the block
// change was on the border
for x in -1..2 {
for y in -1..2 {
let neighbour_pos = pos + Vec3::new(x, y, 0) * block_effect_radius;
let neighbour_chunk_pos = scene_data.state.terrain().pos_key(neighbour_pos);
if skip_lights && !(x == 0 && y == 0) {
// We don't need to remesh neighboring chunks if this block change doesn't
// require relighting.
continue;
}
// Only remesh if this chunk has all its neighbors
let mut neighbours = true;
for i in -1..2 {
for j in -1..2 {
neighbours &= scene_data
.state
.terrain()
.contains_key_real(neighbour_chunk_pos + Vec2::new(i, j));
}
}
if neighbours {
let todo =
self.mesh_todo
.entry(neighbour_chunk_pos)
.or_insert(ChunkMeshState {
pos: neighbour_chunk_pos,
started_tick: current_tick,
is_worker_active: false,
skip_remesh,
});
// Make sure not to skip remeshing a chunk if it already had to be
// fully meshed for other reasons. Even if the mesh is currently active
// (so relighting would be redundant), we currently have to remesh
// everything unless the previous mesh was also able to skip remeshing,
// since otherwise the active remesh is computing new lighting values
// that we don't have yet.
todo.skip_remesh &= skip_remesh;
todo.is_worker_active = false;
todo.started_tick = current_tick;
}
}
}
}
drop(guard);
// Limit ourselves to u16::MAX even if larger textures are supported.
let max_texture_size = renderer.max_texture_size();
let meshing_cores = match num_cpus::get() as u64 {
n if n < 4 => 1,
n if n < 8 => n - 3,
n => n - 4,
};
span!(guard, "Queue meshing from todo list");
let mesh_focus_pos = focus_pos.map(|e| e.trunc()).xy().as_::<i64>();
//TODO: this is actually no loop, it just runs for a single entry because of
// the `min_by_key`. Evaluate actually looping here
while let Some((todo, chunk)) = self
.mesh_todo
.values_mut()
.filter(|todo| !todo.is_worker_active)
.min_by_key(|todo| ((todo.pos.as_::<i64>() * TerrainChunk::RECT_SIZE.as_::<i64>()).distance_squared(mesh_focus_pos), todo.started_tick))
// Find a reference to the actual `TerrainChunk` we're meshing
.and_then(|todo| {
let pos = todo.pos;
Some((todo, scene_data.state
.terrain()
.get_key_arc(pos)
.cloned()
.or_else(|| {
warn!("Invariant violation: a chunk whose neighbors have not been fetched was found in the todo list,
which could halt meshing entirely.");
None
})?))
})
{
if self.mesh_todos_active.load(Ordering::Relaxed) > meshing_cores {
break;
}
// like ambient occlusion and edge elision, we also need the borders
// of the chunk's neighbours too (hence the `- 1` and `+ 1`).
let aabr = Aabr {
min: todo
.pos
.map2(VolGrid2d::<V>::chunk_size(), |e, sz| e * sz as i32 - 1),
max: todo.pos.map2(VolGrid2d::<V>::chunk_size(), |e, sz| {
(e + 1) * sz as i32 + 1
}),
};
// Copy out the chunk data we need to perform the meshing. We do this by taking
// a sample of the terrain that includes both the chunk we want and
// its neighbours.
let volume = match scene_data.state.terrain().sample(aabr) {
Ok(sample) => sample, /* TODO: Ensure that all of the chunk's neighbours still
* exist to avoid buggy shadow borders */
// Either this chunk or its neighbours doesn't yet exist, so we keep it in the
// queue to be processed at a later date when we have its neighbours.
Err(VolGrid2dError::NoSuchChunk) => {
continue;
},
_ => panic!("Unhandled edge case"),
};
// The region to actually mesh
let min_z = volume
.iter()
.fold(i32::MAX, |min, (_, chunk)| chunk.get_min_z().min(min));
let max_z = volume
.iter()
.fold(i32::MIN, |max, (_, chunk)| chunk.get_max_z().max(max));
let aabb = Aabb {
min: Vec3::from(aabr.min) + Vec3::unit_z() * (min_z - 2),
max: Vec3::from(aabr.max) + Vec3::unit_z() * (max_z + 2),
};
// Clone various things so that they can be moved into the thread.
let send = self.mesh_send_tmp.clone();
let pos = todo.pos;
let chunks = &self.chunks;
let skip_remesh = todo
.skip_remesh
.then_some(())
.and_then(|_| chunks.get(&pos))
.map(|chunk| (Arc::clone(&chunk.light_map), Arc::clone(&chunk.glow_map)));
// Queue the worker thread.
let started_tick = todo.started_tick;
let sprite_render_state = Arc::clone(&self.sprite_render_state);
let cnt = Arc::clone(&self.mesh_todos_active);
cnt.fetch_add(1, Ordering::Relaxed);
scene_data
.state
.slow_job_pool()
.spawn("TERRAIN_MESHING", move || {
let _ = send.send(mesh_worker(
pos,
(min_z as f32, max_z as f32),
skip_remesh,
started_tick,
volume,
max_texture_size as u16,
chunk,
aabb,
&sprite_render_state,
));
cnt.fetch_sub(1, Ordering::Relaxed);
});
todo.is_worker_active = true;
}
drop(guard);
// Receive a chunk mesh from a worker thread and upload it to the GPU, then
// store it. Vary the rate at which we pull items out to correlate with the
// framerate, preventing tail latency.
span!(guard, "Get/upload meshed chunk");
const CHUNKS_PER_SECOND: f32 = 240.0;
let recv_count =
scene_data.state.get_delta_time() * CHUNKS_PER_SECOND + self.mesh_recv_overflow;
self.mesh_recv_overflow = recv_count.fract();
let incoming_chunks =
std::iter::from_fn(|| self.mesh_recv.recv_timeout(Duration::new(0, 0)).ok())
.take(recv_count.floor() as usize)
.collect::<Vec<_>>(); // Avoid ownership issue
for response in incoming_chunks {
match self.mesh_todo.get(&response.pos) {
// It's the mesh we want, insert the newly finished model into the terrain model
// data structure (convert the mesh to a model first of course).
Some(todo) if response.started_tick <= todo.started_tick => {
let started_tick = todo.started_tick;
let sprite_instances =
response.sprite_instances.map(|(instances, alt_indices)| {
(renderer.create_instances(&instances), alt_indices)
});
if let Some(mesh) = response.mesh {
// Full update, insert the whole chunk.
let load_time = self
.chunks
.get(&response.pos)
.map(|chunk| chunk.load_time)
.unwrap_or(current_time as f32);
// TODO: Allocate new atlas on allocation failure.
let atlas = &mut self.atlas;
let chunks = &mut self.chunks;
let atlas_textures = &mut self.atlas_textures;
let alloc_size = guillotiere::Size::new(
i32::from(mesh.atlas_size.x),
i32::from(mesh.atlas_size.y),
);
let allocation = atlas.allocate(alloc_size).unwrap_or_else(|| {
// Atlas allocation failure: try allocating a new texture and atlas.
let (new_atlas, new_atlas_textures) =
Self::make_atlas(renderer).expect("Failed to create atlas texture");
// We reset the atlas and clear allocations from existing chunks,
// even though we haven't yet
// checked whether the new allocation can fit in
// the texture. This is reasonable because we don't have a fallback
// if a single chunk can't fit in an empty atlas of maximum size.
//
// TODO: Consider attempting defragmentation first rather than just
// always moving everything into the new chunk.
chunks.iter_mut().for_each(|(_, chunk)| {
chunk.atlas_alloc = None;
});
*atlas = new_atlas;
*atlas_textures = Arc::new(new_atlas_textures);
atlas
.allocate(alloc_size)
.expect("Chunk data does not fit in a texture of maximum size.")
});
// NOTE: Cast is safe since the origin was a u16.
let atlas_offs = Vec2::new(
allocation.rectangle.min.x as u32,
allocation.rectangle.min.y as u32,
);
// Update col_lights texture
renderer.update_texture(
&atlas_textures.textures[0],
atlas_offs.into_array(),
mesh.atlas_size.as_().into_array(),
&mesh.atlas_texture_data.col_lights,
);
// Update kinds texture
renderer.update_texture(
&atlas_textures.textures[1],
atlas_offs.into_array(),
mesh.atlas_size.as_().into_array(),
&mesh.atlas_texture_data.kinds,
);
self.insert_chunk(response.pos, TerrainChunkData {
load_time,
opaque_model: renderer.create_model(&mesh.opaque_mesh),
fluid_model: renderer.create_model(&mesh.fluid_mesh),
atlas_alloc: Some(allocation.id),
atlas_textures: Arc::clone(&self.atlas_textures),
light_map: mesh.light_map,
glow_map: mesh.glow_map,
sprite_instances,
locals: renderer.create_terrain_bound_locals(&[TerrainLocals::new(
Vec3::from(
response.pos.map2(VolGrid2d::<V>::chunk_size(), |e, sz| {
e as f32 * sz as f32
}),
),
Quaternion::identity(),
atlas_offs,
load_time,
)]),
visible: Visibility {
in_range: false,
in_frustum: false,
},
can_shadow_point: false,
can_shadow_sun: false,
blocks_of_interest: response.blocks_of_interest,
z_bounds: mesh.z_bounds,
sun_occluder_z_bounds: mesh.sun_occluder_z_bounds,
frustum_last_plane_index: 0,
alt_indices: mesh.alt_indices,
});
} else if let Some(chunk) = self.chunks.get_mut(&response.pos) {
// There was an update that didn't require a remesh (probably related to
// non-glowing sprites) so we just update those.
chunk.sprite_instances = sprite_instances;
chunk.blocks_of_interest = response.blocks_of_interest;
}
if response.started_tick == started_tick {
self.mesh_todo.remove(&response.pos);
}
},
// Chunk must have been removed, or it was spawned on an old tick. Drop the mesh
// since it's either out of date or no longer needed.
Some(_todo) => {},
None => {},
}
}
drop(guard);
// Construct view frustum
span!(guard, "Construct view frustum");
let focus_off = focus_pos.map(|e| e.trunc());
let frustum = Frustum::from_modelview_projection(
(proj_mat_treeculler * view_mat * Mat4::translation_3d(-focus_off)).into_col_arrays(),
);
drop(guard);
// Update chunk visibility
span!(guard, "Update chunk visibility");
let chunk_sz = V::RECT_SIZE.x as f32;
for (pos, chunk) in &mut self.chunks {
let chunk_pos = pos.as_::<f32>() * chunk_sz;
chunk.can_shadow_sun = false;
// Limit focus_pos to chunk bounds and ensure the chunk is within the fog
// boundary
let nearest_in_chunk = Vec2::from(focus_pos).clamped(chunk_pos, chunk_pos + chunk_sz);
let distance_2 = Vec2::<f32>::from(focus_pos).distance_squared(nearest_in_chunk);
let in_range = distance_2 < loaded_distance.powi(2);
chunk.visible.in_range = in_range;
// Ensure the chunk is within the view frustum
let chunk_min = [chunk_pos.x, chunk_pos.y, chunk.z_bounds.0];
let chunk_max = [
chunk_pos.x + chunk_sz,
chunk_pos.y + chunk_sz,
chunk.sun_occluder_z_bounds.1,
];
let (in_frustum, last_plane_index) = AABB::new(chunk_min, chunk_max)
.coherent_test_against_frustum(&frustum, chunk.frustum_last_plane_index);
chunk.frustum_last_plane_index = last_plane_index;
chunk.visible.in_frustum = in_frustum;
let chunk_area = Aabr {
min: chunk_pos,
max: chunk_pos + chunk_sz,
};
if in_frustum {
let visible_box = Aabb {
min: chunk_area.min.with_z(chunk.sun_occluder_z_bounds.0),
max: chunk_area.max.with_z(chunk.sun_occluder_z_bounds.1),
};
visible_bounding_box = visible_bounding_box
.map(|e| e.union(visible_box))
.or(Some(visible_box));
}
// FIXME: Hack that only works when only the lantern casts point shadows
// (and hardcodes the shadow distance). Should ideally exist per-light, too.
chunk.can_shadow_point = distance_2 < (128.0 * 128.0);
}
drop(guard);
span!(guard, "Shadow magic");
// PSRs: potential shadow receivers
let visible_bounding_box = visible_bounding_box.unwrap_or(Aabb {
min: focus_pos - 2.0,
max: focus_pos + 2.0,
});
let inv_proj_view =
math::Mat4::from_col_arrays((proj_mat_treeculler * view_mat).into_col_arrays())
.as_::<f64>()
.inverted();
// PSCs: Potential shadow casters
let ray_direction = scene_data.get_sun_dir();
let collides_with_aabr = |a: math::Aabb<f32>, b: math::Aabr<f32>| {
let min = math::Vec4::new(a.min.x, a.min.y, b.min.x, b.min.y);
let max = math::Vec4::new(b.max.x, b.max.y, a.max.x, a.max.y);
#[cfg(feature = "simd")]
return min.partial_cmple_simd(max).reduce_and();
#[cfg(not(feature = "simd"))]
return min.partial_cmple(&max).reduce_and();
};
let (visible_light_volume, visible_psr_bounds) = if ray_direction.z < 0.0
&& renderer.pipeline_modes().shadow.is_map()
{
let visible_bounding_box = math::Aabb::<f32> {
min: math::Vec3::from(visible_bounding_box.min - focus_off),
max: math::Vec3::from(visible_bounding_box.max - focus_off),
};
let focus_off = math::Vec3::from(focus_off);
let visible_bounds_fine = visible_bounding_box.as_::<f64>();
let ray_direction = math::Vec3::<f32>::from(ray_direction);
// NOTE: We use proj_mat_treeculler here because
// calc_focused_light_volume_points makes the assumption that the
// near plane lies before the far plane.
let visible_light_volume = math::calc_focused_light_volume_points(
inv_proj_view,
ray_direction.as_::<f64>(),
visible_bounds_fine,
1e-6,
)
.map(|v| v.as_::<f32>())
.collect::<Vec<_>>();
let up: math::Vec3<f32> = { math::Vec3::unit_y() };
let cam_pos = math::Vec3::from(cam_pos);
let ray_mat = math::Mat4::look_at_rh(cam_pos, cam_pos + ray_direction, up);
let visible_bounds = math::Aabr::from(math::fit_psr(
ray_mat,
visible_light_volume.iter().copied(),
|p| p,
));
let ray_mat = ray_mat * math::Mat4::translation_3d(-focus_off);
let can_shadow_sun = |pos: Vec2<i32>, chunk: &TerrainChunkData| {
let chunk_pos = pos.as_::<f32>() * chunk_sz;
// Ensure the chunk is within the PSR set.
let chunk_box = math::Aabb {
min: math::Vec3::new(chunk_pos.x, chunk_pos.y, chunk.z_bounds.0),
max: math::Vec3::new(
chunk_pos.x + chunk_sz,
chunk_pos.y + chunk_sz,
chunk.z_bounds.1,
),
};
let chunk_from_light = math::fit_psr(
ray_mat,
math::aabb_to_points(chunk_box).iter().copied(),
|p| p,
);
collides_with_aabr(chunk_from_light, visible_bounds)
};
// Handle potential shadow casters (chunks that aren't visible, but are still in
// range) to see if they could cast shadows.
self.chunks.iter_mut()
// NOTE: We deliberately avoid doing this computation for chunks we already know
// are visible, since by definition they'll always intersect the visible view
// frustum.
.filter(|chunk| !chunk.1.visible.in_frustum)
.for_each(|(&pos, chunk)| {
chunk.can_shadow_sun = can_shadow_sun(pos, chunk);
});
// Handle dead chunks that we kept around only to make sure shadows don't blink
// out when a chunk disappears.
//
// If the sun can currently cast shadows, we retain only those shadow chunks
// that both: 1. have not been replaced by a real chunk instance,
// and 2. are currently potential shadow casters (as witnessed by
// `can_shadow_sun` returning true).
//
// NOTE: Please make sure this runs *after* any code that could insert a chunk!
// Otherwise we may end up with multiple instances of the chunk trying to cast
// shadows at the same time.
let chunks = &self.chunks;
self.shadow_chunks
.retain(|(pos, chunk)| !chunks.contains_key(pos) && can_shadow_sun(*pos, chunk));
(visible_light_volume, visible_bounds)
} else {
// There's no daylight or no shadows, so there's no reason to keep any
// shadow chunks around.
self.shadow_chunks.clear();
(Vec::new(), math::Aabr {
min: math::Vec2::zero(),
max: math::Vec2::zero(),
})
};
drop(guard);
span!(guard, "Rain occlusion magic");
// Check if there is rain near the camera
let max_weather = scene_data
.state
.max_weather_near(focus_off.xy() + cam_pos.xy());
let (visible_occlusion_volume, visible_por_bounds) = if max_weather.rain > RAIN_THRESHOLD {
let visible_bounding_box = math::Aabb::<f32> {
min: math::Vec3::from(visible_bounding_box.min - focus_off),
max: math::Vec3::from(visible_bounding_box.max - focus_off),
};
let visible_bounds_fine = math::Aabb {
min: visible_bounding_box.min.as_::<f64>(),
max: visible_bounding_box.max.as_::<f64>(),
};
let weather = scene_data.client.weather_at_player();
let ray_direction = math::Vec3::<f32>::from(weather.rain_vel().normalized());
// NOTE: We use proj_mat_treeculler here because
// calc_focused_light_volume_points makes the assumption that the
// near plane lies before the far plane.
let visible_volume = math::calc_focused_light_volume_points(
inv_proj_view,
ray_direction.as_::<f64>(),
visible_bounds_fine,
1e-6,
)
.map(|v| v.as_::<f32>())
.collect::<Vec<_>>();
let cam_pos = math::Vec3::from(cam_pos);
let ray_mat =
math::Mat4::look_at_rh(cam_pos, cam_pos + ray_direction, math::Vec3::unit_y());
let visible_bounds = math::Aabr::from(math::fit_psr(
ray_mat,
visible_volume.iter().copied(),
|p| p,
));
(visible_volume, visible_bounds)
} else {
(Vec::new(), math::Aabr::default())
};
drop(guard);
(
visible_bounding_box,
visible_light_volume,
visible_psr_bounds,
visible_occlusion_volume,
visible_por_bounds,
)
}
pub fn get(&self, chunk_key: Vec2<i32>) -> Option<&TerrainChunkData> {
self.chunks.get(&chunk_key)
}
pub fn chunk_count(&self) -> usize { self.chunks.len() }
pub fn visible_chunk_count(&self) -> usize {
self.chunks
.iter()
.filter(|(_, c)| c.visible.is_visible())
.count()
}
pub fn shadow_chunk_count(&self) -> usize { self.shadow_chunks.len() }
pub fn render_shadows<'a>(
&'a self,
drawer: &mut TerrainShadowDrawer<'_, 'a>,
focus_pos: Vec3<f32>,
culling_mode: CullingMode,
) {
span!(_guard, "render_shadows", "Terrain::render_shadows");
let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
(e as i32).div_euclid(sz as i32)
});
let chunk_iter = Spiral2d::new()
.filter_map(|rpos| {
let pos = focus_chunk + rpos;
self.chunks.get(&pos)
})
.take(self.chunks.len());
// Directed shadows
//
// NOTE: We also render shadows for dead chunks that were found to still be
// potential shadow casters, to avoid shadows suddenly disappearing at
// very steep sun angles (e.g. sunrise / sunset).
chunk_iter
.filter(|chunk| chunk.can_shadow_sun())
.chain(self.shadow_chunks.iter().map(|(_, chunk)| chunk))
.filter_map(|chunk| {
Some((
chunk.opaque_model.as_ref()?,
&chunk.locals,
&chunk.alt_indices,
))
})
.for_each(|(model, locals, alt_indices)| {
drawer.draw(model, locals, alt_indices, culling_mode)
});
}
pub fn render_rain_occlusion<'a>(
&'a self,
drawer: &mut TerrainShadowDrawer<'_, 'a>,
focus_pos: Vec3<f32>,
) {
span!(_guard, "render_occlusion", "Terrain::render_occlusion");
let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
(e as i32).div_euclid(sz as i32)
});
let chunk_iter = Spiral2d::new()
.filter_map(|rpos| {
let pos = focus_chunk + rpos;
self.chunks.get(&pos)
})
.take(self.chunks.len().min(RAIN_OCCLUSION_CHUNKS));
chunk_iter
// Find a way to keep this?
// .filter(|chunk| chunk.can_shadow_sun())
.filter_map(|chunk| Some((
chunk
.opaque_model
.as_ref()?,
&chunk.locals,
&chunk.alt_indices,
)))
.for_each(|(model, locals, alt_indices)| drawer.draw(model, locals, alt_indices, CullingMode::None));
}
pub fn chunks_for_point_shadows(
&self,
focus_pos: Vec3<f32>,
) -> impl Clone
+ Iterator<
Item = (
&Model<pipelines::terrain::Vertex>,
&pipelines::terrain::BoundLocals,
),
> {
let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
(e as i32).div_euclid(sz as i32)
});
let chunk_iter = Spiral2d::new()
.filter_map(move |rpos| {
let pos = focus_chunk + rpos;
self.chunks.get(&pos)
})
.take(self.chunks.len());
// Point shadows
//
// NOTE: We don't bother retaining chunks unless they cast sun shadows, so we
// don't use `shadow_chunks` here.
chunk_iter
.filter(|chunk| chunk.can_shadow_point)
.filter_map(|chunk| {
chunk
.opaque_model
.as_ref()
.map(|model| (model, &chunk.locals))
})
}
pub fn render<'a>(
&'a self,
drawer: &mut FirstPassDrawer<'a>,
focus_pos: Vec3<f32>,
culling_mode: CullingMode,
) {
span!(_guard, "render", "Terrain::render");
let mut drawer = drawer.draw_terrain();
let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
(e as i32).div_euclid(sz as i32)
});
Spiral2d::new()
.filter_map(|rpos| {
let pos = focus_chunk + rpos;
Some((rpos, self.chunks.get(&pos)?))
})
.take(self.chunks.len())
.filter(|(_, chunk)| chunk.visible.is_visible())
.filter_map(|(rpos, chunk)| {
Some((
rpos,
chunk.opaque_model.as_ref()?,
&chunk.atlas_textures,
&chunk.locals,
&chunk.alt_indices,
))
})
.for_each(|(rpos, model, atlas_textures, locals, alt_indices)| {
// Always draw all of close chunks to avoid terrain 'popping'
let culling_mode = if rpos.magnitude_squared() < NEVER_CULL_DIST.pow(2) {
CullingMode::None
} else {
culling_mode
};
drawer.draw(model, atlas_textures, locals, alt_indices, culling_mode)
});
}
pub fn render_sprites<'a>(
&'a self,
sprite_drawer: &mut SpriteDrawer<'_, 'a>,
focus_pos: Vec3<f32>,
cam_pos: Vec3<f32>,
sprite_render_distance: f32,
culling_mode: CullingMode,
) {
span!(_guard, "render_sprites", "Terrain::render_sprites");
let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
(e as i32).div_euclid(sz as i32)
});
// Avoid switching textures
let chunk_iter = Spiral2d::new()
.filter_map(|rpos| {
let pos = focus_chunk + rpos;
Some((rpos, pos, self.chunks.get(&pos)?))
})
.take(self.chunks.len());
let chunk_size = V::RECT_SIZE.map(|e| e as f32);
let sprite_low_detail_distance = sprite_render_distance * 0.75;
let sprite_mid_detail_distance = sprite_render_distance * 0.5;
let sprite_hid_detail_distance = sprite_render_distance * 0.35;
let sprite_high_detail_distance = sprite_render_distance * 0.15;
chunk_iter
.clone()
.filter(|(_, _, c)| c.visible.is_visible())
.for_each(|(rpos, pos, chunk)| {
// Skip chunk if it has no sprites
if chunk.sprite_instances[0].0.count() == 0 {
return;
}
let chunk_center = pos.map2(chunk_size, |e, sz| (e as f32 + 0.5) * sz);
let focus_dist_sqrd = Vec2::from(focus_pos).distance_squared(chunk_center);
let dist_sqrd = Aabr {
min: chunk_center - chunk_size * 0.5,
max: chunk_center + chunk_size * 0.5,
}
.projected_point(cam_pos.xy())
.distance_squared(cam_pos.xy());
if focus_dist_sqrd < sprite_render_distance.powi(2) {
let lod_level = if dist_sqrd < sprite_high_detail_distance.powi(2) {
0
} else if dist_sqrd < sprite_hid_detail_distance.powi(2) {
1
} else if dist_sqrd < sprite_mid_detail_distance.powi(2) {
2
} else if dist_sqrd < sprite_low_detail_distance.powi(2) {
3
} else {
4
};
// Always draw all of close chunks to avoid terrain 'popping'
let culling_mode = if rpos.magnitude_squared() < NEVER_CULL_DIST.pow(2) {
CullingMode::None
} else {
culling_mode
};
sprite_drawer.draw(
&chunk.locals,
&chunk.sprite_instances[lod_level].0,
&chunk.sprite_instances[lod_level].1,
culling_mode,
);
}
});
}
pub fn render_translucent<'a>(
&'a self,
drawer: &mut FirstPassDrawer<'a>,
focus_pos: Vec3<f32>,
) {
span!(_guard, "render_translucent", "Terrain::render_translucent");
let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
(e as i32).div_euclid(sz as i32)
});
// Avoid switching textures
let chunk_iter = Spiral2d::new()
.filter_map(|rpos| {
let pos = focus_chunk + rpos;
self.chunks.get(&pos).map(|c| (pos, c))
})
.take(self.chunks.len());
// Translucent
span!(guard, "Fluid chunks");
let mut fluid_drawer = drawer.draw_fluid();
chunk_iter
.filter(|(_, chunk)| chunk.visible.is_visible())
.filter_map(|(_, chunk)| {
chunk
.fluid_model
.as_ref()
.map(|model| (model, &chunk.locals))
})
.collect::<Vec<_>>()
.into_iter()
.rev() // Render back-to-front
.for_each(|(model, locals)| {
fluid_drawer.draw(
model,
locals,
)
});
drop(fluid_drawer);
drop(guard);
}
}