summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CHANGES11
-rw-r--r--build.zig1
-rw-r--r--src/Constants.h4
-rw-r--r--src/Decals.cpp469
-rw-r--r--src/Decals.h47
-rw-r--r--src/Game.h2
-rw-r--r--src/GameDraw.cpp5
-rw-r--r--src/GameInitDispose.cpp37
-rw-r--r--src/GameTick.cpp16
-rw-r--r--src/Models.cpp13
-rw-r--r--src/Models.h25
-rw-r--r--src/Person.cpp2
-rw-r--r--src/Quaternions.cpp74
-rw-r--r--src/Quaternions.h9
-rw-r--r--src/decal.zig118
-rw-r--r--src/geom.zig59
-rw-r--r--src/main.zig1
17 files changed, 381 insertions, 512 deletions
diff --git a/CHANGES b/CHANGES
index af8462f..d171b83 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,14 @@
+Tag:	2.5.1
+Date:	2023-08-24
+
+	Zig 0.11 compatibility
+
+	The codebase has been ported to Zig 0.11
+	and the following bugs have been fixed:
+
+	* Missing bullet/crater holes on certain walls (from 2.4.6)
+	* Disruptive disappearance of decals
+
 Tag:	2.5.0
 Date:	2023-02-15
 
diff --git a/build.zig b/build.zig
index 989d620..56a86c6 100644
--- a/build.zig
+++ b/build.zig
@@ -43,7 +43,6 @@ pub fn build(b: *Build) void {
         "src/Globals.cpp",
         "src/Models.cpp",
         "src/Person.cpp",
-        "src/Quaternions.cpp",
         "src/Skeleton.cpp",
         "src/Sprites.cpp",
     }, &.{ "--std=c++17", "-Wall", "-Werror", "-fno-sanitize=undefined" });
diff --git a/src/Constants.h b/src/Constants.h
index 654c904..ac3c1c8 100644
--- a/src/Constants.h
+++ b/src/Constants.h
@@ -6,10 +6,6 @@
 #define max_muscles 29
 #define gravity -25
 
-#define bullethole 0
-#define crater 1
-#define bloodpool 2
-
 #define idleanim 0
 #define joganim 1
 #define pistolaimanim 2
diff --git a/src/Decals.cpp b/src/Decals.cpp
index eb2a90b..510a557 100644
--- a/src/Decals.cpp
+++ b/src/Decals.cpp
@@ -1,363 +1,146 @@
+#include <algorithm>
 #include <cmath>
 
-#include "Camera.h"
-#include "Constants.h"
 #include "Decals.h"
-#include "misc.h"
 
-extern float multiplier;
-extern bool slomo;
-extern bool blood;
-extern float fogcolorr;
-extern float fogcolorg;
-extern float fogcolorb;
-//Functions
-extern float sinefluct;
-extern int environment;
-extern Model gunmodels[10];
-extern Camera camera;
-extern float precipitationhorz;
-extern float precipitationvert;
-extern float precipitationdensity;
-extern float snowdelay;
+#define NORMAL_OFFSET 0.02f
 
