about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Decals.h20
-rw-r--r--src/Decals.zig214
-rw-r--r--src/Game.h3
-rw-r--r--src/GameDraw.cpp23
-rw-r--r--src/GameInitDispose.cpp24
-rw-r--r--src/GameTick.cpp28
-rw-r--r--src/Globals.cpp1
-rw-r--r--src/Sprites.cpp6
-rw-r--r--src/Textures.zig85
-rw-r--r--src/cimport.zig1
-rw-r--r--src/decal.zig247
-rw-r--r--src/main.zig11
-rw-r--r--src/misc.zig38
13 files changed, 326 insertions, 375 deletions
diff --git a/src/Decals.h b/src/Decals.h
index 4eb6cf7..743344e 100644
--- a/src/Decals.h
+++ b/src/Decals.h
@@ -25,32 +25,14 @@
 #include "Models.h"
 #include "Quaternions.h"
 
-#define MAX_DECALS 120
-
 enum decal { BULLET_HOLE, CRATER, BLOOD_POOL };
 
-struct Decals {
-	GLuint hole_textures[2];
-	GLuint blood_textures[11];
-
-	GLuint len;
-	enum decal kind[MAX_DECALS];
-	XYZ points[MAX_DECALS * 8];
-	GLuint numpoints[MAX_DECALS];
-	GLfloat texcoordsx[MAX_DECALS * 8];
-	GLfloat texcoordsy[MAX_DECALS * 8];
-	GLfloat alive[MAX_DECALS];
-};
-
 #ifdef __cplusplus
 extern "C" {
 #endif // __cplusplus
-void addDecal(struct Decals *d, enum decal kind, XYZ location, float size,
+void addDecal(void *decals, enum decal kind, XYZ location, float size,
               XYZ normal, int poly, const struct Model *model,
               XYZ move, float rotation);
-void updateDecals(struct Decals *d);
-void drawDecals(struct Decals *d);
-void destroyDecals(struct Decals *d);
 #ifdef __cplusplus
 } // extern "C"
 #endif // __cplusplus
diff --git a/src/Decals.zig b/src/Decals.zig
new file mode 100644
index 0000000..ecbc71c
--- /dev/null
+++ b/src/Decals.zig
@@ -0,0 +1,214 @@
+// Decal construction and drawing
+// Copyright (C) 2002  David Rosen
+// Copyright (C) 2003  Steven Fuller
+// Copyright (C) 2023  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 <https://www.gnu.org/licenses/>.
+
+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 = @fabs(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);
+}
diff --git a/src/Game.h b/src/Game.h
index 5a0d831..363f4f6 100644
--- a/src/Game.h
+++ b/src/Game.h
@@ -109,6 +109,7 @@ struct Game {
 	GLuint deadpersonspritetextureptr;
 	GLuint scopetextureptr;
 	GLuint flaretextureptr;
+	void *decals;
 
 	bool gameinprogress;
 	bool beatgame;
@@ -136,7 +137,7 @@ typedef struct Game Game;
 #ifdef __cplusplus
 extern "C" {
 #endif // __cplusplus
-	Game* makeGame(struct Config config, struct Scores scores);
+	Game* makeGame(void *decals, struct Config config, struct Scores scores);
 	void initGl(Game* game);
 	void initGame(Game* game);
 	void resizeWindow(Game* game, int width, int height);
diff --git a/src/GameDraw.cpp b/src/GameDraw.cpp
index 94e7e3b..5451adb 100644
--- a/src/GameDraw.cpp
+++ b/src/GameDraw.cpp
@@ -40,7 +40,6 @@ extern float fogcolorb;
 extern float sinefluct;
 extern float sinefluctprog;
 extern int environment;
-extern Decals decals;
 
 GLvoid ReSizeGLScene(Game* game, float fov, float near)
 {
@@ -88,7 +87,6 @@ void Game::DrawGLScene(void)
 			glRotatef(sinefluctprog*50,0,0,1);
 			glEnable(GL_TEXTURE_2D);
 			glBindTexture(GL_TEXTURE_2D,  sprites.smoketextureptr);
-			glEnable(GL_BLEND);
 			glColor4f(1,1,1,.4+sinefluct/8);
 			glBegin(GL_TRIANGLES);
 				glTexCoord2f(1.0f, 1.0f); glVertex3f( 1, 1, 0.0f);
@@ -108,7 +106,6 @@ void Game::DrawGLScene(void)
 			glRotatef(-sinefluctprog*50,0,0,1);
 			glEnable(GL_TEXTURE_2D);
 			glBindTexture(GL_TEXTURE_2D,  sprites.smoketextureptr);
-			glEnable(GL_BLEND);
 			glColor4f(1,1,1,.4-sinefluct/8);
 			glBegin(GL_TRIANGLES);
 				glTexCoord2f(1.0f, 1.0f); glVertex3f( 1, 1, 0.0f);
@@ -154,7 +151,6 @@ void Game::DrawGLScene(void)
 			glEnable(GL_TEXTURE_2D);
 			if(mouseoverbutton!=1)glBindTexture(GL_TEXTURE_2D,  sprites.smoketextureptr);
 			if(mouseoverbutton==1)glBindTexture(GL_TEXTURE_2D,  flaretextureptr);
-			glEnable(GL_BLEND);
 			glColor4f(1,0,0,.5+sinefluct/6);
 			glBegin(GL_TRIANGLES);
 				glTexCoord2f(1.0f, 1.0f); glVertex3f( 1, 1, 0.0f);
@@ -175,7 +171,6 @@ void Game::DrawGLScene(void)
 			glEnable(GL_TEXTURE_2D);
 			if(mouseoverbutton!=1)glBindTexture(GL_TEXTURE_2D,  sprites.smoketextureptr);
 			if(mouseoverbutton==1)glBindTexture(GL_TEXTURE_2D,  flaretextureptr);
-			glEnable(GL_BLEND);
 			glColor4f(1,0,0,.5-sinefluct/6);
 			glBegin(GL_TRIANGLES);
 				glTexCoord2f(1.0f, 1.0f); glVertex3f( 1, 1, 0.0f);
@@ -224,7 +219,6 @@ void Game::DrawGLScene(void)
 			if(mouseoverbutton!=2)glBindTexture(GL_TEXTURE_2D,  sprites.smoketextureptr);
 			if(mouseoverbutton==2)glBindTexture(GL_TEXTURE_2D,  flaretextureptr);
 
-			glEnable(GL_BLEND);
 			glColor4f(1,0,0,.5+sinefluct/6);
 			glBegin(GL_TRIANGLES);
 				glTexCoord2f(1.0f, 1.0f); glVertex3f( 1, 1, 0.0f);
@@ -245,7 +239,6 @@ void Game::DrawGLScene(void)
 			glEnable(GL_TEXTURE_2D);
 			if(mouseoverbutton!=2)glBindTexture(GL_TEXTURE_2D,  sprites.smoketextureptr);
 			if(mouseoverbutton==2)glBindTexture(GL_TEXTURE_2D,  flaretextureptr);
-			glEnable(GL_BLEND);
 			glColor4f(1,0,0,.5-sinefluct/6);
 			glBegin(GL_TRIANGLES);
 				glTexCoord2f(1.0f, 1.0f); glVertex3f( 1, 1, 0.0f);
@@ -292,7 +285,6 @@ void Game::DrawGLScene(void)
 			glLoadIdentity();								// Reset The Modelview Matrix
 			glScalef(screenwidth,screenheight,1);
 			glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
-			glEnable(GL_BLEND);
 			glColor4f(flashr,flashg,flashb,flashamount);
 			glBegin(GL_QUADS);
 				glVertex3f(0,		0, 	 0.0f);
@@ -306,10 +298,10 @@ void Game::DrawGLScene(void)
 			glPopMatrix();										// Restore The Old Projection Matrix
 			glEnable(GL_DEPTH_TEST);							// Enables Depth Testing
 			glEnable(GL_CULL_FACE);
-			glDisable(GL_BLEND);
 			glDepthMask(1);
 		}
 	} else { // in-game
+		glDepthMask(1);
 		glLoadIdentity();
 		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 		glEnable(GL_DEPTH_TEST);
@@ -464,7 +456,6 @@ void Game::DrawGLScene(void)
 			glEnable(GL_LIGHTING);
 
 		//Draw blocks
-		glEnable(GL_BLEND);
 		XYZ move;
 		int beginx,endx;
 		int beginz,endz;
@@ -515,8 +506,6 @@ void Game::DrawGLScene(void)
 			}
 		}
 
