Backlog/extras/bsp/src/QuakeMap.zig

424 lines
14 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
const TokenIterator = std.mem.TokenIterator;
pub const za = @import("zalgebra/main.zig");
pub const Vec3 = za.Vec3;
pub const Vec3d = za.Vec3_f64;
const core = @import("Backlog").core;
const logger = std.log.scoped(.quakemap);
const QuakeMap = @This();
worldspawn: Entity,
entities: std.ArrayList(Entity),
allocator: std.mem.Allocator,
// uniqueMaterials: std.StringHashMap(u32), // counts for the number
// of times a material is used.
pub const ErrorInfo = struct {
line_number: usize,
};
pub fn deinit(self: *@This()) void {
for (self.entities.items) |*entity| {
entity.deinit();
}
self.worldspawn.deinit();
self.entities.deinit();
}
pub fn read(allocator: Allocator, data: []const u8, error_info: *ErrorInfo) !QuakeMap {
var worldspawn: ?Entity = null;
var entities = std.ArrayList(Entity){};
var iter = std.mem.tokenizeAny(u8, data, "\r\n");
error_info.line_number = 0;
while (iter.next()) |line| {
error_info.line_number += 1;
switch (line[0]) {
'/' => continue,
'{' => {
const entity = try readEntity(allocator, &iter, error_info);
if (std.mem.eql(u8, entity.classname, "worldspawn")) {
worldspawn = entity;
} else {
try entities.append(allocator, entity);
}
},
else => {
std.debug.print("error in line: {d} {s}\n", .{ error_info.line_number, line });
return error.UnexpectedToken;
},
}
}
return .{
.worldspawn = worldspawn orelse return error.WorldSpawnNotFound,
.entities = entities,
.allocator = allocator,
};
}
const Property = struct {
key: []const u8,
value: []const u8,
};
pub const Entity = struct {
classname: []const u8,
spawnflags: u32,
properties: std.ArrayList(Property),
solids: std.ArrayList(Solid),
allocator: std.mem.Allocator,
pub fn deinit(self: *@This()) void {
for (self.solids.items) |*solid| {
solid.faces.deinit();
}
self.solids.deinit(self.allocator);
self.properties.deinit(self.allocator);
}
pub fn init(allocator: Allocator) Entity {
return .{
.classname = &.{},
.spawnflags = 0,
.allocator = allocator,
.properties = std.ArrayList(Property){},
.solids = std.ArrayList(Solid){},
};
}
fn indexOfProperty(self: Entity, key: []const u8) ?usize {
for (self.properties.items, 0..) |property, i| {
if (std.mem.eql(u8, property.key, key)) {
return i;
}
}
return null;
}
pub fn hasProperty(self: Entity, key: []const u8) bool {
return self.indexOfProperty(key) != null;
}
pub fn getStringProperty(self: Entity, key: []const u8) ![]const u8 {
const i = self.indexOfProperty(key) orelse return error.NotFound;
return self.properties.items[i].value;
}
pub fn getIntProperty(self: Entity, key: []const u8) !i32 {
const string = try self.getStringProperty(key);
return try parseInt(string);
}
pub fn getFloatProperty(self: Entity, key: []const u8) !f32 {
const string = try self.getStringProperty(key);
return try parseFloat(string);
}
pub fn getVec3Property(self: Entity, key: []const u8) !Vec3 {
const string = try self.getStringProperty(key);
var it = std.mem.tokenizeScalar(u8, string, ' ');
var vec3: Vec3 = undefined;
for (0..3) |i| {
vec3.data[i] = try parseFloat(it.next() orelse return error.ExpectedFloat);
}
return vec3;
}
};
pub const Solid = struct {
faces: std.ArrayList(Face),
allocator: std.mem.Allocator,
fn init(allocator: Allocator) Solid {
return .{ .faces = std.ArrayList(Face){}, .allocator = allocator };
}
fn computeVertices(self: *Solid) !void {
const allocator = self.allocator;
var buffer: [64]Vec3d = undefined;
var vertices = std.ArrayListUnmanaged(Vec3d).initBuffer(buffer[0..32]);
var clipped = std.ArrayListUnmanaged(Vec3d).initBuffer(buffer[32..64]);
for (self.faces.items, 0..) |*face, i| {
const quad = face.plane.makeQuadWithRadius(1000000.0);
vertices.clearRetainingCapacity();
vertices.appendSliceAssumeCapacity(&quad);
// clip with other planes
for (self.faces.items, 0..) |clip_face, j| {
if (j == i) continue;
clipped.clearRetainingCapacity();
try clip(vertices, clip_face.plane, &clipped);
if (clipped.items.len < 3) return error.DegenerateFace;
std.mem.swap(std.ArrayListUnmanaged(Vec3d), &vertices, &clipped);
}
face.vertices = try allocator.dupe(Vec3d, vertices.items);
}
}
fn clip(vertices: std.ArrayListUnmanaged(Vec3d), clip_plane: Plane, clipped: *std.ArrayListUnmanaged(Vec3d)) !void {
const epsilon = 0.0001;
var buffer: [32]f64 = undefined;
var distances = std.ArrayListUnmanaged(f64).initBuffer(&buffer);
var cb: usize = 0;
var cf: usize = 0;
for (vertices.items) |vertex| {
var distance = clip_plane.normal.dot(vertex) + clip_plane.d;
if (distance < -epsilon) {
cb += 1;
} else if (distance > epsilon) {
cf += 1;
} else {
distance = 0;
}
distances.appendAssumeCapacity(distance);
}
if (cb == 0 and cf == 0) {
// co-planar
return;
} else if (cb == 0) {
// all vertices in front
return;
} else if (cf == 0) {
// all vertices in back;
// keep
clipped.appendSliceAssumeCapacity(vertices.items);
return;
}
for (vertices.items, 0..) |s, i| {
const j = (i + 1) % vertices.items.len;
const e = vertices.items[j];
const sd = distances.items[i];
const ed = distances.items[j];
if (sd <= 0) clipped.appendAssumeCapacity(s); // back
if ((sd < 0 and ed > 0) or (ed < 0 and sd > 0)) {
const t = sd / (sd - ed);
var intersect = Vec3d.lerp(s, e, t);
// use plane's distance from origin, if plane's normal is a unit vector
if (clip_plane.normal.x() == 1) intersect.data[0] = -clip_plane.d;
if (clip_plane.normal.x() == -1) intersect.data[0] = clip_plane.d;
if (clip_plane.normal.y() == 1) intersect.data[1] = -clip_plane.d;
if (clip_plane.normal.y() == -1) intersect.data[1] = clip_plane.d;
if (clip_plane.normal.z() == 1) intersect.data[2] = -clip_plane.d;
if (clip_plane.normal.z() == -1) intersect.data[2] = clip_plane.d;
clipped.appendAssumeCapacity(intersect);
}
}
}
};
fn closestAxis(v: Vec3d) Vec3d {
if (@abs(v.x()) >= @abs(v.y()) and @abs(v.x()) >= @abs(v.z())) return Vec3d.right(); // 1 0 0
if (@abs(v.y()) >= @abs(v.z())) return Vec3d.up(); // 0 1 0
return Vec3d.forward(); // 0 0 1
}
pub const Face = struct {
plane: Plane,
texture_name: []const u8,
u_axis: Vec3,
v_axis: Vec3,
shift_x: f32,
shift_y: f32,
rotation: f32,
scale_x: f32,
scale_y: f32,
uv_direction: Vec3,
vertices: []Vec3d,
};
const Plane = struct {
normal: Vec3d,
d: f64,
fn initFromVertices(v0: Vec3d, v1: Vec3d, v2: Vec3d) Plane {
const v0v1 = v1.sub(v0);
const v0v2 = v2.sub(v0);
const normal = Vec3d.cross(v0v1, v0v2).norm();
const length = normal.dot(v0);
return .{ .normal = normal, .d = -length };
}
fn makeQuadWithRadius(self: Plane, radius: f32) [4]Vec3d {
const direction = closestAxis(self.normal);
var up = if (direction.z() == 1) Vec3d.right() else Vec3d.new(0, 0, -1);
const upv = up.dot(self.normal);
up = up.sub(self.normal.scale(upv)).norm();
var right = up.cross(self.normal);
up = up.scale(radius);
right = right.scale(radius);
const origin = self.normal.scale(-self.d);
return .{
origin.sub(right).sub(up),
origin.add(right).sub(up),
origin.add(right).add(up),
origin.sub(right).add(up),
};
}
};
fn readEntity(allocator: Allocator, iter: *TokenIterator(u8, .any), error_info: *ErrorInfo) !Entity {
var entity = Entity.init(allocator);
while (iter.next()) |line| {
error_info.line_number += 1;
switch (line[0]) {
'/' => continue,
'"' => {
const property = try readProperty(line);
if (std.mem.eql(u8, property.key, "classname")) {
entity.classname = property.value;
} else if (std.mem.eql(u8, property.key, "spawnflags")) {
entity.spawnflags = try std.fmt.parseInt(u32, property.value, 10);
} else {
try entity.properties.append(entity.allocator, property);
}
},
'{' => try entity.solids.append(entity.allocator, try readSolid(allocator, iter, error_info)),
'}' => break,
else => {
std.debug.print("error in line: {s}\n", .{line});
return error.UnexpectedToken;
},
}
}
return entity;
}
fn readProperty(line: []const u8) !Property {
var property: Property = undefined;
var iter = std.mem.tokenizeScalar(u8, line, '"');
property.key = try readSymbol(&iter);
if (!std.mem.eql(u8, iter.next() orelse return error.UnexpectedEof, " ")) return error.ExpectedSpace;
property.value = try readSymbol(&iter);
return property;
}
fn readSolid(allocator: Allocator, iter: *TokenIterator(u8, .any), error_info: *ErrorInfo) !Solid {
var solid = Solid.init(allocator);
while (iter.next()) |line| {
error_info.line_number += 1;
switch (line[0]) {
'/' => continue,
'(' => try solid.faces.append(solid.allocator, try readFace(line)),
'}' => break,
else => {
std.debug.print("error in line: {s}\n", .{line});
return error.UnexpectedToken;
},
}
}
try solid.computeVertices();
return solid;
}
pub fn calculateRotatedUV(face: Face, u_axis: *Vec3, v_axis: *Vec3) void {
const scaled_u_axis = face.u_axis.scale(1.0 / face.scale_x);
const scaled_v_axis = face.v_axis.scale(1.0 / face.scale_y);
const u_mat = za.Mat4.fromTranslate(scaled_u_axis);
const v_mat = za.Mat4.fromTranslate(scaled_v_axis);
const rotation = za.Mat4.fromRotation(face.rotation, face.uv_direction);
u_axis.* = rotation.mul(u_mat).extractTranslation();
v_axis.* = rotation.mul(v_mat).extractTranslation();
}
fn readFace(line: []const u8) !Face {
var face: Face = undefined;
var iter = std.mem.tokenizeScalar(u8, line, ' ');
const v0 = try readPoint(&iter);
const v1 = try readPoint(&iter);
const v2 = try readPoint(&iter);
// map planes are clockwise, flip them around when computing the plane to get a counter-clockwise plane
face.plane = Plane.initFromVertices(v2, v1, v0);
const closestNormal = closestAxis(face.plane.normal);
face.uv_direction.data = .{
@floatCast(closestNormal.data[0]),
@floatCast(closestNormal.data[1]),
@floatCast(closestNormal.data[2]),
};
face.u_axis = if (closestNormal.x() == 1) Vec3.new(0, 1, 0) else Vec3.new(1, 0, 0);
face.v_axis = if (closestNormal.z() == 1) Vec3.new(0, -1, 0) else Vec3.new(0, 0, -1);
face.texture_name = try readSymbol(&iter);
face.shift_x = try readDecimal(&iter);
face.shift_y = try readDecimal(&iter);
face.rotation = core.radians(try readDecimal(&iter));
face.scale_x = try readDecimal(&iter);
face.scale_y = try readDecimal(&iter);
return face;
}
fn readPoint(iter: *TokenIterator(u8, .scalar)) !Vec3d {
var point: Vec3d = undefined;
if (!std.mem.eql(u8, iter.next() orelse return error.UnexpectedEof, "(")) return error.ExpectedOpenParanthesis;
point.data[0] = try readDecimal(iter);
point.data[1] = try readDecimal(iter);
point.data[2] = try readDecimal(iter);
if (!std.mem.eql(u8, iter.next() orelse return error.UnexpectedEof, ")")) return error.ExpectedCloseParanthesis;
return point;
}
fn readDecimal(iter: *TokenIterator(u8, .scalar)) !f32 {
const string = iter.next() orelse return error.UnexpectedEof;
return try parseFloat(string);
}
fn readSymbol(iter: *TokenIterator(u8, .scalar)) ![]const u8 {
return iter.next() orelse return &.{};
}
// simpler float parsing function that runs quicker in debug
fn parseFloat(string: []const u8) !f32 {
var signed: bool = false;
var decimal_point: usize = string.len - 1;
var decimal: f64 = 0;
for (string, 0..) |c, i| {
switch (c) {
'-' => {
if (i == 0) signed = true else return error.UnexpectedCharacter;
},
'0'...'9' => {
const digit: f64 = @floatFromInt(c - '0');
decimal = 10 * decimal + digit;
},
'.' => decimal_point = i,
else => return error.UnexpectedCharacter,
}
}
if (signed) decimal *= -1;
if (decimal_point < string.len - 1) {
const denom = std.math.pow(f64, 10, @floatFromInt(string.len - 1 - decimal_point));
decimal /= denom;
}
return @floatCast(decimal);
}
fn parseInt(string: []const u8) !i32 {
var signed: bool = false;
var decimal: i32 = 0;
for (string, 0..) |c, i| {
switch (c) {
'-' => {
if (i == 0) signed = true else return error.UnexpectedCharacter;
},
'0'...'9' => {
const digit: i32 = @intCast(c - '0');
decimal = 10 * decimal + digit;
},
else => return error.UnexpectedCharacter,
}
}
return decimal;
}