// Decal construction and drawing // Copyright (C) 2002 David Rosen // Copyright (C) 2003 Steven Fuller // 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 Allocator = std.mem.Allocator; const Child = std.meta.Child; const Model = model.Model; const MultiArrayList = std.MultiArrayList; const Textures = @import("Textures.zig"); const XYZ = geom.XYZ; const c = @import("cimport.zig"); const geom = @import("geom.zig"); const maxInt = std.math.maxInt; const model = @import("model.zig"); const norm = geom.norm; const segCrossModelTrans = model.segCrossModelTrans; const segCrossTrigon = geom.segCrossTrigon; const splat = geom.splat; const std = @import("std"); const ones: @Vector(3, f32) = @splat(1.0); fn cross(u: @Vector(3, f32), v: @Vector(3, f32)) @Vector(3, f32) { return .{ u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0], }; } fn normalize(vector: anytype, unit: Child(@TypeOf(vector))) @TypeOf(vector) { const d = norm(vector) / unit; if (d == 0) return vector; return vector / @as(@TypeOf(vector), @splat(d)); } // TODO: use smaller int const Kind = enum(c_int) { bullet_hole, crater, blood_pool }; const Mapping = struct { vert: XYZ, tex: struct { x: f32, y: f32 }, }; const Decal = struct { kind: Kind, corners: [4]Mapping, time: f32, }; const Decals = @This(); allocator: Allocator, textures: *const Textures, list: MultiArrayList(Decal), pub fn init(allocator: Allocator, textures: *const Textures) Decals { return .{ .allocator = allocator, .textures = textures, .list = .{}, }; } pub fn deinit(self: *Decals) void { self.list.deinit(self.allocator); } export fn addDecal(d: *Decals, kind: Kind, location: XYZ, size: f32, normal: XYZ, poly: c_int, m: *const Model, move: XYZ, rot: f32) void { const n: @Vector(3, f32) = @bitCast(normal); const abs_n = @abs(n); var major: u2 = 0; if (abs_n[1] > abs_n[major]) major = 1; if (abs_n[2] > abs_n[major]) major = 2; const r: @Vector(3, f32) = if (@reduce(.And, abs_n != ones)) cross(switch (major) { 0 => .{ 1.0, 0.0, 0.0 }, 1 => .{ 0.0, 1.0, 0.0 }, 2 => .{ 0.0, 0.0, 1.0 }, else => unreachable, }, n) else if (major == 0 and normal.x > 0 or major == 1) .{ 0.0, 0.0, -1.0 } else if (major == 0) .{ 0.0, 0.0, 1.0 } else .{ normal.z, 0.0, 0.0 }; const up = normalize(cross(n, r), size / 3.0); const right = normalize(r, size / 3.0); const loc: @Vector(3, f32) = @bitCast(location); const face = m.faces[@intCast(poly)]; const n_eps = n * splat(3, @as(f32, 0.02)); const n_eps2 = n * splat(3, @as(f32, 0.04)); var decal = Decal{ .kind = kind, .corners = undefined, .time = switch (kind) { .bullet_hole, .crater => 10.0, .blood_pool => 20.0, }, }; outer: for ([_]f32{ -1, 1, 1, -1 }, [_]f32{ -1, -1, 1, 1 }, &decal.corners) |x, y, *corner| { var p = loc + right * splat(3, x) + up * splat(3, y); var temp: XYZ = undefined; if (move.x == 0 and move.y == 0 and move.z == 0 and rot == 0 or segCrossTrigon(@bitCast(p + n_eps2), @bitCast(p - n_eps2), &face[0].position, &face[1].position, &face[2].position, &normal, &temp)) { corner.vert = @bitCast(p + n_eps); corner.tex = .{ .x = x * 0.5 + 0.5, .y = y * 0.5 + 0.5 }; continue :outer; } const delta = @max(0.01, @min(1.0 / size, 0.2)); var kx: f32 = 1.0; while (kx > 0) : (kx -= delta) { var ky: f32 = 1.0; while (ky > 0) : (ky -= delta) { p = loc + right * splat(3, x * kx) + up * splat(3, y * ky); if (segCrossModelTrans(@bitCast(p + n_eps2), @bitCast(p - n_eps2), m, move, rot, &temp) == poly) { corner.vert = @bitCast(p + n_eps); corner.tex = .{ .x = x * kx * 0.5 + 0.5, .y = y * ky * 0.5 + 0.5, }; continue :outer; } } } return; } d.list.append(d.allocator, decal) catch unreachable; } pub fn draw(self: Decals) void { c.glDepthFunc(c.GL_LEQUAL); c.glEnable(c.GL_CULL_FACE); c.glEnable(c.GL_TEXTURE_2D); c.glEnable(c.GL_LIGHTING); c.glDepthMask(0); c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA); c.glEnable(c.GL_POLYGON_OFFSET_FILL); c.glEnable(c.GL_COLOR_MATERIAL); const s = self.list.slice(); for (s.items(.kind), s.items(.corners), s.items(.time)) |k, corners, t| { switch (k) { .bullet_hole => { c.glColor4f(1.0, 1.0, 1.0, t / 10.0); c.glBindTexture(c.GL_TEXTURE_2D, self.textures.bullet_hole); }, .crater => { c.glColor4f(1.0, 1.0, 1.0, t / 10.0); c.glBindTexture(c.GL_TEXTURE_2D, self.textures.crater); }, .blood_pool => { const alpha = if (t > 18.0) @mod(20.0 - t, 0.2) + 0.8 else t / 18.0; c.glColor4f(1.0, 1.0, 1.0, alpha); const i: usize = @intFromFloat(100.0 - t * 5.0); c.glBindTexture(c.GL_TEXTURE_2D, self.textures.blood_pool[@min(i, 10)]); }, } c.glPushMatrix(); defer c.glPopMatrix(); c.glBegin(c.GL_TRIANGLE_FAN); defer c.glEnd(); for (corners) |corner| { c.glTexCoord2f(corner.tex.x, corner.tex.y); c.glVertex3f(corner.vert.x, corner.vert.y, corner.vert.z); } } } pub fn update(self: *Decals) void { const times = self.list.items(.time); self.list.sort(struct { times: []f32, pub fn lessThan(ctx: @This(), i: usize, j: usize) bool { return ctx.times[i] > ctx.times[j]; // descending time } }{ .times = times }); self.list.shrinkAndFree(self.allocator, for (times, 0..) |*t, i| { t.* -= c.multiplier; if (t.* <= 0.0) break i; } else self.list.len); }