-		drawDecals(&decals);
-
 		// Occluding blocks
 		beginx=(camera.position.x+block_spacing/2)/block_spacing-2;
 		if(beginx<0)beginx=0;
@@ -545,7 +534,6 @@ void Game::DrawGLScene(void)
 
 		glColor4f(1 , 1, 1, 1);
 		glEnable(GL_COLOR_MATERIAL);
-		glEnable(GL_BLEND);
 		for(int i=0;i<numpeople;i++){
 			bool draw = true;
 			if (((!person[i].skeleton.free
@@ -609,7 +597,6 @@ void Game::DrawGLScene(void)
 					glPushMatrix();
 						if(person[i].skeleton.free<1)person[i].DoAnimationslite(i);
 						glColor4f(1,1,1,1);
-						glEnable(GL_BLEND);
 						glDisable(GL_CULL_FACE);
 						glEnable(GL_TEXTURE_2D);
 						glDisable(GL_LIGHTING);
@@ -651,7 +638,6 @@ void Game::DrawGLScene(void)
 					person[i].existing = 1;
 		}
 		glDisable(GL_COLOR_MATERIAL);
-		glDisable(GL_BLEND);
 
 		//Attacker psychicness
 
@@ -707,14 +693,12 @@ void Game::DrawGLScene(void)
 				glTexCoord2f(0,1);
 				glVertex3f(0, 	1, 0.0f);
 			glEnd();
-			glDisable(GL_TEXTURE_2D);
 			glMatrixMode(GL_PROJECTION);						// Select The Projection Matrix
 			glPopMatrix();										// Restore The Old Projection Matrix
 			glMatrixMode(GL_MODELVIEW);							// Select The Modelview Matrix
 			glPopMatrix();										// Restore The Old Projection Matrix
 			glEnable(GL_DEPTH_TEST);							// Enables Depth Testing
 			glEnable(GL_CULL_FACE);
-			glDisable(GL_BLEND);
 			glDepthMask(1);
 		}
 
@@ -754,7 +738,6 @@ void Game::DrawGLScene(void)
 
 			glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
 
-			glEnable(GL_BLEND);
 
 			glColor4f(flashr,flashg,flashb,flashamount);
 
@@ -782,7 +765,6 @@ void Game::DrawGLScene(void)
 
 			glEnable(GL_CULL_FACE);
 
-			glDisable(GL_BLEND);
 
 			glDepthMask(1);
 
@@ -816,7 +798,6 @@ void Game::DrawGLScene(void)
 
 			glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
 
-			glEnable(GL_BLEND);
 
 			glColor4f(0,0,0,1-person[0].longdead);
 
@@ -844,8 +825,6 @@ void Game::DrawGLScene(void)
 
 			glEnable(GL_CULL_FACE);
 
-			glDisable(GL_BLEND);
-
 			glDepthMask(1);
 
 		}
diff --git a/src/GameInitDispose.cpp b/src/GameInitDispose.cpp
index b560b48..c2e7a2f 100644
--- a/src/GameInitDispose.cpp
+++ b/src/GameInitDispose.cpp
@@ -23,7 +23,6 @@
 #include <initializer_list>
 
 #include <AL/alc.h>
-#include <GL/glu.h>
 
 #include "Game.h"
 #include "misc.h"
@@ -33,7 +32,6 @@ extern unsigned int gSampleSet[37];
 extern Camera camera;
 extern Skeleton testskeleton;
 extern Sprites sprites;
-extern Decals decals;
 
 extern Model skeletonmodels[10];
 extern Model gunmodels[11];
@@ -213,9 +211,10 @@ void loadModels(Game* game)
 	gunmodels[shotgunmodel] = loadModel("guns/shotgun.off");
 }
 