-int Decals::MakeDecal(int atype, XYZ location, float size, XYZ normal, int poly, Model *model, XYZ move, float rotation){
-	int major=0;
-	float normalv[3];
-	XYZ right;
-	XYZ up;
-	XYZ nothing;
-	XYZ axis[3];
-	XYZ temp;
-
-	nothing = {};
-
-	axis[0].x=1;
-	axis[1].y=1;
-	axis[2].z=1;
-
-	normalv[0]=abs(normal.x);
-	normalv[1]=abs(normal.y);
-	normalv[2]=abs(normal.z);
-
-	if(normalv[1]>normalv[major])major=1;
-	if(normalv[2]>normalv[major])major=2;
+enum corner { SW, SE, NE, NW };
 
-	if (normalv[0] == 1 || normalv[1] == 1 || normalv[2] == 1) {
-		if ((major == 0 && normal.x > 0) || major == 1)
-			right = {0.0f, 0.0f, -1.0f};
-		else if (major == 0)
-			right = {0.0f, 0.0f, 1.0f};
-		else if (major == 0)
-			right = {normal.z, 0.0f, 0.0f};
-	} else {
-		right = crossProduct(axis[major], normal);
+void bind(struct Decals *d, XYZ location, float size, XYZ normal,
+          int poly, Model *model, XYZ move, float rotation,
+          XYZ right, XYZ up, enum corner direction)
+{
+	float x, y;
+	switch (direction) {
+	case SW:
+		x = -1.0f;
+		y = -1.0f;
+		break;
+	case NW:
+		x = 1.0f;
+		y = -1.0f;
+		break;
+	case NE:
+		x = 1.0f;
+		y = 1.0f;
+		break;
+	case SE:
+		x = -1.0f;
+		y = 1.0f;
+		break;
 	}
 
-	up = normalize(crossProduct(normal, right));
-	right = normalize(right);
+	size_t i = d->len * 8 + d->numpoints[d->len];
+	d->points[i] = location + right * x + up * y;
+	d->texcoordsx[i] = x * 0.5f + 0.5f;
+	d->texcoordsy[i] = y * 0.5f + 0.5f;
 
-	float count;
-	float count2;
-	float countinc=1/size;
-	if(countinc<.01)countinc=.01;
-	if(countinc>.2)countinc=.2;
-	float normaloffset=.02;
-	int good;
-
-	numpoints[howmanydecals]=0;
-    points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing - right - up) * (size/3) /*+ normal/100*/;
-    texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = 0;
-    texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = 0;
-    if((move.x==0&&move.z==0&&rotation==0)||
-    LineFacetd(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25, model->vertex[model->Triangles[poly].vertex[0]], model->vertex[model->Triangles[poly].vertex[1]], model->vertex[model->Triangles[poly].vertex[2]],normal,&temp)
-    )numpoints[howmanydecals]++;
-    else {
-    	good=-1;
-    	count=1-countinc;
-    	while(good==-1&&count>-1){
-    		texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = 0;
-    		texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = .5-count/2;
-    		points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing - right - up*count) * (size/3);
-    		count-=countinc;
-    		good=model->LineCheck2(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25,&temp,move,rotation);
-    	}
-    	if(good!=-1)numpoints[howmanydecals]++;
-    	good=-1;
-    	count=1-countinc;
-    	while(good==-1&&count>-1){
-    		texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = .5-count/2;
-    		texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = 0;
-    		points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing - right*count - up) * (size/3);
-    		count-=countinc;
-    		good=model->LineCheck2(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25,&temp,move,rotation);
-    	}
-    	if(good!=-1)numpoints[howmanydecals]++;
-    	if(good==-1){
-    		good=-1;
-	    	count2=1-countinc;
-	    	while(good==-1&&count2>-1){
-	    		count=1-countinc;
-		    	while(good==-1&&count>-1){
-		    		texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = .5-count2/2;
-		    		texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = .5-count/2;
-		    		points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing - right*count2 - up*count) * (size/3);
-		    		count-=countinc;
-		    		good=model->LineCheck2(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25,&temp,move,rotation);
-		    	}
-		    	count2-=countinc;
-		    }
-	    	if(good!=-1)numpoints[howmanydecals]++;
-    	}
-    }
-
-    points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing + right - up) * (size/3) /*+ normal/100*/;
-    texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = 1;
-    texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = 0;
-    if((move.x==0&&move.y==0&&move.z==0&&rotation==0)||
-    LineFacetd(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25, model->vertex[model->Triangles[poly].vertex[0]], model->vertex[model->Triangles[poly].vertex[1]], model->vertex[model->Triangles[poly].vertex[2]],normal,&temp)
-    )numpoints[howmanydecals]++;
-    else {
-    	good=-1;
-    	count=1-countinc;
-    	while(good==-1&&count>-1){
-    		texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = .5+count/2;
-    		texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = 0;
-    		points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing + right*count - up) * (size/3);
-    		count-=countinc;
-    		good=model->LineCheck2(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25,&temp,move,rotation);
-    	}
-    	if(good!=-1)numpoints[howmanydecals]++;
-    	good=-1;
-    	count=1-countinc;
-    	while(good==-1&&count>-1){
-    		texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = 1;
-    		texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = .5-count/2;
-    		points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing + right - up*count) * (size/3);
-    		count-=countinc;
-    		good=model->LineCheck2(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25,&temp,move,rotation);
-    	}
-    	if(good!=-1)numpoints[howmanydecals]++;
-    	if(good==-1){
-    		good=-1;
-	    	count2=1-countinc;
-	    	while(good==-1&&count2>-1){
-	    		count=1-countinc;
-		    	while(good==-1&&count>-1){
-		    		texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = .5+count2/2;
-		    		texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = .5-count/2;
-		    		points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing + right*count2 - up*count) * (size/3);
-		    		count-=countinc;
-		    		good=model->LineCheck2(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25,&temp,move,rotation);
-		    	}
-		    	count2-=countinc;
-		    }
-	    	if(good!=-1)numpoints[howmanydecals]++;
-    	}
-    }
-
-    points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing + right + up) * (size/3) /*+ normal/100*/;
-    texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = 1;
-    texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = 1;
-    if((move.x==0&&move.y==0&&move.z==0&&rotation==0)||
-    LineFacetd(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25, model->vertex[model->Triangles[poly].vertex[0]], model->vertex[model->Triangles[poly].vertex[1]], model->vertex[model->Triangles[poly].vertex[2]],normal,&temp)
-    )numpoints[howmanydecals]++;
-    else {
-    	good=-1;
-    	count=1-countinc;
-    	while(good==-1&&count>-1){
-    		texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = 1;
-    		texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = .5+count/2;
-    		points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing + right + up*count) * (size/3);
-    		count-=countinc;
-    		good=model->LineCheck2(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25,&temp,move,rotation);
-    	}
-    	if(good!=-1)numpoints[howmanydecals]++;
-    	good=-1;
-    	count=1-countinc;
-    	while(good==-1&&count>-1){
-    		texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = .5+count/2;
-    		texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = 1;
-    		points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing + right*count + up) * (size/3);
-    		count-=countinc;
-    		good=model->LineCheck2(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25,&temp,move,rotation);
-    	}
-    	if(good!=-1)numpoints[howmanydecals]++;
-    	if(good==-1){
-    		good=-1;
-	    	count2=1-countinc;
-	    	while(good==-1&&count2>-1){
-	    		count=1-countinc;
-		    	while(good==-1&&count>-1){
-		    		texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = .5+count2/2;
-		    		texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = .5+count/2;
-		    		points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing + right*count2 + up*count) * (size/3);
-		    		count-=countinc;
-		    		good=model->LineCheck2(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25,&temp,move,rotation);
-		    	}
-		    	count2-=countinc;
-		    }
-	    	if(good!=-1)numpoints[howmanydecals]++;
-    	}
-    }
-
-    points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing - right + up) * (size/3) /*+ normal/100*/;
-	texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = 0;
-    texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = 1;
-    if((move.x==0&&move.y==0&&move.z==0&&rotation==0)||
-    LineFacetd(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25, model->vertex[model->Triangles[poly].vertex[0]], model->vertex[model->Triangles[poly].vertex[1]], model->vertex[model->Triangles[poly].vertex[2]],normal,&temp)
-    )numpoints[howmanydecals]++;
-    else {
-    	good=-1;
-    	count=1-countinc;
-    	while(good==-1&&count>-1){
-    		texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = .5-count/2;
-    		texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = 1;
-    		points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing - right*count + up) * (size/3);
-    		count-=countinc;
-    		good=model->LineCheck2(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25,&temp,move,rotation);
-    	}
-    	if(good!=-1)numpoints[howmanydecals]++;
-    	good=-1;
-    	count=1-countinc;
-    	while(good==-1&&count>-1){
-    		texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = 0;
-    		texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = .5+count/2;
-    		points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing - right + up*count) * (size/3);
-    		count-=countinc;
-    		good=model->LineCheck2(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25,&temp,move,rotation);
-    	}
-    	if(good!=-1)numpoints[howmanydecals]++;
-    	if(good==-1){
-    		good=-1;
-	    	count2=1-countinc;
-	    	while(good==-1&&count2>-1){
-	    		count=1-countinc;
-		    	while(good==-1&&count>-1){
-		    		texcoordsx[howmanydecals*8+numpoints[howmanydecals]] = .5-count2/2;
-		    		texcoordsy[howmanydecals*8+numpoints[howmanydecals]] = .5+count/2;
-		    		points[howmanydecals*8+numpoints[howmanydecals]] = location + (nothing - right*count2 + up*count) * (size/3);
-		    		count-=countinc;
-		    		good=model->LineCheck2(points[howmanydecals*8+numpoints[howmanydecals]]+normal/25,points[howmanydecals*8+numpoints[howmanydecals]]-normal/25,&temp,move,rotation);
-		    	}
-		    	count2-=countinc;
-		    }
-	    	if(good!=-1)numpoints[howmanydecals]++;
-    	}
-    }
-    for(int i=0;i<numpoints[howmanydecals];i++){
-    	 points[howmanydecals*8+i] += normal*normaloffset;
-    }
-
-    type[howmanydecals]=atype;
-	alivetime[howmanydecals]=0;
-	if(howmanydecals<maxdecals){howmanydecals++;}
-
-	return 0;
-}
-
-int Decals::DeleteDecal(int which){
-	if(which>=0){
-		numpoints[which]=numpoints[howmanydecals-1];
-		alivetime[which]=alivetime[howmanydecals-1];
-		type[which]=type[howmanydecals-1];
-		for(int i=0;i<numpoints[which];i++){
-			points[which*8+i] = points[howmanydecals*8-8+i];
-		    texcoordsx[which*8+i] = texcoordsx[howmanydecals*8-8+i];
-		    texcoordsy[which*8+i] = texcoordsy[howmanydecals*8-8+i];
-	    }
-		if(howmanydecals>0){howmanydecals--;}
+	XYZ temp;
+	if ((move.x == 0 && move.y == 0 && move.z == 0 && rotation == 0)
+	    || segCrossTrigon(d->points[i] + normal / 25,
+	                      d->points[i] - normal / 25,
+	                      model->vertex + model->Triangles[poly].vertex[0],
+	                      model->vertex + model->Triangles[poly].vertex[1],
+	                      model->vertex + model->Triangles[poly].vertex[2],
+	                      &normal, &temp)) {
+		d->numpoints[d->len]++;
+		return;
 	}
 
-	return 0;
-}
-
-void Decals::DoStuff()
-{
-	for(int i=0;i<howmanydecals;i++){
-		alivetime[i]+=multiplier;
-		if(alivetime[i]>10&&(type[i]==bullethole||type[i]==crater))DeleteDecal(i);
-		if(alivetime[i]>20&&(type[i]==bloodpool))DeleteDecal(i);
+	const auto n = normal / 25.0f;
+	const float countinc = std::max(0.01f, std::min(1.0f / size, 0.2f));
+	int good = -1;
+	float count = 1.0f - countinc;
+	while (good == -1 && count > -1.0f) {
+		d->texcoordsx[i] = x * 0.5f + 0.5f;
+		d->texcoordsy[i] = y * count * 0.5f + 0.5f;
+		d->points[i] = location + right * x + up * (y * count);
+		count -= countinc;
+		good = model->LineCheck2(d->points[i] + n,
+		                         d->points[i] - n,
+		                         &temp, move, rotation);
+	}
+	if (good > -1) {
+		d->numpoints[d->len]++;
+		i++;
 	}
-}
-
-void Decals::draw()
-{
-	glAlphaFunc(GL_GREATER, 0.01);
-
-	float bloodpoolspeed=1;
-
-	glDepthFunc(GL_LEQUAL);
-	glEnable(GL_BLEND);
-	glEnable(GL_CULL_FACE);
-	glEnable(GL_TEXTURE_2D);
-	glEnable(GL_LIGHTING);
-	glDepthMask(0);
-	glAlphaFunc(GL_GREATER, 0.01);
-	glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP );
-	glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP );
-	glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
-	glEnable(GL_POLYGON_OFFSET_FILL);
-	for(int i=0;i<howmanydecals;i++){
-		if(type[i]==bullethole)glBindTexture(GL_TEXTURE_2D, bulletholetextureptr);
-		if(type[i]==crater)glBindTexture(GL_TEXTURE_2D, cratertextureptr);
-		if(type[i]!=bloodpool)glColor4f(1,1,1,10-alivetime[i]);
-
-		if(type[i]==bloodpool&&alivetime[i]<bloodpoolspeed*.2)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[0]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*.2&&alivetime[i]<bloodpoolspeed*.4)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[1]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*.4&&alivetime[i]<bloodpoolspeed*.6)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[2]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*.6&&alivetime[i]<bloodpoolspeed*.8)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[3]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*.8&&alivetime[i]<bloodpoolspeed*1)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[4]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*1&&alivetime[i]<bloodpoolspeed*1.2)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[5]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*1.2&&alivetime[i]<bloodpoolspeed*1.4)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[6]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*1.4&&alivetime[i]<bloodpoolspeed*1.6)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[7]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*1.6&&alivetime[i]<bloodpoolspeed*1.8)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[8]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*1.8&&alivetime[i]<bloodpoolspeed*2.0)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[9]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*2.0)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[10]);
-		if(type[i]==bloodpool&&alivetime[i]<bloodpoolspeed*2.0)glColor4f(1,1,1,1.5-(alivetime[i]*5/bloodpoolspeed-(int)(alivetime[i]*5/bloodpoolspeed)));
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*2.0)glColor4f(1,1,1,20-alivetime[i]);
-
-		glPushMatrix();
-		glBegin(GL_TRIANGLE_FAN);
-			for(int j=0;j<numpoints[i];j++){
-			 glTexCoord2f(texcoordsx[i*8+j], texcoordsy[i*8+j]); glVertex3f(points[i*8+j].x,points[i*8+j].y,points[i*8+j].z);
-			}
-		glEnd();
-		glPopMatrix();
 
