Skip to content

Commit c9f4143

Browse files
committed
Quake map collision: adding bevel planes, sweep trace for bounding boxes
1 parent 860d1e3 commit c9f4143

File tree

2 files changed

+165
-63
lines changed

2 files changed

+165
-63
lines changed

src/examples/quakemap.zig

+40-44
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ var player_pos: math.Vec3 = math.Vec3.zero;
2222
var player_vel: math.Vec3 = math.Vec3.zero;
2323
var on_ground = true;
2424

25-
var time: f64 = 0.0;
26-
2725
pub fn main() !void {
2826
const example = delve.modules.Module{
2927
.name = "quakemap_example",
@@ -198,50 +196,65 @@ pub fn on_tick(delta: f32) void {
198196
if (delve.platform.input.isKeyJustPressed(.ESCAPE))
199197
std.os.exit(0);
200198

201-
time += delta;
199+
do_player_move(delta);
202200

201+
// update camera position to new player pos
202+
camera.position = player_pos;
203203
camera.runSimpleCamera(0, 60 * delta, true);
204204
}
205205

206206
pub fn on_draw() void {
207207
const proj_view_matrix = camera.getProjView();
208208
var model = math.Mat4.identity;
209209

210+
for (0..map_meshes.items.len) |idx| {
211+
map_meshes.items[idx].draw(proj_view_matrix, model);
212+
}
213+
for (0..entity_meshes.items.len) |idx| {
214+
entity_meshes.items[idx].draw(proj_view_matrix, model);
215+
}
216+
217+
cube_mesh.draw(proj_view_matrix, math.Mat4.translate(camera.position));
218+
}
219+
220+
pub fn do_player_move(delta: f32) void {
210221
// gravity!
211-
player_vel.y -= 0.01;
222+
player_vel.y -= 0.5 * delta;
212223

224+
// get our forward input direction
225+
var move_dir: math.Vec3 = math.Vec3.zero;
213226
if (delve.platform.input.isKeyPressed(.W)) {
214227
var dir = camera.direction;
215228
dir.y = 0.0;
216229
dir = dir.norm();
217-
218-
player_vel.x = -0.1 * dir.x;
219-
player_vel.z = -0.1 * dir.z;
230+
move_dir = move_dir.sub(dir);
220231
}
221232
if (delve.platform.input.isKeyPressed(.S)) {
222233
var dir = camera.direction;
223234
dir.y = 0.0;
224235
dir = dir.norm();
225-
226-
player_vel.x += 0.1 * dir.x;
227-
player_vel.z += 0.1 * dir.z;
236+
move_dir = move_dir.add(dir);
228237
}
229238

239+
// get our sideways input direction
230240
if (delve.platform.input.isKeyPressed(.D)) {
231241
const right_dir = camera.getRightDirection();
232-
player_vel.x += 0.1 * right_dir.x;
233-
player_vel.z += 0.1 * right_dir.z;
242+
move_dir = move_dir.add(right_dir);
234243
}
235244
if (delve.platform.input.isKeyPressed(.A)) {
236245
const right_dir = camera.getRightDirection();
237-
player_vel.x += -0.1 * right_dir.x;
238-
player_vel.z += -0.1 * right_dir.z;
246+
move_dir = move_dir.sub(right_dir);
239247
}
248+
249+
// jumnp and fly
240250
if (delve.platform.input.isKeyPressed(.SPACE) and on_ground) player_vel.y = 0.3;
241251
if (delve.platform.input.isKeyPressed(.F)) player_vel.y = 0.1;
242252

243-
var check_bounds_x = delve.spatial.BoundingBox.init(player_pos.add(math.Vec3.new(player_vel.x, 0, 0)), bounding_box_size);
253+
move_dir = move_dir.norm();
254+
player_vel = player_vel.add(move_dir.scale(10.0).scale(delta));
244255

256+
// horizontal collisions
257+
var check_bounds_x = delve.spatial.BoundingBox.init(player_pos.add(math.Vec3.new(player_vel.x, 0, 0)), bounding_box_size);
245258
var did_collide_x = false;
246259
for (quake_map.worldspawn.solids.items) |solid| {
247260
did_collide_x = solid.checkBoundingBoxCollision(check_bounds_x);
@@ -251,8 +264,18 @@ pub fn on_draw() void {
251264
if (did_collide_x)
252265
player_vel.x = 0.0;
253266

254-
var check_bounds_y = delve.spatial.BoundingBox.init(player_pos.add(math.Vec3.new(0, player_vel.y, 0)), bounding_box_size);
267+
var check_bounds_z = delve.spatial.BoundingBox.init(player_pos.add(math.Vec3.new(player_vel.x, 0, player_vel.z)), bounding_box_size);
268+
var did_collide_z = false;
269+
for (quake_map.worldspawn.solids.items) |solid| {
270+
did_collide_z = solid.checkBoundingBoxCollision(check_bounds_z);
271+
if (did_collide_z)
272+
break;
273+
}
274+
if (did_collide_z)
275+
player_vel.z = 0.0;
255276

277+
// vertical collision
278+
var check_bounds_y = delve.spatial.BoundingBox.init(player_pos.add(math.Vec3.new(player_vel.x, player_vel.y, player_vel.z)), bounding_box_size);
256279
var did_collide_y = false;
257280
for (quake_map.worldspawn.solids.items) |solid| {
258281
did_collide_y = solid.checkBoundingBoxCollision(check_bounds_y);
@@ -266,36 +289,9 @@ pub fn on_draw() void {
266289
on_ground = false;
267290
}
268291

269-
var check_bounds_z = delve.spatial.BoundingBox.init(player_pos.add(math.Vec3.new(0, 0, player_vel.z)), bounding_box_size);
270-
271-
var did_collide_z = false;
272-
for (quake_map.worldspawn.solids.items) |solid| {
273-
did_collide_z = solid.checkBoundingBoxCollision(check_bounds_z);
274-
if (did_collide_z)
275-
break;
276-
}
277-
if (did_collide_z)
278-
player_vel.z = 0.0;
279-
292+
// velocity has been clipped to collisions, can move now
280293
player_pos = player_pos.add(player_vel);
281294

282-
for (0..map_meshes.items.len) |idx| {
283-
// if(did_collide) {
284-
// map_meshes.items[idx].drawWithMaterial(&fallback_material, proj_view_matrix, model);
285-
// }
286-
// else {
287-
288-
map_meshes.items[idx].draw(proj_view_matrix, model);
289-
}
290-
for (0..entity_meshes.items.len) |idx| {
291-
entity_meshes.items[idx].draw(proj_view_matrix, model);
292-
}
293-
294-
cube_mesh.draw(proj_view_matrix, math.Mat4.translate(camera.position));
295-
296-
// update camera position to new player pos
297-
camera.position = player_pos;
298-
299295
// dumb friction!
300296
player_vel.x = 0.0;
301297
player_vel.z = 0.0;

src/framework/utils/quakemap.zig

+125-19
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ pub const ErrorInfo = struct {
2525
line_number: usize,
2626
};
2727

28+
pub const QuakeMapHit = struct {
29+
loc: Vec3,
30+
plane: Plane,
31+
};
32+
2833
pub const Property = struct {
2934
key: []const u8,
3035
value: []const u8,
@@ -97,6 +102,7 @@ pub const Entity = struct {
97102

98103
pub const Solid = struct {
99104
faces: std.ArrayList(Face),
105+
bounds: BoundingBox = undefined,
100106

101107
fn init(allocator: Allocator) Solid {
102108
return .{ .faces = std.ArrayList(Face).init(allocator) };
@@ -110,6 +116,9 @@ pub const Solid = struct {
110116
defer vertices.deinit(allocator);
111117
defer clipped.deinit(allocator);
112118

119+
// build a bounding box along the way too
120+
var solid_bounds: BoundingBox = undefined;
121+
113122
for (self.faces.items, 0..) |*face, i| {
114123
const quad = makeQuadWithRadius(face.plane, 1000000.0);
115124
vertices.clearRetainingCapacity();
@@ -125,7 +134,20 @@ pub const Solid = struct {
125134
}
126135

127136
face.vertices = try allocator.dupe(Vec3, vertices.items);
137+
138+
// create a bounding box out of these vertices
139+
const face_bounds: BoundingBox = BoundingBox.initFromPositions(face.vertices);
140+
if (i == 0) {
141+
solid_bounds = face_bounds;
142+
continue;
143+
}
144+
145+
// expand our solid's bounding box by the new face bounds
146+
solid_bounds.min = Vec3.min(solid_bounds.min, face_bounds.min);
147+
solid_bounds.max = Vec3.max(solid_bounds.max, face_bounds.max);
128148
}
149+
150+
self.bounds = solid_bounds;
129151
}
130152

131153
fn clip(allocator: Allocator, vertices: std.ArrayListUnmanaged(Vec3), clip_plane: Plane, clipped: *std.ArrayListUnmanaged(Vec3)) !void {
@@ -193,44 +215,128 @@ pub const Solid = struct {
193215
return true;
194216
}
195217

196-
pub fn checkBoundingBoxCollision(self: *const Solid, bounds: BoundingBox) bool {
197-
const x_size = (bounds.max.x - bounds.min.x) * 0.5;
198-
const y_size = (bounds.max.y - bounds.min.y) * 0.5;
199-
const z_size = (bounds.max.z - bounds.min.z) * 0.5;
218+
// Get our planes expanded by the Minkowski sum of the bounding box
219+
pub fn getExpandedPlanes(self: *const Solid, size: math.Vec3) [24]?Plane {
220+
var expanded_planes = [_]?Plane{null} ** 24;
221+
var plane_count: usize = 0;
222+
223+
if (self.faces.items.len == 0)
224+
return expanded_planes;
200225

201-
const point = bounds.center;
202226
for (self.faces.items) |*face| {
203227
var expand_dist: f32 = 0;
204228

205229
// x_axis
206-
const x_d = face.plane.normal.dot(Vec3.x_axis);
207-
if (x_d > 0) expand_dist += -x_d * x_size;
230+
const x_d = face.plane.normal.dot(math.Vec3.x_axis);
231+
if (x_d > 0) expand_dist += -x_d * size.x;
208232

209-
const x_d_n = face.plane.normal.dot(Vec3.x_axis.scale(-1));
210-
if (x_d_n > 0) expand_dist += -x_d_n * x_size;
233+
const x_d_n = face.plane.normal.dot(math.Vec3.x_axis.scale(-1));
234+
if (x_d_n > 0) expand_dist += -x_d_n * size.x;
211235

212236
// y_axis
213-
const y_d = face.plane.normal.dot(Vec3.y_axis);
214-
if (y_d > 0) expand_dist += -y_d * y_size;
237+
const y_d = face.plane.normal.dot(math.Vec3.y_axis);
238+
if (y_d > 0) expand_dist += -y_d * size.y;
215239

216-
const y_d_n = face.plane.normal.dot(Vec3.y_axis.scale(-1));
217-
if (y_d_n > 0) expand_dist += -y_d_n * y_size;
240+
const y_d_n = face.plane.normal.dot(math.Vec3.y_axis.scale(-1));
241+
if (y_d_n > 0) expand_dist += -y_d_n * size.y;
218242

219243
// z_axis
220-
const z_d = face.plane.normal.dot(Vec3.z_axis);
221-
if (z_d > 0) expand_dist += -z_d * z_size;
244+
const z_d = face.plane.normal.dot(math.Vec3.z_axis);
245+
if (z_d > 0) expand_dist += -z_d * size.z;
222246

223-
const z_d_n = face.plane.normal.dot(Vec3.z_axis.scale(-1));
224-
if (z_d_n > 0) expand_dist += -z_d_n * z_size;
247+
const z_d_n = face.plane.normal.dot(math.Vec3.z_axis.scale(-1));
248+
if (z_d_n > 0) expand_dist += -z_d_n * size.z;
225249

226250
var expandedface = face.plane;
227251
expandedface.d += expand_dist;
228252

229-
if (expandedface.testPoint(point) == .FRONT)
230-
return false;
253+
expanded_planes[plane_count] = expandedface;
254+
plane_count += 1;
231255
}
256+
257+
// Make the Minkowski sum of both bounding boxes
258+
var solid_bounds = self.bounds;
259+
solid_bounds.min.x -= size.x;
260+
solid_bounds.min.y -= size.y;
261+
solid_bounds.min.z -= size.z;
262+
263+
solid_bounds.max.x += size.x;
264+
solid_bounds.max.y += size.y;
265+
solid_bounds.max.z += size.z;
266+
267+
// Can use the sum as our bevel planes
268+
const bevel_planes = solid_bounds.getPlanes();
269+
for (bevel_planes) |p| {
270+
expanded_planes[plane_count] = p;
271+
plane_count += 1;
272+
}
273+
274+
return expanded_planes;
275+
}
276+
277+
pub fn checkBoundingBoxCollision(self: *const Solid, bounds: BoundingBox) bool {
278+
const x_size = (bounds.max.x - bounds.min.x) * 0.5;
279+
const y_size = (bounds.max.y - bounds.min.y) * 0.5;
280+
const z_size = (bounds.max.z - bounds.min.z) * 0.5;
281+
const point = bounds.center;
282+
283+
const expanded_planes = self.getExpandedPlanes(Vec3.new(x_size, y_size, z_size));
284+
for (expanded_planes) |ep| {
285+
if (ep) |p| {
286+
if (p.testPoint(point) == .FRONT)
287+
return false;
288+
}
289+
}
290+
232291
return true;
233292
}
293+
294+
pub fn checkBoundingBoxCollisionWithVelocity(self: *const Solid, bounds: BoundingBox, velocity: Vec3) ?QuakeMapHit {
295+
var worldhit: ?QuakeMapHit = null;
296+
297+
const size = bounds.max.sub(bounds.min).scale(0.5);
298+
const planes = getExpandedPlanes(self, size);
299+
300+
const point = bounds.center;
301+
const next = point.add(velocity);
302+
303+
if (planes.len == 0)
304+
return null;
305+
306+
for (0..planes.len) |idx| {
307+
const ep = planes[idx];
308+
if (ep) |p| {
309+
if (p.testPoint(next) == .FRONT)
310+
return null;
311+
312+
const hit = p.intersectLine(point, next);
313+
if (hit) |h| {
314+
var didhit = true;
315+
for (0..planes.len) |h_idx| {
316+
if (idx == h_idx)
317+
continue;
318+
319+
// check that this hit point is behind the other clip planes
320+
if (planes[h_idx]) |pp| {
321+
if (pp.testPoint(h) == .FRONT) {
322+
didhit = false;
323+
break;
324+
}
325+
}
326+
}
327+
328+
if (didhit) {
329+
worldhit = .{
330+
.loc = h,
331+
.plane = p,
332+
};
333+
}
334+
}
335+
}
336+
}
337+
338+
return worldhit;
339+
}
234340
};
235341

236342
pub const QuakeMaterial = struct {

0 commit comments

Comments
 (0)