-Game* makeGame(Config config, Scores scores)
+Game* makeGame(void *decals, Config config, Scores scores)
 {
 	auto game = new Game();
+	game->decals = decals;
 	loadModels(game); // FIXME: free models
 	game->screenwidth = config.width;
 	game->screenheight = config.height;
@@ -718,7 +717,6 @@ void initGame(Game* game)
 	glClearColor(fogcolorr,fogcolorg,fogcolorb,1);
 	game->initialized = true;
 
-	decals.len = 0;
 	sprites.howmanysprites=0;
 	game->losedelay = 1;
 }
@@ -729,6 +727,9 @@ void initGl(Game* game)
 	glDepthFunc(GL_LESS);
 	glPolygonOffset(-8,0);
 	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+	glEnable(GL_BLEND);
+	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
+	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
 
 	game->text.texture = loadTexture("font.qoi");
 	buildFont(&game->text);
@@ -743,20 +744,6 @@ void initGl(Game* game)
 	sprites.bloodtextureptr = loadTexture("sprites/blood.qoi");
 	sprites.raintextureptr = loadTexture("sprites/white.qoi");
 	sprites.snowtextureptr = loadTexture("sprites/snowflake.qoi");
-
-	decals.hole_textures[0] = loadTexture("bullet-hole.qoi");
-	decals.hole_textures[1] = loadTexture("black.qoi");
-	decals.blood_textures[0u] = loadTexture("blood/00.qoi");
-	decals.blood_textures[1u] = loadTexture("blood/01.qoi");
-	decals.blood_textures[2u] = loadTexture("blood/02.qoi");
-	decals.blood_textures[3u] = loadTexture("blood/03.qoi");
-	decals.blood_textures[4u] = loadTexture("blood/04.qoi");
-	decals.blood_textures[5u] = loadTexture("blood/05.qoi");
-	decals.blood_textures[6u] = loadTexture("blood/06.qoi");
-	decals.blood_textures[7u] = loadTexture("blood/07.qoi");
-	decals.blood_textures[8u] = loadTexture("blood/08.qoi");
-	decals.blood_textures[9u] = loadTexture("blood/09.qoi");
-	decals.blood_textures[10] = loadTexture("blood/10.qoi");
 }
 
 struct Scores getScores(Game* game)