-		if(type[i]==bloodpool&&alivetime[i]<bloodpoolspeed*2.0){
-			if(type[i]==bloodpool&&alivetime[i]<bloodpoolspeed*.2)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[1]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*.2&&alivetime[i]<bloodpoolspeed*.4)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[2]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*.4&&alivetime[i]<bloodpoolspeed*.6)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[3]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*.6&&alivetime[i]<bloodpoolspeed*.8)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[4]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*.8&&alivetime[i]<bloodpoolspeed*1)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[5]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*1&&alivetime[i]<bloodpoolspeed*1.2)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[6]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*1.2&&alivetime[i]<bloodpoolspeed*1.4)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[7]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*1.4&&alivetime[i]<bloodpoolspeed*1.6)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[8]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*1.6&&alivetime[i]<bloodpoolspeed*1.8)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[9]);
-		if(type[i]==bloodpool&&alivetime[i]>=bloodpoolspeed*1.8&&alivetime[i]<bloodpoolspeed*2.0)glBindTexture(GL_TEXTURE_2D, bloodtextureptr[10]);
-		if(type[i]==bloodpool)glColor4f(1,1,1,alivetime[i]*5/bloodpoolspeed-(int)(alivetime[i]*5/bloodpoolspeed));
+	good = -1;
+	count = 1.0f - countinc;
+	while (good == -1 && count > -1.0f) {
+		d->texcoordsx[i] = x * count * 0.5f + 0.5f;
+		d->texcoordsy[i] = y * 0.5f + 0.5f;
+		d->points[i] = location + right * (x * count) + up * y;
+		count -= countinc;
+		good = model->LineCheck2(d->points[i] + n,
+		                         d->points[i] - n,
+		                         &temp, move, rotation);
+	}
+	if (good > -1) {
+		d->numpoints[d->len]++;
+		return;
+	}
 
