Use AABBs for collision (#1726)

Fixes #826 
Fixes #495

---------

Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>
Co-authored-by: IntegratedQuantum <jahe788@gmail.com>
This commit is contained in:
codemob 2025-08-15 11:54:54 -04:00 committed by GitHub
parent b47579e41e
commit 83d37bca40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 245 additions and 166 deletions

View File

@ -52,102 +52,22 @@ pub const camera = struct { // MARK: camera
};
pub const collision = struct {
pub fn triangleAABB(triangle: [3]Vec3d, box_center: Vec3d, box_extents: Vec3d) bool {
const X = 0;
const Y = 1;
const Z = 2;
pub const Box = struct {
min: Vec3d,
max: Vec3d,
// Translate triangle as conceptually moving AABB to origin
const v0 = triangle[0] - box_center;
const v1 = triangle[1] - box_center;
const v2 = triangle[2] - box_center;
// Compute edge vectors for triangle
const f0 = triangle[1] - triangle[0];
const f1 = triangle[2] - triangle[1];
const f2 = triangle[0] - triangle[2];
// Test axis a00
const a00 = Vec3d{0, -f0[Z], f0[Y]};
if(!test_axis(a00, v0, v1, v2, box_extents[Y]*@abs(f0[Z]) + box_extents[Z]*@abs(f0[Y]))) {
return false;
pub fn center(self: Box) Vec3d {
return (self.min + self.max)*@as(Vec3d, @splat(0.5));
}
// Test axis a01
const a01 = Vec3d{0, -f1[Z], f1[Y]};
if(!test_axis(a01, v0, v1, v2, box_extents[Y]*@abs(f1[Z]) + box_extents[Z]*@abs(f1[Y]))) {
return false;
pub fn extent(self: Box) Vec3d {
return (self.max - self.min)*@as(Vec3d, @splat(0.5));
}
// Test axis a02
const a02 = Vec3d{0, -f2[Z], f2[Y]};
if(!test_axis(a02, v0, v1, v2, box_extents[Y]*@abs(f2[Z]) + box_extents[Z]*@abs(f2[Y]))) {
return false;
pub fn intersects(self: Box, other: Box) bool {
return @reduce(.And, (self.max >= other.min)) and @reduce(.And, (self.min <= other.max));
}
// Test axis a10
const a10 = Vec3d{f0[Z], 0, -f0[X]};
if(!test_axis(a10, v0, v1, v2, box_extents[X]*@abs(f0[Z]) + box_extents[Z]*@abs(f0[X]))) {
return false;
}
// Test axis a11
const a11 = Vec3d{f1[Z], 0, -f1[X]};
if(!test_axis(a11, v0, v1, v2, box_extents[X]*@abs(f1[Z]) + box_extents[Z]*@abs(f1[X]))) {
return false;
}
// Test axis a12
const a12 = Vec3d{f2[Z], 0, -f2[X]};
if(!test_axis(a12, v0, v1, v2, box_extents[X]*@abs(f2[Z]) + box_extents[Z]*@abs(f2[X]))) {
return false;
}
// Test axis a20
const a20 = Vec3d{-f0[Y], f0[X], 0};
if(!test_axis(a20, v0, v1, v2, box_extents[X]*@abs(f0[Y]) + box_extents[Y]*@abs(f0[X]))) {
return false;
}
// Test axis a21
const a21 = Vec3d{-f1[Y], f1[X], 0};
if(!test_axis(a21, v0, v1, v2, box_extents[X]*@abs(f1[Y]) + box_extents[Y]*@abs(f1[X]))) {
return false;
}
// Test axis a22
const a22 = Vec3d{-f2[Y], f2[X], 0};
if(!test_axis(a22, v0, v1, v2, box_extents[X]*@abs(f2[Y]) + box_extents[Y]*@abs(f2[X]))) {
return false;
}
// Test the three axes corresponding to the face normals of AABB
if(@max(v0[X], @max(v1[X], v2[X])) < -box_extents[X] or @min(v0[X], @min(v1[X], v2[X])) > box_extents[X]) {
return false;
}
if(@max(v0[Y], @max(v1[Y], v2[Y])) < -box_extents[Y] or @min(v0[Y], @min(v1[Y], v2[Y])) > box_extents[Y]) {
return false;
}
if(@max(v0[Z], @max(v1[Z], v2[Z])) < -box_extents[Z] or @min(v0[Z], @min(v1[Z], v2[Z])) > box_extents[Z]) {
return false;
}
// Test separating axis corresponding to triangle face normal
const plane_normal = vec.cross(f0, f1);
const plane_distance = @abs(vec.dot(plane_normal, v0));
const r = box_extents[X]*@abs(plane_normal[X]) + box_extents[Y]*@abs(plane_normal[Y]) + box_extents[Z]*@abs(plane_normal[Z]);
return plane_distance <= r;
}
fn test_axis(axis: Vec3d, v0: Vec3d, v1: Vec3d, v2: Vec3d, r: f64) bool {
const p0 = vec.dot(v0, axis);
const p1 = vec.dot(v1, axis);
const p2 = vec.dot(v2, axis);
const min_p = @min(p0, @min(p1, p2));
const max_p = @max(p0, @max(p1, p2));
return @max(-max_p, min_p) <= r;
}
};
const Direction = enum(u2) {x = 0, y = 1, z = 2};
@ -158,61 +78,21 @@ pub const collision = struct {
const model = block.mode().model(block).model();
const pos = Vec3d{@floatFromInt(x), @floatFromInt(y), @floatFromInt(z)};
const entityCollision = Box{.min = entityPosition - entityBoundingBoxExtent, .max = entityPosition + entityBoundingBoxExtent};
for(model.neighborFacingQuads) |quads| {
for(quads) |quadIndex| {
const quad = quadIndex.quadInfo();
if(triangleAABB(.{quad.cornerVec(0) + quad.normalVec() + pos, quad.cornerVec(2) + quad.normalVec() + pos, quad.cornerVec(1) + quad.normalVec() + pos}, entityPosition, entityBoundingBoxExtent)) {
const min = @min(@min(quad.cornerVec(0), quad.cornerVec(1)), @min(quad.cornerVec(2), quad.cornerVec(3))) + quad.normalVec() + pos;
const max = @max(@max(quad.cornerVec(0), quad.cornerVec(1)), @max(quad.cornerVec(2), quad.cornerVec(3))) + quad.normalVec() + pos;
const dist = @min(vec.dot(directionVector, min), vec.dot(directionVector, max));
if(dist < minDistance) {
resultBox = .{.min = min, .max = max};
minDistance = dist;
} else if(dist == minDistance) {
resultBox.?.min = @min(resultBox.?.min, min);
resultBox.?.max = @min(resultBox.?.max, max);
}
}
if(triangleAABB(.{quad.cornerVec(1) + quad.normalVec() + pos, quad.cornerVec(2) + quad.normalVec() + pos, quad.cornerVec(3) + quad.normalVec() + pos}, entityPosition, entityBoundingBoxExtent)) {
const min = @min(@min(quad.cornerVec(0), quad.cornerVec(1)), @min(quad.cornerVec(2), quad.cornerVec(3))) + quad.normalVec() + pos;
const max = @max(@max(quad.cornerVec(0), quad.cornerVec(1)), @max(quad.cornerVec(2), quad.cornerVec(3))) + quad.normalVec() + pos;
const dist = @min(vec.dot(directionVector, min), vec.dot(directionVector, max));
if(dist < minDistance) {
resultBox = .{.min = min, .max = max};
minDistance = dist;
} else if(dist == minDistance) {
resultBox.?.min = @min(resultBox.?.min, min);
resultBox.?.max = @min(resultBox.?.max, max);
}
}
}
}
for(model.collision) |relativeBlockCollision| {
const blockCollision = Box{.min = relativeBlockCollision.min + pos, .max = relativeBlockCollision.max + pos};
if(blockCollision.intersects(entityCollision)) {
const dotMin = vec.dot(directionVector, blockCollision.min);
const dotMax = vec.dot(directionVector, blockCollision.max);
for(model.internalQuads) |quadIndex| {
const quad = quadIndex.quadInfo();
if(triangleAABB(.{quad.cornerVec(0) + pos, quad.cornerVec(2) + pos, quad.cornerVec(1) + pos}, entityPosition, entityBoundingBoxExtent)) {
const min = @min(@min(quad.cornerVec(0), quad.cornerVec(1)), @min(quad.cornerVec(2), quad.cornerVec(3))) + pos;
const max = @max(@max(quad.cornerVec(0), quad.cornerVec(1)), @max(quad.cornerVec(2), quad.cornerVec(3))) + pos;
const dist = @min(vec.dot(directionVector, min), vec.dot(directionVector, max));
if(dist < minDistance) {
resultBox = .{.min = min, .max = max};
minDistance = dist;
} else if(dist == minDistance) {
resultBox.?.min = @min(resultBox.?.min, min);
resultBox.?.max = @min(resultBox.?.max, max);
}
}
if(triangleAABB(.{quad.cornerVec(1) + pos, quad.cornerVec(2) + pos, quad.cornerVec(3) + pos}, entityPosition, entityBoundingBoxExtent)) {
const min = @min(@min(quad.cornerVec(0), quad.cornerVec(1)), @min(quad.cornerVec(2), quad.cornerVec(3))) + pos;
const max = @max(@max(quad.cornerVec(0), quad.cornerVec(1)), @max(quad.cornerVec(2), quad.cornerVec(3))) + pos;
const dist = @min(vec.dot(directionVector, min), vec.dot(directionVector, max));
if(dist < minDistance) {
resultBox = .{.min = min, .max = max};
minDistance = dist;
} else if(dist == minDistance) {
resultBox.?.min = @min(resultBox.?.min, min);
resultBox.?.max = @min(resultBox.?.max, max);
const distance = @min(dotMin, dotMax);
if(distance < minDistance) {
resultBox = blockCollision;
minDistance = distance;
} else if(distance == minDistance) {
resultBox = .{.min = @min(resultBox.?.min, blockCollision.min), .max = @max(resultBox.?.max, blockCollision.max)};
}
}
}
@ -459,18 +339,14 @@ pub const collision = struct {
fn isBlockIntersecting(block: Block, posX: i32, posY: i32, posZ: i32, center: Vec3d, extent: Vec3d) bool {
const model = block.mode().model(block).model();
const position = Vec3d{@floatFromInt(posX), @floatFromInt(posY), @floatFromInt(posZ)};
for(model.neighborFacingQuads) |quads| {
for(quads) |quadIndex| {
const quad = quadIndex.quadInfo();
if(triangleAABB(.{quad.cornerVec(0) + quad.normalVec() + position, quad.cornerVec(2) + quad.normalVec() + position, quad.cornerVec(1) + quad.normalVec() + position}, center, extent) or
triangleAABB(.{quad.cornerVec(1) + quad.normalVec() + position, quad.cornerVec(2) + quad.normalVec() + position, quad.cornerVec(3) + quad.normalVec() + position}, center, extent)) return true;
const entityBox = Box{.min = center - extent, .max = center + extent};
for(model.collision) |relativeBlockCollision| {
const blockBox = Box{.min = position + relativeBlockCollision.min, .max = position + relativeBlockCollision.max};
if(blockBox.intersects(entityBox)) {
return true;
}
}
for(model.internalQuads) |quadIndex| {
const quad = quadIndex.quadInfo();
if(triangleAABB(.{quad.cornerVec(0) + position, quad.cornerVec(2) + position, quad.cornerVec(1) + position}, center, extent) or
triangleAABB(.{quad.cornerVec(1) + position, quad.cornerVec(2) + position, quad.cornerVec(3) + position}, center, extent)) return true;
}
return false;
}
@ -510,19 +386,6 @@ pub const collision = struct {
}
}
}
pub const Box = struct {
min: Vec3d,
max: Vec3d,
pub fn center(self: Box) Vec3d {
return (self.min + self.max)*@as(Vec3d, @splat(0.5));
}
pub fn extent(self: Box) Vec3d {
return (self.max - self.min)*@as(Vec3d, @splat(0.5));
}
};
};
pub const Gamemode = enum(u8) {survival = 0, creative = 1};

View File

@ -7,10 +7,13 @@ const main = @import("main");
const vec = @import("vec.zig");
const Vec3i = vec.Vec3i;
const Vec3f = vec.Vec3f;
const Vec3d = vec.Vec3d;
const Vec2f = vec.Vec2f;
const Mat4f = vec.Mat4f;
const FaceData = main.renderer.chunk_meshing.FaceData;
const NeverFailingAllocator = main.heap.NeverFailingAllocator;
const Box = main.game.collision.Box;
var quadSSBO: graphics.SSBO = undefined;
@ -40,6 +43,8 @@ const ExtraQuadInfo = struct {
};
const gridSize = 4096;
const collisionGridSize = 16;
const CollisionGridInteger = std.meta.Int(.unsigned, collisionGridSize);
fn snapToGrid(x: anytype) @TypeOf(x) {
const T = @TypeOf(x);
@ -92,6 +97,7 @@ pub const Model = struct {
allNeighborsOccluded: bool,
noNeighborsOccluded: bool,
hasNeighborFacingQuads: bool,
collision: []Box,
fn getFaceNeighbor(quad: *const QuadInfo) ?chunk.Neighbor {
var allZero: @Vector(3, bool) = .{true, true, true};
@ -195,9 +201,218 @@ pub const Model = struct {
self.allNeighborsOccluded = self.allNeighborsOccluded and self.isNeighborOccluded[neighbor];
self.noNeighborsOccluded = self.noNeighborsOccluded and !self.isNeighborOccluded[neighbor];
}
generateCollision(self, adjustedQuads);
return modelIndex;
}
fn edgeInterp(y: f32, x0: f32, y0: f32, x1: f32, y1: f32) f32 {
if(y1 == y0) return x0;
return x0 + (x1 - x0)*(y - y0)/(y1 - y0);
}
fn solveDepth(normal: Vec3f, v0: Vec3f, xIndex: usize, yIndex: usize, zIndex: usize, u: f32, v: f32) f32 {
const nX = normal[xIndex];
const nY = normal[yIndex];
const nZ = normal[zIndex];
const planeOffset = -vec.dot(v0, normal);
return (-(nX*u + nY*v + planeOffset))/nZ;
}
fn rasterize(triangle: [3]Vec3f, grid: *[collisionGridSize][collisionGridSize]CollisionGridInteger, normal: Vec3f) void {
var xIndex: usize = undefined;
var yIndex: usize = undefined;
var zIndex: usize = undefined;
const v0 = triangle[0]*@as(Vec3f, @splat(@floatFromInt(collisionGridSize)));
const v1 = triangle[1]*@as(Vec3f, @splat(@floatFromInt(collisionGridSize)));
const v2 = triangle[2]*@as(Vec3f, @splat(@floatFromInt(collisionGridSize)));
const absNormal = @abs(normal);
if(absNormal[0] >= absNormal[1] and absNormal[0] >= absNormal[2]) {
xIndex = 1;
yIndex = 2;
zIndex = 0;
} else if(absNormal[1] >= absNormal[0] and absNormal[1] >= absNormal[2]) {
xIndex = 0;
yIndex = 2;
zIndex = 1;
} else {
xIndex = 0;
yIndex = 1;
zIndex = 2;
}
const min: Vec3f = @min(v0, v1, v2);
const max: Vec3f = @max(v0, v1, v2);
const voxelMin: Vec3i = @max(@as(Vec3i, @intFromFloat(@floor(min))), @as(Vec3i, @splat(0)));
const voxelMax: Vec3i = @max(@as(Vec3i, @intFromFloat(@ceil(max))), @as(Vec3i, @splat(0)));
var p0 = Vec2f{v0[xIndex], v0[yIndex]};
var p1 = Vec2f{v1[xIndex], v1[yIndex]};
var p2 = Vec2f{v2[xIndex], v2[yIndex]};
if(p0[1] > p1[1]) {
std.mem.swap(Vec2f, &p0, &p1);
}
if(p0[1] > p2[1]) {
std.mem.swap(Vec2f, &p0, &p2);
}
if(p1[1] > p2[1]) {
std.mem.swap(Vec2f, &p1, &p2);
}
for(@intCast(voxelMin[yIndex])..@intCast(voxelMax[yIndex])) |y| {
if(y >= collisionGridSize) continue;
const yf = std.math.clamp(@as(f32, @floatFromInt(y)) + 0.5, min[yIndex], max[yIndex]);
var xa: f32 = undefined;
var xb: f32 = undefined;
if(yf < p1[1]) {
xa = edgeInterp(yf, p0[0], p0[1], p1[0], p1[1]);
xb = edgeInterp(yf, p0[0], p0[1], p2[0], p2[1]);
} else {
xa = edgeInterp(yf, p1[0], p1[1], p2[0], p2[1]);
xb = edgeInterp(yf, p0[0], p0[1], p2[0], p2[1]);
}
const xStart: f32 = @min(xa, xb);
const xEnd: f32 = @max(xa, xb);
const voxelXStart: usize = @intFromFloat(@max(@floor(xStart), 0.0));
const voxelXEnd: usize = @intFromFloat(@max(@ceil(xEnd), 0.0));
for(voxelXStart..voxelXEnd) |x| {
if(x < 0 or x >= collisionGridSize) continue;
const xf = std.math.clamp(@as(f32, @floatFromInt(x)) + 0.5, xStart, xEnd);
const zf = solveDepth(normal, v0, xIndex, yIndex, zIndex, xf, yf);
if(zf < 0.0) continue;
const z: usize = @intFromFloat(zf);
if(z >= collisionGridSize) continue;
const pos: [3]usize = .{x, y, z};
var realPos: [3]usize = undefined;
realPos[xIndex] = pos[0];
realPos[yIndex] = pos[1];
realPos[zIndex] = pos[2];
grid[realPos[0]][realPos[1]] |= @as(CollisionGridInteger, 1) << @intCast(realPos[2]);
}
}
}
fn generateCollision(self: *Model, modelQuads: []QuadInfo) void {
var hollowGrid: [collisionGridSize][collisionGridSize]CollisionGridInteger = @splat(@splat(0));
const voxelSize: Vec3f = @splat(1.0/@as(f32, collisionGridSize));
for(modelQuads) |quad| {
var shift = Vec3f{0, 0, 0};
for(0..3) |i| {
if(@abs(quad.normalVec()[i]) == 1.0 and @floor(quad.corners[0][i]*collisionGridSize) == quad.corners[0][i]*collisionGridSize) {
shift = quad.normalVec()*voxelSize*@as(Vec3f, @splat(0.5));
}
}
const triangle1: [3]Vec3f = .{
quad.cornerVec(0) - shift,
quad.cornerVec(1) - shift,
quad.cornerVec(2) - shift,
};
const triangle2: [3]Vec3f = .{
quad.cornerVec(1) - shift,
quad.cornerVec(2) - shift,
quad.cornerVec(3) - shift,
};
rasterize(triangle1, &hollowGrid, quad.normalVec());
rasterize(triangle2, &hollowGrid, quad.normalVec());
}
const allOnes = ~@as(CollisionGridInteger, 0);
var grid: [collisionGridSize][collisionGridSize]CollisionGridInteger = @splat(@splat(allOnes));
var floodfillQueue = main.utils.CircularBufferQueue(struct {x: usize, y: usize, val: CollisionGridInteger}).init(main.stackAllocator, 1024);
defer floodfillQueue.deinit();
for(0..collisionGridSize) |x| {
for(0..collisionGridSize) |y| {
var val = 1 | @as(CollisionGridInteger, 1) << (@bitSizeOf(CollisionGridInteger) - 1);
if(x == 0 or x == collisionGridSize - 1 or y == 0 or y == collisionGridSize - 1) val = allOnes;
floodfillQueue.pushBack(.{.x = x, .y = y, .val = val});
}
}
while(floodfillQueue.popFront()) |elem| {
const oldValue = grid[elem.x][elem.y];
const newValue = oldValue & ~(~hollowGrid[elem.x][elem.y] & elem.val);
if(oldValue == newValue) continue;
grid[elem.x][elem.y] = newValue;
if(elem.x != 0) floodfillQueue.pushBack(.{.x = elem.x - 1, .y = elem.y, .val = ~newValue});
if(elem.x != collisionGridSize - 1) floodfillQueue.pushBack(.{.x = elem.x + 1, .y = elem.y, .val = ~newValue});
if(elem.y != 0) floodfillQueue.pushBack(.{.x = elem.x, .y = elem.y - 1, .val = ~newValue});
if(elem.y != collisionGridSize - 1) floodfillQueue.pushBack(.{.x = elem.x, .y = elem.y + 1, .val = ~newValue});
floodfillQueue.pushBack(.{.x = elem.x, .y = elem.y, .val = ~newValue << 1 | ~newValue >> 1});
}
var collision: main.List(Box) = .init(main.globalAllocator);
for(0..collisionGridSize) |x| {
for(0..collisionGridSize) |y| {
while(grid[x][y] != 0) {
const startZ = @ctz(grid[x][y]);
const height = @min(@bitSizeOf(CollisionGridInteger) - startZ, @ctz(~grid[x][y] >> @intCast(startZ)));
const mask = allOnes << @intCast(startZ) & ~((allOnes << 1) << @intCast(height + startZ - 1));
const boxMin = Vec3i{@intCast(x), @intCast(y), startZ};
var boxMax = Vec3i{@intCast(x + 1), @intCast(y + 1), startZ + height};
while(canExpand(&grid, boxMin, boxMax, .x, mask)) boxMax[0] += 1;
while(canExpand(&grid, boxMin, boxMax, .y, mask)) boxMax[1] += 1;
disableAll(&grid, boxMin, boxMax, mask);
const min = @as(Vec3f, @floatFromInt(boxMin))/@as(Vec3f, @splat(collisionGridSize));
const max = @as(Vec3f, @floatFromInt(boxMax))/@as(Vec3f, @splat(collisionGridSize));
collision.append(Box{.min = min, .max = max});
}
}
}
self.collision = collision.toOwnedSlice();
}
fn allTrue(grid: *const [collisionGridSize][collisionGridSize]CollisionGridInteger, min: Vec3i, max: Vec3i, mask: CollisionGridInteger) bool {
if(max[0] > collisionGridSize or max[1] > collisionGridSize) {
return false;
}
for(@intCast(min[0])..@intCast(max[0])) |x| {
for(@intCast(min[1])..@intCast(max[1])) |y| {
if((grid[x][y] & mask) != mask) {
return false;
}
}
}
return true;
}
fn disableAll(grid: *[collisionGridSize][collisionGridSize]CollisionGridInteger, min: Vec3i, max: Vec3i, mask: CollisionGridInteger) void {
for(@intCast(min[0])..@intCast(max[0])) |x| {
for(@intCast(min[1])..@intCast(max[1])) |y| {
grid[x][y] &= ~mask;
}
}
}
fn canExpand(grid: *const [collisionGridSize][collisionGridSize]CollisionGridInteger, min: Vec3i, max: Vec3i, dir: enum {x, y}, mask: CollisionGridInteger) bool {
return switch(dir) {
.x => allTrue(grid, Vec3i{max[0], min[1], min[2]}, Vec3i{max[0] + 1, max[1], max[2]}, mask),
.y => allTrue(grid, Vec3i{min[0], max[1], min[2]}, Vec3i{max[0], max[1] + 1, max[2]}, mask),
};
}
fn addVert(vert: Vec3f, vertList: *main.List(Vec3f)) usize {
const ind = for(vertList.*.items, 0..) |vertex, index| {
if(vertex == vert) break index;
@ -386,6 +601,7 @@ pub const Model = struct {
main.globalAllocator.free(self.neighborFacingQuads[i]);
}
main.globalAllocator.free(self.internalQuads);
main.globalAllocator.free(self.collision);
}
pub fn getRawFaces(model: Model, quadList: *main.List(QuadInfo)) void {