@@ -800,5 +787,4 @@ void closeGame(Game* game)
 	alDeleteSources(34 + game->musictoggle * 3, gSourceID);
 	alDeleteBuffers(34 + game->musictoggle * 3, gSampleSet);
 	destroyModels(game);
-	destroyDecals(&decals);
 }
diff --git a/src/GameTick.cpp b/src/GameTick.cpp
index 311ba9a..68fe508 100644
--- a/src/GameTick.cpp
+++ b/src/GameTick.cpp
@@ -23,6 +23,7 @@
 // along with Black Shades.  If not, see <https://www.gnu.org/licenses/>.
 
 #include <algorithm>
+#include <cassert>
 
 #include "Game.h"
 #include "misc.h"
@@ -41,7 +42,6 @@ extern float snowdelay;
 extern float precipitationdensity;
 extern float soundscalefactor;
 extern int slomo;
-extern Decals decals;
 
 #define maxfallvel 40
 
@@ -722,18 +722,9 @@ void bleed(Game* game, size_t i)
 		XYZ move {(float) x * block_spacing, 0.0f, (float) y * block_spacing, };
 		auto whichtri = segCrossModelTrans(overpoint, underpoint,
 			&game->sidewalkcollide, move, rot, &loc);
-
-		XYZ normish {0.0f, 1.0f, 0.0f};
-		if (whichtri >= 0) {
-			addDecal(&decals, BLOOD_POOL, loc, 12, normish,
-				whichtri, &game->sidewalkcollide, move, rot);
-		} else {
-			loc = person.skeleton.joints[abdomen].position;
-			loc.y = -0.5f;
-			move = {0.0f};
-			addDecal(&decals, BLOOD_POOL, loc, 12, normish,
-				0, &game->sidewalkcollide, move, 0);
-		}
+		assert(whichtri >= 0);
+		addDecal(game->decals, BLOOD_POOL, loc, 12, {0.0f, 1.0f, 0.0f},
+			whichtri, &game->sidewalkcollide, move, rot);
 		person.firstlongdead = true;
 		return;
 	}
@@ -974,7 +965,6 @@ void Game::Tick()
 
 	spawnNpc(this);
 	sprites.DoStuff();
-	updateDecals(&decals);
 
 	// Facing
 	XYZ facing {0, 0, -1};
@@ -1739,7 +1729,7 @@ void Game::Tick()
 					hitNorm = {0.0f, 1.0f, 0.0f};
 hit_terrain:
 					whichhit = -1;
