diff --git a/src/control/Replay.cpp b/src/control/Replay.cpp
index 2b7c6b62..5d651748 100644
--- a/src/control/Replay.cpp
+++ b/src/control/Replay.cpp
@@ -280,7 +280,7 @@ void CReplay::RecordThisFrame(void)
 		}
 		memory_required += sizeof(tPedUpdatePacket);
 	}
-	for (uint8 i = 0; i < 16; i++) {
+	for (uint8 i = 0; i < CBulletTraces::NUM_BULLET_TRACES; i++) {
 		if (!CBulletTraces::aTraces[i].m_bInUse)
 			continue;
 		memory_required += sizeof(tBulletTracePacket);
@@ -340,7 +340,7 @@ void CReplay::RecordThisFrame(void)
 		}
 		StorePedUpdate(p, i);
 	}
-	for (uint8 i = 0; i < 16; i++){
+	for (uint8 i = 0; i < CBulletTraces::NUM_BULLET_TRACES; i++){
 		if (!CBulletTraces::aTraces[i].m_bInUse)
 			continue;
 		tBulletTracePacket* bt = (tBulletTracePacket*)&Record.m_pBase[Record.m_nOffset];
@@ -348,8 +348,8 @@ void CReplay::RecordThisFrame(void)
 		bt->index = i;
 		bt->frames = CBulletTraces::aTraces[i].m_framesInUse;
 		bt->lifetime = CBulletTraces::aTraces[i].m_lifeTime;
-		bt->inf = CBulletTraces::aTraces[i].m_vecInf;
-		bt->sup = CBulletTraces::aTraces[i].m_vecSup;
+		bt->inf = CBulletTraces::aTraces[i].m_vecCurrentPos;
+		bt->sup = CBulletTraces::aTraces[i].m_vecTargetPos;
 		Record.m_nOffset += sizeof(*bt);
 	}
 	tEndOfFramePacket* eof = (tEndOfFramePacket*)&Record.m_pBase[Record.m_nOffset];
@@ -995,8 +995,8 @@ bool CReplay::PlayBackThisFrameInterpolation(CAddressInReplayBuffer *buffer, flo
 			CBulletTraces::aTraces[pb->index].m_bInUse = true;
 			CBulletTraces::aTraces[pb->index].m_framesInUse = pb->frames;
 			CBulletTraces::aTraces[pb->index].m_lifeTime = pb->lifetime;
-			CBulletTraces::aTraces[pb->index].m_vecInf = pb->inf;
-			CBulletTraces::aTraces[pb->index].m_vecSup = pb->sup;
+			CBulletTraces::aTraces[pb->index].m_vecCurrentPos = pb->inf;
+			CBulletTraces::aTraces[pb->index].m_vecTargetPos = pb->sup;
 			buffer->m_nOffset += sizeof(tBulletTracePacket);
 		}
 		default:
diff --git a/src/render/SpecialFX.cpp b/src/render/SpecialFX.cpp
index 76abcd5b..a14da60c 100644
--- a/src/render/SpecialFX.cpp
+++ b/src/render/SpecialFX.cpp
@@ -14,6 +14,8 @@
 #include "Particle.h"
 #include "General.h"
 #include "Camera.h"
+#include "Shadows.h"
+#include "main.h"
 
 WRAPPER void CSpecialFX::Render(void) { EAXJMP(0x518DC0); }
 WRAPPER void CSpecialFX::Update(void) { EAXJMP(0x518D40); }
@@ -21,9 +23,101 @@ WRAPPER void CSpecialFX::Update(void) { EAXJMP(0x518D40); }
 WRAPPER void CMotionBlurStreaks::RegisterStreak(int32 id, uint8 r, uint8 g, uint8 b, CVector p1, CVector p2) { EAXJMP(0x519460); }
 
 
-CBulletTrace (&CBulletTraces::aTraces)[16] = *(CBulletTrace(*)[16])*(uintptr*)0x72B1B8;
+CBulletTrace (&CBulletTraces::aTraces)[NUM_BULLET_TRACES] = *(CBulletTrace(*)[NUM_BULLET_TRACES])*(uintptr*)0x72B1B8;
+RxObjSpace3DVertex (&TraceVertices)[6] = *(RxObjSpace3DVertex(*)[6])*(uintptr*)0x649884;
+RwImVertexIndex (&TraceIndexList)[12] = *(RwImVertexIndex(*)[12])*(uintptr*)0x64986C;
 
-WRAPPER void CBulletTraces::Init(void) { EAXJMP(0x518DE0); }
+void CBulletTraces::Init(void)
+{
+	for (int i = 0; i < NUM_BULLET_TRACES; i++)
+		aTraces[i].m_bInUse = false;
+}
+
+void CBulletTraces::AddTrace(CVector* vecStart, CVector* vecTarget)
+{
+	int index;
+	for (index = 0; index < NUM_BULLET_TRACES; index++) {
+		if (!aTraces[index].m_bInUse)
+			break;
+	}
+	if (index == NUM_BULLET_TRACES)
+		return;
+	aTraces[index].m_vecCurrentPos = *vecStart;
+	aTraces[index].m_vecTargetPos = *vecTarget;
+	aTraces[index].m_bInUse = true;
+	aTraces[index].m_framesInUse = 0;
+	aTraces[index].m_lifeTime = 25 + CGeneral::GetRandomNumber() % 32;
+}
+
+void CBulletTraces::Render(void)
+{
+	for (int i = 0; i < NUM_BULLET_TRACES; i++) {
+		if (!aTraces[i].m_bInUse)
+			continue;
+		RwRenderStateSet(rwRENDERSTATEZWRITEENABLE, (void*)0);
+		RwRenderStateSet(rwRENDERSTATESRCBLEND, (void*)2);
+		RwRenderStateSet(rwRENDERSTATEDESTBLEND, (void*)2);
+		RwRenderStateSet(rwRENDERSTATETEXTURERASTER, gpShadowExplosionTex->raster);
+		CVector inf = aTraces[i].m_vecCurrentPos;
+		CVector sup = aTraces[i].m_vecTargetPos;
+		CVector center = (inf + sup) / 2;
+		CVector screenPos = CrossProduct(TheCamera.GetForward(), (sup - inf));
+		screenPos.Normalise();
+		screenPos /= 20;
+		uint8 intensity = aTraces[i].m_lifeTime;
+		uint32 color = 0xFF << 24 | intensity << 16 | intensity << 8 | intensity;
+		TraceVertices[0].color = color;
+		TraceVertices[1].color = color;
+		TraceVertices[2].color = color;
+		TraceVertices[3].color = color;
+		TraceVertices[4].color = color;
+		TraceVertices[5].color = color;
+		// cast to satisfy compiler
+		TraceVertices[0].objVertex = (const CVector&)(inf + screenPos);
+		TraceVertices[1].objVertex = (const CVector&)(inf - screenPos);
+		TraceVertices[2].objVertex = (const CVector&)(center + screenPos);
+		TraceVertices[3].objVertex = (const CVector&)(center - screenPos);
+		TraceVertices[4].objVertex = (const CVector&)(sup + screenPos);
+		TraceVertices[5].objVertex = (const CVector&)(sup - screenPos);
+		LittleTest();
+		if (RwIm3DTransform(TraceVertices, ARRAY_SIZE(TraceVertices), nil, 1)) {
+			RwIm3DRenderIndexedPrimitive(rwPRIMTYPETRILIST, TraceIndexList, ARRAY_SIZE(TraceIndexList));
+			RwIm3DEnd();
+		}
+	}
+	RwRenderStateSet(rwRENDERSTATEZWRITEENABLE, (void*)1);
+	RwRenderStateSet(rwRENDERSTATESRCBLEND, (void*)5);
+	RwRenderStateSet(rwRENDERSTATEDESTBLEND, (void*)6);
+}
+
+void CBulletTraces::Update(void)
+{
+	for (int i = 0; i < NUM_BULLET_TRACES; i++) {
+		if (aTraces[i].m_bInUse)
+			aTraces[i].Update();
+	}
+}
+
+void CBulletTrace::Update(void)
+{
+	if (m_framesInUse == 0) {
+		m_framesInUse++;
+		return;
+	}
+	if (m_framesInUse > 60) {
+		m_bInUse = false;
+		return;
+	}
+	CVector diff = m_vecCurrentPos - m_vecTargetPos;
+	float remaining = diff.Magnitude();
+	if (remaining > 0.8f)
+		m_vecCurrentPos = m_vecTargetPos + (remaining - 0.8f) / remaining * diff;
+	else
+		m_bInUse = false;
+	if (--m_lifeTime == 0)
+		m_bInUse = false;
+	m_framesInUse++;
+}
 
 WRAPPER void CBrightLights::RegisterOne(CVector pos, CVector up, CVector right, CVector fwd, uint8 type, uint8 unk1, uint8 unk2, uint8 unk3) { EAXJMP(0x51A410); }
 
@@ -460,6 +554,11 @@ CSpecialParticleStuff::UpdateBoatFoamAnimation(CMatrix* pMatrix)
 }
 
 STARTPATCHES
