// 3D model // Copyright (C) 2002 David Rosen // Copyright (C) 2023, 2025 Nguyễn Gia Phong // // This file is part of Black Shades. // // Black Shades is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Black Shades is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Black Shades. If not, see . const TokenIterator = std.mem.TokenIterator(u8, .scalar); const XYZ = geom.XYZ; const allocator = std.heap.c_allocator; const assert = std.debug.assert; const c = @import("cimport.zig"); const crossProduct = geom.crossProduct; const cwd = std.fs.cwd; const data_dir = misc.data_dir; const degreesToRadians = std.math.degreesToRadians; const endsWith = std.mem.endsWith; const eql = std.mem.eql; const floatMax = std.math.floatMax; const geom = @import("geom.zig"); const misc = @import("misc.zig"); const normalize = geom.normalize; const parseFloat = std.fmt.parseFloat; const parseInt = std.fmt.parseInt; const rotate2d = geom.rotate2d; const readFile = misc.readFile; const segCrossSphere = geom.segCrossSphere; const segCrossTrigon = geom.segCrossTrigon; const sep = std.fs.path.sep; const splat = geom.splat; const sqr = geom.sqr; const startsWith = std.mem.startsWith; const std = @import("std"); const tokenizeScalar = std.mem.tokenizeScalar; const Vertex = extern struct { position: XYZ, normal: XYZ, color: [3]f32, }; const Face = [3]Vertex; pub const Model = extern struct { faces: [*]Face, face_count: u16, center: XYZ, radius: f32, }; /// Object File Format tokenizer const OffIterator = struct { token_iterator: TokenIterator, pub fn init(buffer: []const u8) OffIterator { var self = OffIterator{ .token_iterator = tokenizeScalar(u8, buffer, '\n'), }; if (!endsWith(u8, self.token_iterator.next().?, "OFF")) self.token_iterator.reset(); return self; } pub fn next(self: *OffIterator) ?TokenIterator { while (self.token_iterator.next()) |line| { var words = tokenizeScalar(u8, line, ' '); if (words.next()) |word| { // not empty if (!startsWith(u8, word, "#")) { // not comment words.reset(); return words; } } } return null; } }; export fn loadModel(path: [*:0]const u8) Model { const file = readFile(cwd(), data_dir ++ "models{c}{s}", .{ sep, path, }) catch unreachable; defer allocator.free(file); var lines = OffIterator.init(file); var counts = lines.next().?; const vertex_count = parseInt(u16, counts.next().?, 10) catch unreachable; const face_count = parseInt(u16, counts.next().?, 10) catch unreachable; const vertices = allocator.alloc(@Vector(3, f32), vertex_count) catch unreachable; defer allocator.free(vertices); for (vertices) |*position| { var numbers = lines.next().?; position.* = .{ parseFloat(f32, numbers.next().?) catch unreachable, parseFloat(f32, numbers.next().?) catch unreachable, parseFloat(f32, numbers.next().?) catch unreachable, }; } const center = blk: { var sum = @Vector(3, f32){0.0, 0.0, 0.0}; for (vertices) |position| sum += position; break :blk sum / splat(3, @as(f32, @floatFromInt(vertex_count))); }; const faces = allocator.alloc(Face, face_count) catch unreachable; for (faces) |*face| { var numbers = lines.next().?; assert(eql(u8, numbers.next().?, "3")); const indices = [3]u16{ parseInt(u16, numbers.next().?, 10) catch unreachable, parseInt(u16, numbers.next().?, 10) catch unreachable, parseInt(u16, numbers.next().?, 10) catch unreachable, }; const normal = normalize(crossProduct( @bitCast(vertices[indices[1]] - vertices[indices[0]]), @bitCast(vertices[indices[2]] - vertices[indices[0]]))); const color = [3]f32{ parseFloat(f32, numbers.next().?) catch unreachable, parseFloat(f32, numbers.next().?) catch unreachable, parseFloat(f32, numbers.next().?) catch unreachable, }; for (face, indices) |*vertex, index| vertex.* = .{ .position = @bitCast(vertices[index]), // OpenGL has no concept of face normal or color. .normal = normal, .color = color, }; } return .{ .faces = faces.ptr, .face_count = face_count, .center = @bitCast(center), .radius = blk: { var square: f32 = 0; for (vertices) |position| square = @max(square, @reduce(.Add, sqr(position - center))); break :blk @sqrt(square); }, }; } export fn destroyModel(m: *const Model) void { allocator.free(m.faces[0..m.face_count]); } pub export fn drawModel(m: *const Model, color: ?*const [3]f32) void { c.glEnableClientState(c.GL_VERTEX_ARRAY); c.glVertexPointer(3, c.GL_FLOAT, @sizeOf(Vertex), &m.faces[0][0].position); c.glEnableClientState(c.GL_NORMAL_ARRAY); c.glNormalPointer(c.GL_FLOAT, @sizeOf(Vertex), &m.faces[0][0].normal); if (color) |ptr| { c.glDisableClientState(c.GL_COLOR_ARRAY); c.glColor3fv(ptr); } else { c.glEnableClientState(c.GL_COLOR_ARRAY); c.glColorPointer(3, c.GL_FLOAT, @sizeOf(Vertex), &m.faces[0][0].color); } c.glDrawArrays(c.GL_TRIANGLES, 0, @intCast(m.face_count * 3)); } pub export fn segCrossModel(start: XYZ, end: XYZ, m: *const Model, intersection: *XYZ) c_int { if (!segCrossSphere(start, end, m.center, m.radius)) return -1; const p: @Vector(3, f32) = @bitCast(start); var result: c_int = -1; var shortest = floatMax(f32); for (m.faces[0..m.face_count], 0..) |face, i| { var v: @Vector(3, f32) = undefined; if (!segCrossTrigon(start, end, &face[0].position, &face[1].position, &face[2].position, &face[0].normal, @ptrCast(&v))) continue; const distance = @reduce(.Add, sqr(v - p)); if (distance < shortest or result == -1) { shortest = distance; result = @intCast(i); assert(i < m.face_count); intersection.* = @bitCast(v); } } return result; } pub export fn segCrossModelTrans(start: XYZ, end: XYZ, m: *const Model, move: XYZ, rot: f32, intersection: *XYZ) c_int { const t: @Vector(3, f32) = @bitCast(move); var p = @as(@Vector(3, f32), @bitCast(start)) - t; rotate2d(&p[2], &p[0], degreesToRadians(-rot)); var q = @as(@Vector(3, f32), @bitCast(end)) - t; rotate2d(&q[2], &q[0], degreesToRadians(-rot)); defer { rotate2d(&intersection.z, &intersection.x, degreesToRadians(rot)); intersection.* = @bitCast(@as(@Vector(3, f32), @bitCast(intersection.*)) + t); } return segCrossModel(@bitCast(p), @bitCast(q), m, intersection); }