alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
root crates client src render terrain.rs
//! Section meshes on the GPU: a buffer per section, a table of them that `terrain.slang` culls
//! and draws from, a shared quad index buffer and background meshing.
use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::mpsc::{self, Receiver, Sender};
use bytemuck::{Pod, Zeroable};
use common::world::ChunkPos;
use common::world::chunk::HEIGHT;
use glam::{DVec3, IVec3, Mat4, Vec3, Vec4};
use rustc_hash::{FxHashMap, FxHashSet};
use super::mesher::{self, GROUPS, MAX_QUADS, SectionMesh};
use super::staging::Staging;
use crate::gpu::vk::*;
use crate::gpu::{Buffer, Device, MemoryLocation};
use crate::world::ClientWorld;
pub const SECTIONS_PER_COLUMN: i32 = (HEIGHT / 16) as i32;
const _: () = assert!(SECTIONS_PER_COLUMN <= u8::BITS as i32);
const INITIAL_SLOTS: u32 = 1 << 12;
/// Draws a section adds to a layer at most: runs of visible facings among the seven groups.
const DRAWS_PER_SECTION: u64 = 4;
const COMMAND_SIZE: u64 = size_of::<VkDrawIndexedIndirectCommand>() as u64;
const RECORD_SIZE: u64 = size_of::<SectionRecord>() as u64;
/// The work buffer's counters: solid and cutout draws, sections drawn this frame, then the
/// section box test's `VkDrawIndirectCommand`.
pub const HEADER: [u32; 8] = [0, 0, 0, 0, 0, 1, 0, 0];
pub const HEADER_SIZE: u64 = size_of::<[u32; 8]>() as u64;
pub const DRAWN_OFFSET: u64 = 8;
pub const BOX_DRAW_OFFSET: u64 = 16;
/// Frames between reorderings while the camera stays in its section and sections change.
const REORDER_FRAMES: u64 = 30;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct SectionPos {
pub x: i32,
pub y: i32,
pub z: i32,
}
impl SectionPos {
fn new(column: ChunkPos, y: i32) -> Self {
let (x, z) = (column.x, column.z);
Self { x, y, z }
}
fn column(self) -> ChunkPos {
ChunkPos::new(self.x, self.z)
}
fn min_corner(self) -> DVec3 {
DVec3::new(
f64::from(self.x * 16),
f64::from(self.y * 16),
f64::from(self.z * 16),
)
}
}
/// A section's row in the table, `Section` in `terrain.slang`.
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct SectionRecord {
position: [i32; 3],
visible: u32,
was_visible: u32,
/// Bounds, then facing planes, see [`SectionMesh`].
extents: [i8; 12],
shapes: VkDeviceAddress,
lights: VkDeviceAddress,
tints: VkDeviceAddress,
faces: VkDeviceAddress,
counts: [u32; GROUPS],
/// Faces in all groups; 0 marks a free slot.
total: u32,
}
/// Push data of `cullMain`.
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct CullPush {
planes: [[f32; 4]; 5],
eye_fraction: [f32; 4],
/// The camera relative to the eye.
camera: [f32; 4],
eye_block: [i32; 4],
sections: VkDeviceAddress,
order: VkDeviceAddress,
counters: VkDeviceAddress,
tested: VkDeviceAddress,
commands: VkDeviceAddress,
translucent: VkDeviceAddress,
translucent_commands: VkDeviceAddress,
count: u32,
capacity: u32,
mode: u32,
_padding: u32,
}
impl CullPush {
/// The push data for one pass over `count` items.
pub fn pass(self, mode: CullMode, count: u32) -> Self {
Self {
mode: mode as u32,
count,
..self
}
}
}
/// The culling passes, see `cullMain`.
#[derive(Clone, Copy)]
pub enum CullMode {
/// Draws what was visible last frame and lists the boxes to test.
Previous = 0,
/// Draws what the box test newly found visible.
Revealed = 1,
/// Fills the translucent draws, back to front.
Translucent = 2,
}
/// The eye split into whole blocks and a fraction, as the shaders take it.
pub fn split_eye(eye: DVec3) -> ([i32; 4], [f32; 4]) {
let block = eye.floor();
(
block.as_ivec3().extend(0).to_array(),
(eye - block).as_vec3().extend(0.0).to_array(),
)
}
struct GpuMesh {
buffer: Buffer,
slot: u32,
}
struct MeshResult {
pos: SectionPos,
version: u64,
mesh: SectionMesh,
}
/// The device buffers sized by the slot capacity.
struct Tables {
capacity: u32,
sections: Buffer,
order: Buffer,
/// [`HEADER`], tested slots, solid then cutout draws, then translucent draws.
work: Buffer,
}
impl Tables {
fn new(device: &Arc<Device>, capacity: u32) -> crate::gpu::Result<Self> {
let usage = VkBufferUsageFlags2::TRANSFER_DST;
let slots = u64::from(capacity);
let work_usage =
usage | VkBufferUsageFlags2::TRANSFER_SRC | VkBufferUsageFlags2::INDIRECT_BUFFER;
let work_size =
HEADER_SIZE + slots * 4 + (2 * DRAWS_PER_SECTION + 1) * slots * COMMAND_SIZE;
Ok(Self {
capacity,
sections: Buffer::new(device, slots * RECORD_SIZE, usage, MemoryLocation::GpuOnly)?,
order: Buffer::new(device, slots * 4, usage, MemoryLocation::GpuOnly)?,
work: Buffer::new(device, work_size, work_usage, MemoryLocation::GpuOnly)?,
})
}
fn draws_per_layer(&self) -> u64 {
DRAWS_PER_SECTION * u64::from(self.capacity)
}
fn tested_offset(&self) -> u64 {
HEADER_SIZE
}
/// Solid draws for layer 0, cutout draws for layer 1.
fn commands_offset(&self, layer: u64) -> u64 {
HEADER_SIZE + u64::from(self.capacity) * 4 + layer * self.draws_per_layer() * COMMAND_SIZE
}
fn translucent_offset(&self) -> u64 {
self.commands_offset(2)
}
}
/// Copies [`Terrain::upload`] records: into each new mesh buffer, and into the section table and
/// culling order.
#[derive(Default)]
pub struct TerrainCopies {
pub meshes: Vec<(VkBuffer, VkBufferCopy)>,
pub sections: Vec<VkBufferCopy>,
pub order: Vec<VkBufferCopy>,
}
pub struct Terrain {
device: Arc<Device>,
indices: Buffer,
tables: Tables,
/// Buffers no longer referenced, with the last frame that may read them.
retired: VecDeque<(i64, Buffer)>,
/// Meshed sections; `None` for those without geometry.
meshes: FxHashMap<SectionPos, Option<GpuMesh>>,
/// The section table as uploaded, by slot.
records: Vec<SectionRecord>,
free_slots: Vec<u32>,
dirty_slots: Vec<u32>,
/// Every slot is uploaded again, after the table grew.
dirty_all: bool,
/// Slots in the order `cullMain` goes through them, near to far when sorted.
order: Vec<u32>,
in_order: Vec<bool>,
/// Entries from this index on are not uploaded yet.
order_uploaded: usize,
/// The camera section and frame of the last sort, and whether slots changed since.
sorted: (IVec3, u64),
order_changed: bool,
translucent: FxHashSet<SectionPos>,
versions: FxHashMap<SectionPos, u64>,
next_version: u64,
building: FxHashSet<SectionPos>,
/// Sections to mesh, as bits of their y per column.
dirty: FxHashMap<ChunkPos, u8>,
in_flight: usize,
/// `WorldRenderer.chunksUpdated`: meshes uploaded since the last [`Self::take_updates`].
updates: u32,
waiting: VecDeque<MeshResult>,
results_sender: Sender<MeshResult>,
results: Receiver<MeshResult>,
}
impl Terrain {
pub fn new(device: &Arc<Device>) -> crate::gpu::Result<Self> {
let quad_indices: Vec<u32> = (0..MAX_QUADS as u32)
.flat_map(|q| [0, 1, 2, 0, 2, 3].map(|i| q * 4 + i))
.collect();
let size = size_of_val(quad_indices.as_slice()) as u64;
let usage = VkBufferUsageFlags2::INDEX_BUFFER | VkBufferUsageFlags2::TRANSFER_DST;
let indices = Buffer::new(device, size, usage, MemoryLocation::GpuOnly)?;
let usage = VkBufferUsageFlags2::TRANSFER_SRC;
let mut staging = Buffer::new(device, size, usage, MemoryLocation::CpuToGpu)?;
staging.write(0, bytemuck::cast_slice(&quad_indices))?;
device.submit_now(|command_buffer| {
let region = VkBufferCopy {
srcOffset: 0,
dstOffset: 0,
size,
};
let (source, target) = (staging.handle(), indices.handle());
let commands = &device.commands;
unsafe { commands.vkCmdCopyBuffer(command_buffer, source, target, 1, ®ion) };
})?;
let (results_sender, results) = mpsc::channel();
Ok(Self {
device: device.clone(),
indices,
tables: Tables::new(device, INITIAL_SLOTS)?,
retired: VecDeque::new(),
meshes: FxHashMap::default(),
records: Vec::new(),
free_slots: Vec::new(),
dirty_slots: Vec::new(),
dirty_all: false,
order: Vec::new(),
in_order: Vec::new(),
order_uploaded: 0,
sorted: (IVec3::MAX, 0),
order_changed: false,
translucent: FxHashSet::default(),
versions: FxHashMap::default(),
next_version: 0,
building: FxHashSet::default(),
dirty: FxHashMap::default(),
in_flight: 0,
updates: 0,
waiting: VecDeque::new(),
results_sender,
results,
})
}
pub fn column_ready(&self, pos: ChunkPos) -> bool {
(0..SECTIONS_PER_COLUMN).all(|y| self.meshes.contains_key(&SectionPos::new(pos, y)))
}
pub fn clear(&mut self, frame: u64) {
let sections: Vec<_> = self.meshes.keys().copied().collect();
for section in sections {
self.remove(section, frame);
}
self.versions.clear();
self.dirty.clear();
self.waiting.clear();
}
pub fn rebuild(&mut self) {
let sections: Vec<_> = self.versions.keys().copied().collect();
for section in sections {
self.mark_section_dirty(section.column(), section.y);
}
}
pub fn index_buffer(&self) -> VkBuffer {
self.indices.handle()
}
/// Queues every section of a column for (re)meshing.
pub fn mark_column_dirty(&mut self, pos: ChunkPos) {
for y in 0..SECTIONS_PER_COLUMN {
self.mark_section_dirty(pos, y);
}
}
/// Queues one section for (re)meshing.
pub fn mark_section_dirty(&mut self, pos: ChunkPos, y: i32) {
if !(0..SECTIONS_PER_COLUMN).contains(&y) {
return;
}
self.next_version += 1;
self.versions
.insert(SectionPos::new(pos, y), self.next_version);
*self.dirty.entry(pos).or_default() |= 1 << y;
}
pub fn remove_column(&mut self, pos: ChunkPos, frame: u64) {
self.dirty.remove(&pos);
for y in 0..SECTIONS_PER_COLUMN {
let section = SectionPos::new(pos, y);
self.versions.remove(§ion);
self.remove(section, frame);
}
}
/// Starts meshing dirty sections nearest to `eye` whose neighborhoods are loaded. Columns
/// with an incomplete neighborhood are dropped: the neighbor completing it marks them again.
pub fn schedule(
&mut self,
world: &ClientWorld,
eye: DVec3,
fancy: bool,
smooth_lighting: bool,
) {
self.collect();
// Enough jobs to keep every worker busy until the next frame collects them
let budget =
(rayon::current_num_threads() * 4).saturating_sub(self.in_flight + self.waiting.len());
if budget == 0 || self.dirty.is_empty() {
return;
}
let mut ready = Vec::new();
let building = &self.building;
self.dirty.retain(|&column, sections| {
let complete =
(-1..=1).all(|dx| (-1..=1).all(|dz| world.contains(column.offset(dx, dz))));
if complete {
ready.extend(
(0..SECTIONS_PER_COLUMN)
.filter(|y| *sections & 1 << y != 0)
.map(|y| SectionPos::new(column, y))
.filter(|section| !building.contains(section)),
);
}
complete
});
let nearer = |a: &SectionPos, b: &SectionPos| {
let distance = |s: &SectionPos| (s.min_corner() + 8.0).distance_squared(eye);
distance(a).total_cmp(&distance(b))
};
if ready.len() > budget {
ready.select_nth_unstable_by(budget, nearer);
ready.truncate(budget);
}
ready.sort_unstable_by(nearer);
for section in ready {
let Some(input) = world.mesh_input(section.column(), section.y, fancy, smooth_lighting)
else {
continue;
};
if let Some(sections) = self.dirty.get_mut(§ion.column()) {
*sections &= !(1 << section.y);
if *sections == 0 {
self.dirty.remove(§ion.column());
}
}
self.in_flight += 1;
self.building.insert(section);
let version = self.versions.get(§ion).copied().unwrap_or_default();
let sender = self.results_sender.clone();
rayon::spawn(move || {
let mesh = mesher::build(&input);
let _ = sender.send(MeshResult {
pos: section,
version,
mesh,
});
});
}
}
/// Queues the meshes the workers finished for upload.
fn collect(&mut self) {
for result in self.results.try_iter() {
self.in_flight -= 1;
self.building.remove(&result.pos);
self.waiting.push_back(result);
}
}
/// Drops a section's mesh and frees its slot.
fn remove(&mut self, pos: SectionPos, frame: u64) {
if let Some(Some(mesh)) = self.meshes.remove(&pos) {
self.release(mesh, frame);
}
self.translucent.remove(&pos);
}
fn release(&mut self, mesh: GpuMesh, frame: u64) {
self.retired.push_back((frame as i64 - 1, mesh.buffer));
self.records[mesh.slot as usize] = SectionRecord::zeroed();
self.dirty_slots.push(mesh.slot);
self.free_slots.push(mesh.slot);
}
/// Staging space kept for the table and order uploads of this frame, plus one more slot.
fn reserved(&self) -> usize {
let slots = if self.dirty_all {
self.records.len()
} else {
self.dirty_slots.len()
};
(slots + 1) * (RECORD_SIZE as usize + 16) + (self.order.len() + 1) * 4 + 16
}
/// Moves finished meshes into their own buffers through `staging` and updates the section
/// table and culling order, recording the copies. `frame` is the frame being recorded;
/// `completed` the last one the GPU has finished.
pub fn upload(
&mut self,
staging: &mut Staging,
copies: &mut TerrainCopies,
frame: u64,
completed: i64,
camera: DVec3,
) -> crate::gpu::Result<()> {
while self
.retired
.pop_front_if(|(last_use, _)| *last_use <= completed)
.is_some()
{}
if self.records.len() as u64 * 4 >= u64::from(self.tables.capacity) * 3 {
self.grow(frame)?;
}
self.collect();
for _ in 0..self.waiting.len() {
let Some(result) = self.waiting.pop_front() else {
break;
};
if self.versions.get(&result.pos) != Some(&result.version) {
continue;
}
let mesh = &result.mesh;
if mesh.is_empty() {
self.remove(result.pos, frame);
self.meshes.insert(result.pos, None);
self.updates += 1;
continue;
}
let bytes = size_of_val(mesh.data.as_slice());
let replaces = matches!(self.meshes.get(&result.pos), Some(Some(_)));
let room = replaces
|| !self.free_slots.is_empty()
|| self.records.len() < self.tables.capacity as usize;
if !room || bytes + 16 + self.reserved() > staging.remaining() {
self.waiting.push_front(result);
break;
}
let usage = VkBufferUsageFlags2::TRANSFER_DST;
let buffer =
match Buffer::new(&self.device, bytes as u64, usage, MemoryLocation::GpuOnly) {
Ok(buffer) => buffer,
Err(error) => {
tracing::warn!("no memory for a section mesh: {error}");
*self.dirty.entry(result.pos.column()).or_default() |= 1 << result.pos.y;
break;
}
};
let source = staging
.push(bytemuck::cast_slice(&mesh.data))
.expect("reserved staging space");
copies.meshes.push((
buffer.handle(),
VkBufferCopy {
srcOffset: source,
dstOffset: 0,
size: bytes as u64,
},
));
let slot = match self.meshes.remove(&result.pos).flatten() {
Some(old) => {
let slot = old.slot;
self.retired.push_back((frame as i64 - 1, old.buffer));
slot
}
None => self.allocate_slot(),
};
self.records[slot as usize] = record(result.pos, mesh, buffer.address());
self.dirty_slots.push(slot);
if mesh.counts[GROUPS - 1] > 0 {
self.translucent.insert(result.pos);
} else {
self.translucent.remove(&result.pos);
}
self.meshes
.insert(result.pos, Some(GpuMesh { buffer, slot }));
self.updates += 1;
}
self.upload_order(staging, copies, frame, camera);
self.upload_records(staging, copies);
Ok(())
}
fn allocate_slot(&mut self) -> u32 {
let slot = self.free_slots.pop().unwrap_or_else(|| {
self.records.push(SectionRecord::zeroed());
self.in_order.push(false);
self.records.len() as u32 - 1
});
if !self.in_order[slot as usize] {
self.in_order[slot as usize] = true;
self.order.push(slot);
}
self.order_changed = true;
slot
}
/// Replaces the tables with ones of twice the capacity, uploaded in full.
fn grow(&mut self, frame: u64) -> crate::gpu::Result<()> {
let tables = Tables::new(&self.device, self.tables.capacity * 2)?;
let old = std::mem::replace(&mut self.tables, tables);
let last_use = frame as i64 - 1;
self.retired
.extend([old.sections, old.order, old.work].map(|buffer| (last_use, buffer)));
self.dirty_all = true;
self.order_uploaded = 0;
Ok(())
}
/// Sorts the order near to far when the camera entered another section, or a while after
/// slots changed, and uploads what changed.
fn upload_order(
&mut self,
staging: &mut Staging,
copies: &mut TerrainCopies,
frame: u64,
camera: DVec3,
) {
let section = (camera / 16.0).floor().as_ivec3();
let (sorted_section, sorted_frame) = self.sorted;
if section != sorted_section || self.order_changed && frame >= sorted_frame + REORDER_FRAMES
{
self.sort_order(section);
self.sorted = (section, frame);
self.order_changed = false;
self.order_uploaded = 0;
}
if self.order_uploaded < self.order.len() {
let source = staging
.push(bytemuck::cast_slice(&self.order[self.order_uploaded..]))
.expect("reserved staging space");
copies.order.push(VkBufferCopy {
srcOffset: source,
dstOffset: self.order_uploaded as u64 * 4,
size: (self.order.len() - self.order_uploaded) as u64 * 4,
});
self.order_uploaded = self.order.len();
}
}
/// Orders the live slots by their distance in sections from `camera`, with a counting sort.
fn sort_order(&mut self, camera: IVec3) {
let distance = |slot: u32| {
let position = IVec3::from_array(self.records[slot as usize].position);
(position - camera).abs().max_element() as usize
};
self.in_order.fill(false);
self.order
.retain(|&slot| self.records[slot as usize].total != 0);
let mut starts = vec![0; self.order.iter().map(|&s| distance(s)).max().unwrap_or(0) + 2];
for &slot in &self.order {
starts[distance(slot) + 1] += 1;
}
for i in 1..starts.len() {
starts[i] += starts[i - 1];
}
let mut sorted = vec![0; self.order.len()];
for &slot in &self.order {
let start = &mut starts[distance(slot)];
sorted[*start] = slot;
*start += 1;
self.in_order[slot as usize] = true;
}
self.order = sorted;
}
/// Uploads changed records, one copy per run of consecutive slots.
fn upload_records(&mut self, staging: &mut Staging, copies: &mut TerrainCopies) {
if std::mem::take(&mut self.dirty_all) {
self.dirty_slots = (0..self.records.len() as u32).collect();
}
self.dirty_slots.sort_unstable();
self.dirty_slots.dedup();
for run in self.dirty_slots.chunk_by(|a, b| a + 1 == *b) {
let records = &self.records[run[0] as usize..=run[run.len() - 1] as usize];
let source = staging
.push(bytemuck::cast_slice(records))
.expect("reserved staging space");
copies.sections.push(VkBufferCopy {
srcOffset: source,
dstOffset: u64::from(run[0]) * RECORD_SIZE,
size: size_of_val(records) as u64,
});
}
self.dirty_slots.clear();
}
pub fn take_updates(&mut self) -> u32 {
std::mem::take(&mut self.updates)
}
/// Sections with geometry.
pub fn section_count(&self) -> usize {
self.records.len() - self.free_slots.len()
}
/// Slots `cullMain` goes through.
pub fn order_len(&self) -> u32 {
self.order.len() as u32
}
pub fn tables(&self) -> (VkBuffer, VkBuffer, VkBuffer) {
let tables = &self.tables;
(
tables.sections.handle(),
tables.order.handle(),
tables.work.handle(),
)
}
pub fn sections_address(&self) -> VkDeviceAddress {
self.tables.sections.address()
}
pub fn tested_address(&self) -> VkDeviceAddress {
self.tables.work.address() + self.tables.tested_offset()
}
/// Where the solid (0) or cutout (1) draws start in the work buffer, and how many fit.
pub fn commands(&self, layer: u64) -> (u64, u32) {
let tables = &self.tables;
(
tables.commands_offset(layer),
tables.draws_per_layer() as u32,
)
}
pub fn translucent_offset(&self) -> u64 {
self.tables.translucent_offset()
}
/// Push data for `cullMain`, reading the back to front translucent slots at `translucent`.
pub fn cull_push(
&self,
eye: DVec3,
camera: Vec3,
view_projection: Mat4,
translucent: VkDeviceAddress,
) -> CullPush {
let (eye_block, eye_fraction) = split_eye(eye);
let tables = &self.tables;
let work = tables.work.address();
CullPush {
planes: Frustum::from_matrix(view_projection)
.planes
.map(|plane| plane.to_array()),
eye_fraction,
camera: camera.extend(0.0).to_array(),
eye_block,
sections: tables.sections.address(),
order: tables.order.address(),
counters: work,
tested: work + tables.tested_offset(),
commands: work + tables.commands_offset(0),
translucent,
translucent_commands: work + tables.translucent_offset(),
count: 0,
capacity: tables.draws_per_layer() as u32,
mode: 0,
_padding: 0,
}
}
/// Slots of the sections with translucent geometry inside the frustum, back to front.
pub fn translucent(&self, eye: DVec3, view_projection: Mat4, slots: &mut Vec<u32>) {
let frustum = Frustum::from_matrix(view_projection);
let mut sections: Vec<(f64, u32)> = self
.translucent
.iter()
.filter_map(|pos| {
let mesh = self.meshes.get(pos)?.as_ref()?;
let record = &self.records[mesh.slot as usize];
let origin = (pos.min_corner() - eye).as_vec3();
let e = record.extents.map(f32::from);
let (min, max) = (Vec3::new(e[0], e[1], e[2]), Vec3::new(e[3], e[4], e[5]));
frustum
.intersects_box(origin + min, origin + max)
.then(|| ((pos.min_corner() + 8.0).distance_squared(eye), mesh.slot))
})
.collect();
sections.sort_by(|a, b| b.0.total_cmp(&a.0));
slots.extend(sections.iter().map(|&(_, slot)| slot));
}
}
fn record(pos: SectionPos, mesh: &SectionMesh, address: VkDeviceAddress) -> SectionRecord {
let mut extents = [0; 12];
extents[..6].copy_from_slice(&mesh.bounds);
extents[6..].copy_from_slice(&mesh.planes);
SectionRecord {
position: [pos.x, pos.y, pos.z],
visible: 0,
was_visible: 0,
extents,
shapes: address,
lights: address + u64::from(mesh.lights),
tints: address + u64::from(mesh.tints),
faces: address + u64::from(mesh.faces),
counts: mesh.counts,
total: mesh.counts.iter().sum(),
}
}
/// View frustum planes in camera-relative space.
struct Frustum {
planes: [Vec4; 5],
}
impl Frustum {
/// Extracts the side and near planes of a reverse-Z projection with an infinite far plane.
fn from_matrix(view_projection: Mat4) -> Self {
let row = |index| view_projection.row(index);
let planes = [
row(3) + row(0),
row(3) - row(0),
row(3) + row(1),
row(3) - row(1),
row(3) - row(2),
];
Self { planes }
}
fn intersects_box(&self, min: Vec3, max: Vec3) -> bool {
self.planes.iter().all(|plane| {
let corner = Vec3::new(
if plane.x >= 0.0 { max.x } else { min.x },
if plane.y >= 0.0 { max.y } else { min.y },
if plane.z >= 0.0 { max.z } else { min.z },
);
plane.truncate().dot(corner) + plane.w >= 0.0
})
}
}