-					addDecal(&decals, BULLET_HOLE, wallhit, 0.7f,
+					addDecal(this->decals, BULLET_HOLE, wallhit, 0.7f,
 						hitNorm, whichtri, model, move, hitRot);
 					const auto& velocity = hitNorm * 3;
 					switch (person[j].whichgun) {
@@ -2096,7 +2086,7 @@ hit_terrain:
 				const auto& normalrotated = rotate(blocks[citytype[wherex][wherey]].faces[whichtri][0].normal,
 					0, cityrotation[wherex][wherey] * 90, 0);
 				if (sprites.size[i] > 1)
-					addDecal(&decals, CRATER, wallhit, 9.0f,
+					addDecal(this->decals, CRATER, wallhit, 9.0f,
 						normalrotated, whichtri,
 						&blocks[citytype[wherex][wherey]],
 						move, cityrotation[wherex][wherey] * 90);
@@ -2132,7 +2122,7 @@ hit_terrain:
 						move = {};
 						sprites.location[i].y=-.5;
 						XYZ normish = {0.0f, 1.0f, 0.0f};
-						addDecal(&decals, CRATER, sprites.location[i], 9.0f,
+						addDecal(this->decals, CRATER, sprites.location[i], 9.0f,
 							normish, 0, blocks + citytype[wherex][wherey], move, 0);
 					}
 
@@ -2257,14 +2247,14 @@ hit_terrain:
 				move, cityrotation[wherex][wherey] * 90, &temp);
 			XYZ normish = {0.0f, 1.0f, 0.0f};
 			if (whichtri > -1) {
-				addDecal(&decals, CRATER, sprites.location[i], 9.0f,
+				addDecal(this->decals, CRATER, sprites.location[i], 9.0f,
 					normish, 0, &sidewalkcollide, move,
 					cityrotation[wherex][wherey]*90);
 			} else {
 				temp = sprites.location[i];
 				temp.y = -0.5f;
 				move = {0.0f};
-				addDecal(&decals, CRATER, sprites.location[i], 9.0f,
+				addDecal(this->decals, CRATER, sprites.location[i], 9.0f,
 					normish, 0, &sidewalkcollide, move, 0);
 			}
 
diff --git a/src/Globals.cpp b/src/Globals.cpp
index e94a7fc..53aa3fd 100644
--- a/src/Globals.cpp
+++ b/src/Globals.cpp
@@ -27,7 +27,6 @@ Model skeletonmodels[10];
 Model gunmodels[11];
 Costume costume[10];
 Sprites sprites;
-Decals decals;
 int thirdperson;
 bool visions;
 Camera camera;
diff --git a/src/Sprites.cpp b/src/Sprites.cpp
index 12bb8b7..f5da5f7 100644
--- a/src/Sprites.cpp
+++ b/src/Sprites.cpp
@@ -142,9 +142,6 @@ void Sprites::draw()
 	XYZ point;
 	glAlphaFunc(GL_GREATER, 0.01);
 
-	//glEnable(GL_POLYGON_OFFSET_FILL);
-
-	glEnable(GL_BLEND);
 	glDisable(GL_CULL_FACE);
 	glEnable(GL_TEXTURE_2D);
 	glDisable(GL_LIGHTING);
@@ -245,7 +242,6 @@ void Sprites::draw()
 
 		if(type[i]==grenadesprite||type[i]==spoonsprite||type[i]==pinsprite){
 			glTranslatef(location[i].x,location[i].y+.2,location[i].z);
-			glDisable(GL_BLEND);
 			glEnable(GL_FOG);
 			glEnable(GL_CULL_FACE);
 			glDisable(GL_TEXTURE_2D);
@@ -264,7 +260,6 @@ void Sprites::draw()
 				drawModel(gunmodels + grenadepinmodel,
 					visions ? BLACK : NULL);
 
-			glEnable(GL_BLEND);
 			glDisable(GL_FOG);
 			glDisable(GL_CULL_FACE);
 			glEnable(GL_TEXTURE_2D);
@@ -320,7 +315,6 @@ void Sprites::draw()
 	glDepthMask(1);
 	glDisable(GL_TEXTURE_2D);
 	glEnable(GL_CULL_FACE);
-	//glDisable(GL_POLYGON_OFFSET_FILL);
 }
 
 Sprites::~Sprites()
diff --git a/src/Textures.zig b/src/Textures.zig
new file mode 100644
index 0000000..068db80
--- /dev/null
+++ b/src/Textures.zig
@@ -0,0 +1,85 @@
+// Texture store
+// Copyright (C) 2002  David Rosen
+// Copyright (C) 2021-2023  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 <https://www.gnu.org/licenses/>.
+
+const allocator = std.heap.c_allocator;
+const c = @import("cimport.zig");
+const cwd = std.fs.cwd;
+const data_dir = misc.data_dir;
+const formatIntBuf = std.fmt.formatIntBuf;
+const misc = @import("misc.zig");
+const qoi = @import("qoi");
+const readFile = misc.readFile;
+const sep = std.fs.path.sep;
+const span = std.mem.span;
+const std = @import("std");
+
+/// Load QOI file into an OpenGL buffer bound to the given texture.
+fn load(texture: u32, path: [*:0]const u8) !void {
+    const file = try readFile(cwd(), data_dir ++ "textures{c}{s}", .{
+        sep, path,
+    });
+    defer allocator.free(file);
+
+    var image = try qoi.decodeBuffer(allocator, file);
+    defer image.deinit(allocator);
+    const data: [*c]const u8 = @ptrCast(image.pixels.ptr);
+
+    c.glBindTexture(c.GL_TEXTURE_2D, texture);
+    defer c.glBindTexture(c.GL_TEXTURE_2D, 0);
+    c.glTexEnvi(c.GL_TEXTURE_ENV, c.GL_TEXTURE_ENV_MODE, c.GL_MODULATE);
+    c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_LINEAR);
+    c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_LINEAR);
+    c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_GENERATE_MIPMAP, c.GL_TRUE);
+
+    const width: i32 = @intCast(image.width);
+    const height: i32 = @intCast(image.height);
+    c.glPixelStorei(c.GL_UNPACK_ALIGNMENT, 1);
+    c.glTexImage2D(c.GL_TEXTURE_2D, 0, 4, width, height,
+                   0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, data);
+}
+
+export fn loadTexture(path: [*:0]const u8) u32 {
+    var texture: u32 = undefined;
+    c.glGenTextures(1, &texture);
+    load(texture, path) catch unreachable;
+    return texture;
+}
+
+const Textures = @This();
+const size = @sizeOf(Textures) / @sizeOf(u32);
+bullet_hole: u32,
+crater: u32,
+blood_pool: [11]u32,
+
+pub fn init() !Textures {
+    var self: Textures = undefined;
+    c.glGenTextures(size, @ptrCast(&self));
+    try load(self.bullet_hole, "bullet-hole.qoi");
+    try load(self.crater, "black.qoi");
+    for (0..11) |i| {
+        var buf: [2]u8 = undefined;
+        _ = formatIntBuf(&buf, i, 10, .lower, .{ .width = 2, .fill = '0' });
+        try load(self.blood_pool[i], "blood/" ++ buf ++ ".qoi");
+    }
+    return self;
+}
+
+pub fn deinit(self: Textures) void {
+    c.glDeleteTextures(size, @ptrCast(&self));
+}
diff --git a/src/cimport.zig b/src/cimport.zig
index ea29276..c55d916 100644
--- a/src/cimport.zig
+++ b/src/cimport.zig
@@ -1,7 +1,6 @@
 pub usingnamespace @cImport({
     @cInclude("AL/al.h");
     @cInclude("GL/gl.h");
-    @cInclude("GL/glu.h");
 
     @cInclude("Game.h");
     @cInclude("Constants.h");
diff --git a/src/decal.zig b/src/decal.zig
deleted file mode 100644
index 7633880..0000000
--- a/src/decal.zig
+++ /dev/null
@@ -1,247 +0,0 @@
-// Decal construction and drawing
-// Copyright (C) 2002  David Rosen
-// Copyright (C) 2003  Steven Fuller
-// Copyright (C) 2023  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 <https://www.gnu.org/licenses/>.
-
-const Child = std.meta.Child;
-const Model = model.Model;
-const XYZ = geom.XYZ;
-const assert = std.debug.assert;
-const c = @import("cimport.zig");
-const geom = @import("geom.zig");
-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);
-const max_len = 120;
-
-const Kind = enum(c_int) { bullet_hole, crater, blood_pool };
-
-const Decals = extern struct {
-    // GLuint is always 32-bit.
-    hole_textures: [2]u32,
-    blood_textures: [11]u32,
-    len: u32,
-    kind: [max_len]Kind,
-    points: [max_len * 8]XYZ,
-    numpoints: [max_len]u32,
-    texcoordsx: [max_len * 8]f32,
-    texcoordsy: [max_len * 8]f32,
-    alive: [max_len]f32,
-};
-
-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));
-}
-
-fn pointTouchModel(p: @Vector(3, f32), n: @Vector(3, f32),
-                   m: *const Model, move: XYZ, rot: f32) bool {
-    var temp: XYZ = undefined;
-    return segCrossModelTrans(@bitCast(p + n), @bitCast(p - n),
-                              m, move, rot, &temp) > -1;
-}
-
-export fn addDecal(d: *Decals, kind: Kind, location: XYZ,
-                   size: f32, normal: XYZ, poly: c_int,
-                   m: *const Model, move: XYZ, rot: f32) void {
-    if (d.len >= max_len) return;
-    const n: @Vector(3, f32) = @bitCast(normal);
-    const abs_n = @fabs(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));
-
-    d.kind[d.len] = kind;
-    d.numpoints[d.len] = 0;
-    d.alive[d.len] = 0;
-    for ([_]f32{ -1, 1, 1, -1 }, [_]f32{ -1, -1, 1, 1 }) |x, y| {
-        var p = loc + right * splat(3, x) + up * splat(3, y);
-        var i = d.len * 8 + d.numpoints[d.len];
-        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)) {
-            d.texcoordsx[i] = x * 0.5 + 0.5;
-            d.texcoordsy[i] = y * 0.5 + 0.5;
-            d.points[i] = @bitCast(p + n_eps);
-            d.numpoints[d.len] += 1;
-            continue;
-        }
-
-        const count_inc = @max(0.01, @min(1.0 / size, 0.2));
-        var good: bool = false;
-        var count = 1.0 - count_inc;
-        while (!good and count > -1.0) : (count -= count_inc) {
-            d.texcoordsx[i] = x * 0.5 + 0.5;
-            d.texcoordsy[i] = y * count * 0.5 + 0.5;
-            p = loc + right * splat(3, x) + up * splat(3, y * count);
-            good = pointTouchModel(p, n_eps2, m, move, rot);
-        }
-        if (good) {
-            d.points[i] = @bitCast(p + n_eps);
-            d.numpoints[d.len] += 1;
-            i += 1;
-        }
-
-        good = false;
-        count = 1.0 - count_inc;
-        while (!good and count > -1.0) : (count -= count_inc) {
-            d.texcoordsx[i] = x * count * 0.5 + 0.5;
-            d.texcoordsy[i] = y * 0.5 + 0.5;
-            p = loc + right * splat(3, x * count) + up * splat(3, y);
-            good = pointTouchModel(p, n_eps2, m, move, rot);
-	}
-        if (good) {
-            d.points[i] = @bitCast(p + n_eps);
-            d.numpoints[d.len] += 1;
-            continue;
-        }
-
-        var count2 = 1.0 - count_inc;
-        while (!good and count2 > -1.0) : (count2 -= count_inc) {
-            count = 1.0 - count_inc;
-            while (!good and count > -1.0) : (count -= count_inc) {
-                d.texcoordsx[i] = x * count2 * 0.5 + 0.5;
-                d.texcoordsy[i] = y * count * 0.5 + 0.5;
-                p = loc + right * splat(3, x * count2)
-                        + up * splat(3, y * count);
-                good = pointTouchModel(p, n_eps2, m, move, rot);
-            }
-        }
-        if (good) {
-            d.points[i] = @bitCast(p + n_eps);
-            d.numpoints[d.len] += 1;
-        }
-    }
-    d.len += 1;
-}
-
-export fn drawDecals(d: *const Decals) void {
-    c.glAlphaFunc(c.GL_GREATER, 0.01);
-    c.glDepthFunc(c.GL_LEQUAL);
-    c.glEnable(c.GL_BLEND);
-    c.glEnable(c.GL_CULL_FACE);
-    c.glEnable(c.GL_TEXTURE_2D);
-    c.glEnable(c.GL_LIGHTING);
-    c.glDepthMask(0);
-    c.glAlphaFunc(c.GL_GREATER, 0.01);
-    c.glTexParameterf(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_S, c.GL_CLAMP);
-    c.glTexParameterf(c.GL_TEXTURE_2D, c.GL_TEXTURE_WRAP_T, c.GL_CLAMP);
-    c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA);
-    c.glEnable(c.GL_POLYGON_OFFSET_FILL);
-    for (0..d.len) |i| {
-        switch (d.kind[i]) {
-            .bullet_hole, .crater => |k| {
-                c.glColor4f(1.0, 1.0, 1.0, 1.0 - d.alive[i] / 10.0);
-                c.glBindTexture(c.GL_TEXTURE_2D, d.hole_textures[switch (k) {
-                    .bullet_hole => 0,
-                    .crater => 1,
-                    // https://github.com/ziglang/zig/issues/12863
-                    else => unreachable,  // TODO: remove
-                }]);
-            },
-            .blood_pool => {
-                const alpha = if (d.alive[i] < 2.0)
-                    @mod(d.alive[i], 0.2) + 0.8
-                else
-                    (20.0 - d.alive[i]) / 18.0;
-                c.glColor4f(1.0, 1.0, 1.0, alpha);
-                const j: usize = @intFromFloat(d.alive[i] * 5.0);
-                c.glBindTexture(c.GL_TEXTURE_2D, d.blood_textures[@min(j, 10)]);
-            },
-        }
-
-        c.glPushMatrix();
-        c.glBegin(c.GL_TRIANGLE_FAN);
-        for (0..d.numpoints[i]) |j| {
-            c.glTexCoord2f(d.texcoordsx[i * 8 + j], d.texcoordsy[i * 8 + j]);
-            c.glVertex3f(d.points[i * 8 + j].x,
-                d.points[i * 8 + j].y, d.points[i * 8 + j].z);
-        }
-        c.glEnd();
-        c.glPopMatrix();
-    }
-    c.glDepthMask(1);
-    c.glDisable(c.GL_TEXTURE_2D);
-    c.glEnable(c.GL_CULL_FACE);
-    c.glDisable(c.GL_POLYGON_OFFSET_FILL);
-    c.glDepthFunc(c.GL_LEQUAL);
-}
-
-export fn updateDecals(d: *Decals) void {
-    for (0..d.len) |i| {
-        d.alive[i] += c.multiplier;
-        if (d.alive[i] < @as(f32, switch (d.kind[i]) {
-            .bullet_hole, .crater => 10.0,
-            .blood_pool => 20.0,
-        })) continue;
-
-        d.len -= 1;
-        const last = d.len;
-        d.numpoints[i] = d.numpoints[last];
-        d.alive[i] = d.alive[last];
-        d.kind[i] = d.kind[last];
-        for (0..d.numpoints[i]) |j| {
-            d.points[i * 8 + j] = d.points[last * 8 + j];
-            d.texcoordsx[i * 8 + j] = d.texcoordsx[last * 8 + j];
-            d.texcoordsy[i * 8 + j] = d.texcoordsy[last * 8 + j];
-        }
-    }
-}
-
-export fn destroyDecals(d: *const Decals) void {
-    c.glDeleteTextures(2, &d.hole_textures);
-    c.glDeleteTextures(11, &d.blood_textures);
-}
diff --git a/src/main.zig b/src/main.zig
index 7a7d21e..9accfc8 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -26,7 +26,9 @@ const al = @import("zeal");
 const gf = @import("gfz");
 const gl = @import("zgl");
 