+	InjectHook(0x518DE0, &CBulletTraces::Init, PATCH_JUMP);
+	InjectHook(0x518E90, &CBulletTraces::AddTrace, PATCH_JUMP);
+	InjectHook(0x518F20, &CBulletTraces::Render, PATCH_JUMP);
+	InjectHook(0x519240, &CBulletTraces::Update, PATCH_JUMP);
+
 	InjectHook(0x51B070, &C3dMarker::AddMarker, PATCH_JUMP);
 	InjectHook(0x51B170, &C3dMarker::DeleteMarkerObject, PATCH_JUMP);
 	InjectHook(0x51B1B0, &C3dMarker::Render, PATCH_JUMP);
diff --git a/src/render/SpecialFX.h b/src/render/SpecialFX.h
index 7c0e3436..2d758fdd 100644
--- a/src/render/SpecialFX.h
+++ b/src/render/SpecialFX.h
@@ -15,19 +15,27 @@ public:
 
 struct CBulletTrace
 {
-	CVector m_vecInf;
-	CVector m_vecSup;
+	CVector m_vecCurrentPos;
+	CVector m_vecTargetPos;
 	bool m_bInUse;
 	uint8 m_framesInUse;
 	uint8 m_lifeTime;
+
+	void Update(void);
 };
 
 class CBulletTraces
 {
 public:
-	static CBulletTrace (&aTraces)[16];
+	enum {
+		NUM_BULLET_TRACES = 16
+	};
+	static CBulletTrace (&aTraces)[NUM_BULLET_TRACES];
 
 	static void Init(void);
+	static void AddTrace(CVector*, CVector*);
+	static void Render(void);
+	static void Update(void);
 };
 
 class CBrightLights