-			glPushMatrix();
-			glBegin(GL_TRIANGLE_FAN);
-				for(int j=0;j<numpoints[i];j++){
-				 glTexCoord2f(texcoordsx[i*8+j], texcoordsy[i*8+j]); glVertex3f(points[i*8+j].x,points[i*8+j].y,points[i*8+j].z);
-				}
-			glEnd();
-			glPopMatrix();
+	float count2 = 1.0f - countinc;
+	while (good == -1 && count2 > -1.0f){
+		count = 1.0f - countinc;
+		while (good == -1 && count > -1.0f) {
+			d->texcoordsx[i] = x * count2 * 0.5f + 0.5f;
+			d->texcoordsy[i] = y * count * 0.5f + 0.5f;
+			d->points[i] = location + right * (x * count2) + up * (y * count);
+			count -= countinc;
+			good = model->LineCheck2(d->points[i] + n,
+			                         d->points[i] - n,
+			                         &temp, move, rotation);
 		}
+		count2 -= countinc;
 	}
-	glDepthMask(1);
-	glDisable(GL_TEXTURE_2D);
-	glColor4f(1,1,1,1);
-	glEnable(GL_CULL_FACE);
-	glDisable(GL_POLYGON_OFFSET_FILL);
-	glDepthFunc(GL_LEQUAL);
+	if (good > -1)
+		d->numpoints[d->len]++;
 }
 
-Decals::~Decals()
+void addDecal(struct Decals *d, enum decal kind, XYZ location, float size,
+              XYZ normal, int poly, Model *model, XYZ move, float rotation)
 {
-	const GLuint holes[] {bulletholetextureptr, cratertextureptr};
-	glDeleteTextures(2, holes);
-	glDeleteTextures(11, bloodtextureptr);
+	if (d->len >= MAX_DECALS)
+		return;
+	d->kind[d->len] = kind;
+	d->alive[d->len] = 0;
+
+	float normalv[] = {abs(normal.x), abs(normal.y), abs(normal.z)};
+	unsigned char major = 0;
+	if (normalv[1] > normalv[major])
+		major = 1;
+	if (normalv[2] > normalv[major])
+		major = 2;
+
+	XYZ right = {0.0f};
+	if (normalv[0] == 1.0f || normalv[1] == 1.0f || normalv[2] == 1.0f) {
+		if ((major == 0 && normal.x > 0) || major == 1)
+			right.z = -1.0f;
+		else if (major == 0)
+			right.z = 1.0f;
+		else
+			right.x = normal.z;
+	} else {
+		XYZ axis = {0.0f};
+		((float *) &axis)[major] = 1.0f;
+		right = crossProduct(axis, normal);
+	}
+
+	d->numpoints[d->len] = 0;
+	XYZ up = normalize(crossProduct(normal, right)) * (size / 3);
+	right = normalize(right) * (size / 3);
+	bind(d, location, size, normal, poly, model,
+	     move, rotation, right, up, SW);
+	bind(d, location, size, normal, poly, model,
+	     move, rotation, right, up, NW);
+	bind(d, location, size, normal, poly, model,
+	     move, rotation, right, up, NE);
+	bind(d, location, size, normal, poly, model,
+	     move, rotation, right, up, SE);
+	for (int i = 0; i < d->numpoints[d->len]; ++i)
+		d->points[d->len * 8 + i] += normal * NORMAL_OFFSET;
+	d->len++;
 }