+const Decals = @import("Decals.zig");
 const Scores = misc.Scores;
+const Textures = @import("Textures.zig");
 const c = @import("cimport.zig");
 const configuration = @import("config.zig");
 const misc = @import("misc.zig");
@@ -37,7 +39,6 @@ var game: *c.Game = undefined;
 var prng: DefaultPrng = undefined;
 
 comptime {
-    _ = @import("decal.zig");
     _ = @import("geom.zig");
     _ = @import("model.zig");
 } // export functions in C ABI
@@ -95,7 +96,11 @@ pub fn main() !void {
     try window.makeCurrent();
 
     prng = DefaultPrng.init(@bitCast(try gf.getTime()));
-    game = c.makeGame(@bitCast(config),
+    const textures = try Textures.init();
+    defer textures.deinit();
+    var decals = Decals.init(allocator, &textures);
+    defer decals.deinit();
+    game = c.makeGame(&decals, @bitCast(config),
                       @bitCast(try loadScores(loca.user_data))).?;
     defer saveScores(loca.user_data, @bitCast(c.getScores(game)))
         catch unreachable;
@@ -122,6 +127,8 @@ pub fn main() !void {
     defer c.closeGame(game);
     while (!try window.shouldClose()) {
         c.eventLoop(game);
+        decals.update();
+        decals.draw();
         try window.swapBuffers();
         try gf.pollEvents();
     }
diff --git a/src/misc.zig b/src/misc.zig
index ab11ca3..46237cb 100644
--- a/src/misc.zig
+++ b/src/misc.zig
@@ -36,7 +36,6 @@ const tokenizeScalar = std.mem.tokenizeScalar;
 const al = @import("zeal");
 const gf = @import("gfz");
 const ini = @import("ini");
-const qoi = @import("qoi");
 
 const c = @import("cimport.zig");
 pub const data_dir = @import("build_options").data_dir ++ [_]u8{ sep };
@@ -221,42 +220,6 @@ export fn loadSound(filename: [*:0]const u8) u32 {
     return buffer.reference;
 }
 
-fn check(comptime errorString: fn (c_uint) callconv(.C) [*c]const u8,
-         status: anytype) void {
-    if (status != 0)
-        @panic(span(errorString(@intCast(status))));
-}
-
-/// Load PNG file into an OpenGL buffer and return it.
-export fn loadTexture(filename: [*:0]const u8) u32 {
-    const file = readFile(cwd(), data_dir ++ "textures{c}{s}", .{
-        sep, filename,
-    }) catch unreachable;
-    defer allocator.free(file);
-
-    var image = qoi.decodeBuffer(allocator, file) catch unreachable;
-    defer image.deinit(allocator);
-    const data: [*c]const u8 = @ptrCast(image.pixels.ptr);
-
-    var texture: u32 = undefined;
-    c.glGenTextures(1, &texture);
-    c.glBindTexture(c.GL_TEXTURE_2D, texture);
-    defer c.glBindTexture(c.GL_TEXTURE_2D, 0);
-    c.glTexEnvi(c.GL_TEXTURE_ENV, c.GL_TEXTURE_ENV_MODE, c.GL_MODULATE);
-    c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MAG_FILTER, c.GL_LINEAR);
-    c.glTexParameteri(c.GL_TEXTURE_2D, c.GL_TEXTURE_MIN_FILTER, c.GL_LINEAR);
-
-    const width: i32 = @intCast(image.width);
-    const height: i32 = @intCast(image.height);
-    c.glPixelStorei(c.GL_UNPACK_ALIGNMENT, 1);
-    c.glTexImage2D(c.GL_TEXTURE_2D, 0, 4, width, height,
-                   0, c.GL_RGBA, c.GL_UNSIGNED_BYTE, data);
-    check(c.gluErrorString,
-          c.gluBuild2DMipmaps(c.GL_TEXTURE_2D, 4, width, height,
-                              c.GL_RGBA, c.GL_UNSIGNED_BYTE, data));
-    return texture;
-}
-
 /// Move sound source to given position and play it.
 export fn playSound(source: u32, x: f32, y: f32, z: f32) void {
     const src = al.Source{ .reference = source };
@@ -321,7 +284,6 @@ export fn glPrint(text: *const Text, x: f64, y: f64, str: [*:0]const u8,
     c.glBindTexture(c.GL_TEXTURE_2D, text.texture);
     c.glDisable(c.GL_DEPTH_TEST);
     c.glDisable(c.GL_LIGHTING);
-    c.glEnable(c.GL_BLEND);
     c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA);
     c.glMatrixMode(c.GL_PROJECTION);
     c.glPushMatrix();