diff --git a/src/Decals.h b/src/Decals.h
index f37f523..cfe7a0e 100644
--- a/src/Decals.h
+++ b/src/Decals.h
@@ -2,7 +2,7 @@
 // Copyright (C) 2002  David Rosen
 // Copyright (C) 2003  Steven Fuller
 // Copyright (C) 2003  Zachary Jack Slater
-// Copyright (C) 2021  Nguyễn Gia Phong
+// Copyright (C) 2021, 2023  Nguyễn Gia Phong
 //
 // This file is part of Black Shades.
 //
@@ -25,30 +25,33 @@
 #include "Models.h"
 #include "Quaternions.h"
 
-#define maxdecals 120
+#define MAX_DECALS 120
 
-class Decals{
-public:
-	GLuint bulletholetextureptr;
-	GLuint cratertextureptr;
-	GLuint bloodtextureptr[11];
+enum decal { BULLET_HOLE, CRATER, BLOOD_POOL };
 
-	int howmanydecals;
+struct Decals {
+	GLuint hole_textures[2];
+	GLuint blood_textures[11];
 
-	int type[maxdecals];
-
-	XYZ points[8*maxdecals];
-	int numpoints[maxdecals];
-	float texcoordsx[8*maxdecals];
-	float texcoordsy[8*maxdecals];
-	float alivetime[maxdecals];
-
-	void draw();
+	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];
+};
 
-	int DeleteDecal(int which);
-	int MakeDecal(int atype, XYZ location, float size, XYZ normal, int poly, Model *model, XYZ move, float rotation);
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+void addDecal(struct Decals *d, enum decal kind, XYZ location, float size,
+              XYZ normal, int poly, 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
 
-	void DoStuff();
-	~Decals();
-};
 #endif // BLACKSHADES_DECALS_H
diff --git a/src/Game.h b/src/Game.h
index 24c6792..0639532 100644
--- a/src/Game.h
+++ b/src/Game.h
@@ -27,6 +27,8 @@
 #define max_people 90
 #define max_people_block 20
 
+extern float multiplier;
+
 #include <stdbool.h>
 
 #include <AL/al.h>
diff --git a/src/GameDraw.cpp b/src/GameDraw.cpp
index 911e9ea..b97f814 100644
--- a/src/GameDraw.cpp
+++ b/src/GameDraw.cpp
@@ -2,7 +2,7 @@
 // Copyright (C) 2002  David Rosen
 // Copyright (C) 2003  Ryan C. Gordon
 // Copyright (C) 2003  Steven Fuller
-// Copyright (C) 2021  Nguyễn Gia Phong
+// Copyright (C) 2021, 2023  Nguyễn Gia Phong
 //
 // This file is part of Black Shades.
 //
@@ -519,8 +519,7 @@ void Game::DrawGLScene(void)
 			}
 		}
 
-		// Decals
-		decals.draw();
+		drawDecals(&decals);
 
 		// Occluding blocks
 		beginx=(camera.position.x+block_spacing/2)/block_spacing-2;
diff --git a/src/GameInitDispose.cpp b/src/GameInitDispose.cpp
index 0a21275..86bb2bb 100644
--- a/src/GameInitDispose.cpp
+++ b/src/GameInitDispose.cpp
@@ -998,15 +998,8 @@ void initGame(Game* game)
 	glClearColor(fogcolorr,fogcolorg,fogcolorb,1);
 
 	game->initialized = true;
-	/*
-	for(int i=0;i<sprites.howmanysprites;i++){
-		sprites.DeleteSprite(0);
-	}
-	for(int i=0;i<decals.howmanydecals;i++){
-		decals.DeleteDecal(0);
-	}*/
 
-	decals.howmanydecals=0;
+	decals.len = 0;
 	sprites.howmanysprites=0;
 	game->losedelay = 1;
 }
@@ -1032,19 +1025,20 @@ void initGl(Game* game)
 	sprites.raintextureptr = loadTexture("sprites/white.qoi");
 	sprites.snowtextureptr = loadTexture("sprites/white.qoi");
 
-	decals.bulletholetextureptr = loadTexture("black.qoi");
-	decals.cratertextureptr = loadTexture("black.qoi");
-	decals.bloodtextureptr[0u] = loadTexture("blood/00.qoi");
-	decals.bloodtextureptr[1u] = loadTexture("blood/01.qoi");
-	decals.bloodtextureptr[2u] = loadTexture("blood/02.qoi");
-	decals.bloodtextureptr[3u] = loadTexture("blood/03.qoi");
-	decals.bloodtextureptr[4u] = loadTexture("blood/04.qoi");
-	decals.bloodtextureptr[5u] = loadTexture("blood/05.qoi");
-	decals.bloodtextureptr[6u] = loadTexture("blood/06.qoi");
-	decals.bloodtextureptr[7u] = loadTexture("blood/07.qoi");
-	decals.bloodtextureptr[8u] = loadTexture("blood/08.qoi");
-	decals.bloodtextureptr[9u] = loadTexture("blood/09.qoi");
-	decals.bloodtextureptr[10] = loadTexture("blood/10.qoi");
+	// TODO: use more detailed textuures for hole decals
+	decals.hole_textures[0] = loadTexture("black.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)
@@ -1067,4 +1061,5 @@ void closeGame(Game* game)
 	glDeleteTextures(5, textures);
 	alDeleteSources(33 + game->musictoggle * 4, gSourceID);
 	alDeleteBuffers(33 + game->musictoggle * 4, gSampleSet);
+	destroyDecals(&decals);
 }
diff --git a/src/GameTick.cpp b/src/GameTick.cpp
index 634db76..7fa90fa 100644
--- a/src/GameTick.cpp
+++ b/src/GameTick.cpp
@@ -758,12 +758,12 @@ void bleed(Game* game, size_t i)
 
 		XYZ normish {0.0f, 1.0f, 0.0f};
 		if (whichtri >= 0) {
-			decals.MakeDecal(bloodpool, temp, 12, normish,
+			addDecal(&decals, BLOOD_POOL, temp, 12, normish,
 				whichtri, &game->sidewalkcollide, move, rot);
 		} else {
 			temp = person.skeleton.joints[abdomen].position;
 			temp.y = -0.5f;
-			decals.MakeDecal(bloodpool, temp, 12, normish,
+			addDecal(&decals, BLOOD_POOL, temp, 12, normish,
 				0, &game->sidewalkcollide, {}, 0);
 		}
 		person.firstlongdead = true;
@@ -1004,7 +1004,7 @@ void Game::Tick()
 
 	spawnNpc(this);
 	sprites.DoStuff();
-	decals.DoStuff();
+	updateDecals(&decals);
 
 	// Facing
 	XYZ facing {0, 0, -1};
@@ -2089,7 +2089,7 @@ void Game::Tick()
 				// with wall
 				if(oldend==finalwallhit){
 
-					decals.MakeDecal(bullethole, finalwallhit,.7,hitnorm, hitpoly, model, hitmove, hitrotation);
+					addDecal(&decals, BULLET_HOLE, finalwallhit,.7,hitnorm, hitpoly, model, hitmove, hitrotation);
 
 					XYZ velocity;
 
@@ -2234,7 +2234,7 @@ void Game::Tick()
 				impact = true;
 				auto normalrotated = rotate(blocks[citytype[wherex][wherey]].normals[whichtri], 0, cityrotation[wherex][wherey] * 90, 0);
 				if (sprites.size[i] > 1)
-					decals.MakeDecal(crater, wallhit, 9, normalrotated, whichtri, &blocks[citytype[wherex][wherey]], move, cityrotation[wherex][wherey] * 90);
+					addDecal(&decals, CRATER, wallhit, 9, normalrotated, whichtri, &blocks[citytype[wherex][wherey]], move, cityrotation[wherex][wherey] * 90);
 				sprites.location[i] = wallhit + normalrotated * 0.02f;
 				reflect(&sprites.velocity[i], normalrotated);
 				sprites.velocity[i] *= 0.3f;
@@ -2267,7 +2267,7 @@ void Game::Tick()
 						move = {};
 						sprites.location[i].y=-.5;
 						XYZ normish = {0.0f, 1.0f, 0.0f};
-						decals.MakeDecal(crater, sprites.location[i],9,normish, 0, &blocks[citytype[wherex][wherey]], move, 0);
+						addDecal(&decals, CRATER, sprites.location[i],9,normish, 0, &blocks[citytype[wherex][wherey]], move, 0);
 					}
 
 					auto soundpos = sprites.location[i] - camera.position;
@@ -2390,14 +2390,14 @@ void Game::Tick()
 
 			XYZ normish = {0.0f, 1.0f, 0.0f};
 			if(whichtri>=0){
-				decals.MakeDecal(crater, sprites.location[i],9,normish, 0, &sidewalkcollide, move, cityrotation[wherex][wherey]*90);
+				addDecal(&decals, CRATER, sprites.location[i],9,normish, 0, &sidewalkcollide, move, cityrotation[wherex][wherey]*90);
 			}
 
 			if(whichtri==-1){
 				temp=sprites.location[i];
 				temp.y=-.5;
 				move = {};
-				decals.MakeDecal(crater, sprites.location[i],9,normish, 0, &sidewalkcollide, move, 0);
+				addDecal(&decals, CRATER, sprites.location[i],9,normish, 0, &sidewalkcollide, move, 0);
 			}
 
 			for(int k=0;k<numpeople;k++){
diff --git a/src/Models.cpp b/src/Models.cpp
index acbbaa8..0e558e2 100644
--- a/src/Models.cpp
+++ b/src/Models.cpp
@@ -120,16 +120,15 @@ void Model::draw(float r, float g, float b)
 int Model::LineCheck(XYZ p1, XYZ p2, XYZ *p)
 {
 	int result = -1;
-	if (segmentIntersectsSphere(p1, p2, boundingspherecenter,
-	                            boundingsphereradius)) {
+	if (segCrossSphere(p1, p2, boundingspherecenter, boundingsphereradius)) {
 		float olddistance = 9999999.0;
 		for (int j = 0; j < TriangleNum; ++j) {
 			XYZ point;
-			if (!LineFacetd(p1, p2,
-			                vertex[Triangles[j].vertex[0]],
-			                vertex[Triangles[j].vertex[1]],
-			                vertex[Triangles[j].vertex[2]],
-			                normals[j], &point))
+			if (!segCrossTrigon(p1, p2,
+			                    vertex + Triangles[j].vertex[0],
+			                    vertex + Triangles[j].vertex[1],
+			                    vertex + Triangles[j].vertex[2],
+			                    normals + j, &point))
 				continue;
 			float distance = sqrlen(point - p1);
 			if (distance < olddistance || result == -1) {
diff --git a/src/Models.h b/src/Models.h
index 5c27d6f..7929887 100644
--- a/src/Models.h
+++ b/src/Models.h
@@ -1,36 +1,25 @@
 #ifndef _MODELS_H_
 #define _MODELS_H_
 
-/**> Model Loading <**/
-//
-// Model Maximums
-//
 #include <GL/gl.h>
 
 #include "Quaternions.h"
 #include "Constants.h"
 
-#define max_textured_triangle		400		// maximum number of texture-filled triangles in a model
-#define max_model_vertex			max_textured_triangle*3		// maximum number of vertexs
+#define MAX_TEXTURED_TRIANGLES 400
 
-//
-// Model Structures
-//
-
-class TexturedTriangle{
-public:
+struct TexturedTriangle {
 	short vertex[3];
 	float r,g,b;
 };
 
-class Model{
-public:
+struct Model {
 	short vertexNum, TriangleNum;
 
-	XYZ vertex[max_model_vertex];
-	XYZ normals[max_textured_triangle];
-	TexturedTriangle Triangles[max_textured_triangle];
-	GLfloat vArray[max_textured_triangle*27];
+	XYZ vertex[MAX_TEXTURED_TRIANGLES * 3];
+	XYZ normals[MAX_TEXTURED_TRIANGLES];
+	TexturedTriangle Triangles[MAX_TEXTURED_TRIANGLES];
+	GLfloat vArray[MAX_TEXTURED_TRIANGLES * 27];
 
 	XYZ boundingspherecenter;
 	float boundingsphereradius;
diff --git a/src/Person.cpp b/src/Person.cpp
index d4a298b..0115a89 100644
--- a/src/Person.cpp
+++ b/src/Person.cpp
@@ -72,7 +72,7 @@ HitStruct 	Person::BulletCollideWithPlayer(int who, XYZ start, XYZ end){
 	}
 	tempbulletloc[0]=start;
 	tempbulletloc[1]=end;
-	if (segmentIntersectsSphere(start, end, average, distancemax)) {
+	if (segCrossSphere(start, end, average, distancemax)) {
 	for (auto& joint : skeleton.joints) {
 		if (joint.hasparent && joint.visible) {
 			tempbulletloc[0] = start;
diff --git a/src/Quaternions.cpp b/src/Quaternions.cpp
deleted file mode 100644
index 9b32dc0..0000000
--- a/src/Quaternions.cpp
+++ /dev/null
@@ -1,74 +0,0 @@
-#include <cmath>
-
-#include "Quaternions.h"
-
-bool PointInTriangle(XYZ *p, XYZ normal, XYZ *p1, XYZ *p2, XYZ *p3)
-{
-	bool bInter = false;
-	float pointv[3] { p->x, p->y, p->z };
-	float p1v[3] { p1->x, p1->y, p1->z };
-	float p2v[3] { p2->x, p2->y, p2->z };
-	float p3v[3] { p3->x, p3->y, p3->z };
-	float normalv[3] { normal.x, normal.y, normal.z };
-
-	int i = 0, j = 0;
-#define ABS(X) (((X)<0.f)?-(X):(X) )
-#define MAX(A, B) (((A)<(B))?(B):(A))
-	float max = MAX(MAX(ABS(normalv[0]), ABS(normalv[1])), ABS(normalv[2]));
-#undef MAX
-	if (max == ABS(normalv[0])) {i = 1; j = 2;} // y, z
-	if (max == ABS(normalv[1])) {i = 0; j = 2;} // x, z
-	if (max == ABS(normalv[2])) {i = 0; j = 1;} // x, y
-#undef ABS
-
-	float u0 = pointv[i] - p1v[i];
-	float v0 = pointv[j] - p1v[j];
-	float u1 = p2v[i] - p1v[i];
-	float v1 = p2v[j] - p1v[j];
-	float u2 = p3v[i] - p1v[i];
-	float v2 = p3v[j] - p1v[j];
-
-	if (u1 > -1.0e-05f && u1 < 1.0e-05f)// == 0.0f)
-	{
-		float b = u0 / u2;
-		if (0.0f <= b && b <= 1.0f)
-		{
-			float a = (v0 - b * v2) / v1;
-			if ((a >= 0.0f) && (( a + b ) <= 1.0f))
-				bInter = 1;
-		}
-	}
-	else
-	{
-		float b = (v0 * u1 - u0 * v1) / (v2 * u1 - u2 * v1);
-		if (0.0f <= b && b <= 1.0f)
-		{
-			float a = (u0 - b * u2) / u1;
-			if ((a >= 0.0f) && (( a + b ) <= 1.0f ))
-				bInter = 1;
-		}
-	}
-
-	return bInter;
-}
-
-float LineFacetd(XYZ p1,XYZ p2,XYZ pa,XYZ pb,XYZ pc, XYZ n, XYZ *p)
-{
-
-   //Calculate the parameters for the plane
-   float d = - n.x * pa.x - n.y * pa.y - n.z * pa.z;
-
-   //Calculate the position on the line that intersects the plane
-   float denom = n.x * (p2.x - p1.x) + n.y * (p2.y - p1.y) + n.z * (p2.z - p1.z);
-   if (abs(denom) < 0.0000001)        // Line and plane don't intersect
-      return 0;
-   float mu = - (d + n.x * p1.x + n.y * p1.y + n.z * p1.z) / denom;
-   p->x = p1.x + mu * (p2.x - p1.x);
-   p->y = p1.y + mu * (p2.y - p1.y);
-   p->z = p1.z + mu * (p2.z - p1.z);
-   if (mu < 0 || mu > 1)   // Intersection not along line segment
-      return 0;
-
-   if(!PointInTriangle( p, n, &pa, &pb, &pc)){return 0;}
-   return 1;
-}
diff --git a/src/Quaternions.h b/src/Quaternions.h
index 6ed778a..4c10cd6 100644
--- a/src/Quaternions.h
+++ b/src/Quaternions.h
@@ -46,13 +46,12 @@ extern "C" {
 	struct XYZ crossProduct(struct XYZ, struct XYZ);
 	struct XYZ normalize(struct XYZ);
 	void reflect(struct XYZ*, struct XYZ);
-	bool segmentIntersectsSphere(struct XYZ, struct XYZ, struct XYZ, float);
+	bool segCrossSphere(struct XYZ, struct XYZ, struct XYZ, float);
+	bool segCrossTrigon(struct XYZ p1, struct XYZ p2,
+		struct XYZ *pa, struct XYZ *pb, struct XYZ *pc,
+		struct XYZ *n, struct XYZ *p);
 	struct XYZ rotate(struct XYZ, float, float, float);
 
-	float LineFacetd(struct XYZ p1, struct XYZ p2,
-		struct XYZ pa, struct XYZ pb, struct XYZ pc,
-		struct XYZ n, struct XYZ *p);
-
 	void setFrustum(float (*)[4], float*, float*);
 	int cubeInFrustum(float (*)[4], float, float, float, float);
 	int sphereInFrustum(float (*)[4], float, float, float, float);
diff --git a/src/decal.zig b/src/decal.zig
new file mode 100644
index 0000000..de4d9e8
--- /dev/null
+++ b/src/decal.zig
@@ -0,0 +1,118 @@
+// 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 f32Eps = std.math.floatEps(f32);
+const XYZ = @import("geom.zig").XYZ;
+const c = @import("cimport.zig");
+
+const size = 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: [size]Kind,
+    points: [size * 8]XYZ,
+    numpoints: [size]u32,
+    texcoordsx: [size * 8]f32,
+    texcoordsy: [size * 8]f32,
+    alive: [size]f32,
+};
+
+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/geom.zig b/src/geom.zig
index 80cf1db..d6dc036 100644
--- a/src/geom.zig
+++ b/src/geom.zig
@@ -19,6 +19,7 @@
 
 const Child = std.meta.Child;
 const degreesToRadians = std.math.degreesToRadians;
+const f32Eps = std.math.floatEps(f32);
 const std = @import("std");
 
 fn sqr(x: anytype) @TypeOf(x) {
@@ -29,6 +30,8 @@ fn dot(u: anytype, v: @TypeOf(u)) Child(@TypeOf(u)) {
     return @reduce(.Add, u * v);
 }
 
+pub const XYZ = extern struct { x: f32, y: f32, z: f32 };
+
 export fn sqrlen(v: XYZ) f32 {
     const u: @Vector(3, f32) = @bitCast(v);
     return dot(u, u);
@@ -43,8 +46,6 @@ export fn len(v: XYZ) f32 {
     return norm(u);
 }
 
-const XYZ = extern struct { x: f32, y: f32, z: f32 };
-
 export fn crossProduct(u: XYZ, v: XYZ) XYZ {
     return .{
         .x = u.y * v.z - u.z * v.y,
@@ -86,12 +87,10 @@ export fn rotate(v: XYZ, deg_x: f32, deg_y: f32, deg_z: f32) XYZ {
     return u;
 }
 
-export fn segmentIntersectsSphere(a: XYZ, b: XYZ, i: XYZ, r: f32) bool {
-    // FIXME: call directly with vectors
+export fn segCrossSphere(a: XYZ, b: XYZ, i: XYZ, r: f32) bool {
     const p: @Vector(3, f32) = @bitCast(a);
     const q: @Vector(3, f32) = @bitCast(b);
     const c: @Vector(3, f32) = @bitCast(i);
-
     if (@reduce(.Or, @max(p, q) < c - splat(3, r))) return false;
     if (@reduce(.Or, @min(p, q) > c + splat(3, r))) return false;
     // https://en.wikipedia.org/wiki/Line–sphere_intersection
@@ -100,6 +99,56 @@ export fn segmentIntersectsSphere(a: XYZ, b: XYZ, i: XYZ, r: f32) bool {
     return sqr(dot(u, (p - c))) >= @reduce(.Add, sqr(p - c)) - sqr(r);
 }
 
+export fn segCrossTrigon(start: XYZ, end: XYZ,
+                         p_a: *const XYZ, p_b: *const XYZ, p_c: *const XYZ,
+                         normal: *const XYZ, intersection: *XYZ) bool {
+    const p: @Vector(3, f32) = @bitCast(start);
+    const q: @Vector(3, f32) = @bitCast(end);
+    const n: @Vector(3, f32) = @bitCast(normal.*);
+    const denom = dot(q - p, n);
+    if (@fabs(denom) < f32Eps)
+        return false; // parallel segment and triangle
+
+    const a: @Vector(3, f32) = @bitCast(p_a.*);
+    const mu = (dot(a, n) - dot(p, n)) / denom;
+    const i = p + (q - p) * splat(3, mu);
+    if (mu < 0 or mu > 1)
+        return false; // intersection not within segment
+
+    const n_abs = @fabs(n);
+    const n_max = @reduce(.Max, n_abs);
+    const k: struct { usize, usize } = if (n_max == n_abs[0])
+        .{ 1, 2 }
+    else if (n_max == n_abs[1])
+        .{ 0, 2 }
+    else if (n_max == n_abs[2])
+        .{ 0, 1 }
+    else unreachable;
+
+    const b: @Vector(3, f32) = @bitCast(p_b.*);
+    const c: @Vector(3, f32) = @bitCast(p_c.*);
+    const u = @Vector(3, f32){ i[k[0]], b[k[0]], c[k[0]] } - splat(3, a[k[0]]);
+    const v = @Vector(3, f32){ i[k[1]], b[k[1]], c[k[1]] } - splat(3, a[k[1]]);
+    intersection.* = @bitCast(i);
+
+    if (@fabs(u[1]) < f32Eps) {
+        const s = u[0] / u[2];
+        if (s >= 0 and s <= 1) {
+            const t = (v[0] - s * v[2]) / v[1];
+            if (t >= 0 and s + t <= 1)
+                return true;
+        }
+    } else {
+        const s = (v[0] * u[1] - u[0] * v[1]) / (v[2] * u[1] - u[2] * v[1]);
+        if (s >= 0 and s <= 1) {
+            const t = (u[0] - s * u[2]) / u[1];
+            if (t >= 0 and s + t <= 1)
+                return true;
+        }
+    }
+    return false;
+}
+
 fn transpose(comptime n: comptime_int, m: [n]@Vector(n, f32)) @TypeOf(m) {
     const flat: @Vector(sqr(n), f32) = @bitCast(m);
     return @bitCast(@shuffle(f32, flat, undefined, blk: {
diff --git a/src/main.zig b/src/main.zig
index b44b592..afffafd 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -37,6 +37,7 @@ var game: *c.Game = undefined;
 var prng: DefaultPrng = undefined;
 
 comptime {
+    _ = @import("decal.zig");
     _ = @import("geom.zig");
 } // export functions in C ABI