summary refs log tree commit diff
path: root/src/Particle.c
diff options
context:
space:
mode:
authorWlodekM <[email protected]>2024-06-16 10:35:45 +0300
committerWlodekM <[email protected]>2024-06-16 10:35:45 +0300
commitabef6da56913f1c55528103e60a50451a39628b1 (patch)
treeb3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/Particle.c
initial commit
Diffstat (limited to 'src/Particle.c')
-rw-r--r--src/Particle.c619
1 files changed, 619 insertions, 0 deletions
diff --git a/src/Particle.c b/src/Particle.c
new file mode 100644
index 0000000..977fbfa
--- /dev/null
+++ b/src/Particle.c
@@ -0,0 +1,619 @@
+#include "Particle.h"
+#include "Block.h"
+#include "World.h"
+#include "ExtMath.h"
+#include "Lighting.h"
+#include "Entity.h"
+#include "TexturePack.h"
+#include "Graphics.h"
+#include "Funcs.h"
+#include "Game.h"
+#include "Event.h"
+
+
+/*########################################################################################################################*
+*------------------------------------------------------Particle base------------------------------------------------------*
+*#########################################################################################################################*/
+static GfxResourceID particles_TexId, particles_VB;
+#define PARTICLES_MAX 600
+static RNGState rnd;
+static cc_bool hitTerrain;
+typedef cc_bool (*CanPassThroughFunc)(BlockID b);
+
+void Particle_DoRender(const Vec2* size, const Vec3* pos, const TextureRec* rec, PackedCol col, struct VertexTextured* v) {
+	struct Matrix* view;
+	float sX, sY;
+	Vec3 centre;
+	float aX, aY, aZ, bX, bY, bZ;
+
+	sX = size->x * 0.5f; sY = size->y * 0.5f;
+	centre = *pos; centre.y += sY;
+	view   = &Gfx.View;
+	
+	aX = view->row1.x * sX; aY = view->row2.x * sX; aZ = view->row3.x * sX; /* right * size.x * 0.5f */
+	bX = view->row1.y * sY; bY = view->row2.y * sY; bZ = view->row3.y * sY; /* up    * size.y * 0.5f */
+
+	v->x = centre.x - aX - bX; v->y = centre.y - aY - bY; v->z = centre.z - aZ - bZ; v->Col = col; v->U = rec->u1; v->V = rec->v2; v++;
+	v->x = centre.x - aX + bX; v->y = centre.y - aY + bY; v->z = centre.z - aZ + bZ; v->Col = col; v->U = rec->u1; v->V = rec->v1; v++;
+	v->x = centre.x + aX + bX; v->y = centre.y + aY + bY; v->z = centre.z + aZ + bZ; v->Col = col; v->U = rec->u2; v->V = rec->v1; v++;
+	v->x = centre.x + aX - bX; v->y = centre.y + aY - bY; v->z = centre.z + aZ - bZ; v->Col = col; v->U = rec->u2; v->V = rec->v2; v++;
+}
+
+static cc_bool CollidesHor(Vec3* nextPos, BlockID block) {
+	Vec3 horPos = Vec3_Create3((float)Math_Floor(nextPos->x), 0.0f, (float)Math_Floor(nextPos->z));
+	Vec3 min, max;
+	Vec3_Add(&min, &Blocks.MinBB[block], &horPos);
+	Vec3_Add(&max, &Blocks.MaxBB[block], &horPos);
+	return nextPos->x >= min.x && nextPos->z >= min.z && nextPos->x < max.x && nextPos->z < max.z;
+}
+
+static BlockID GetBlock(int x, int y, int z) {
+	if (World_Contains(x, y, z)) return World_GetBlock(x, y, z);
+
+	if (y >= Env.EdgeHeight)  return BLOCK_AIR;
+	if (y >= Env_SidesHeight) return Env.EdgeBlock;
+	return Env.SidesBlock;
+}
+
+static cc_bool ClipY(struct Particle* p, int y, cc_bool topFace, CanPassThroughFunc canPassThrough) {
+	BlockID block;
+	Vec3 minBB, maxBB;
+	float collideY;
+	cc_bool collideVer;
+
+	if (y < 0) {
+		p->nextPos.y = ENTITY_ADJUSTMENT; 
+		p->lastPos.y = ENTITY_ADJUSTMENT;
+
+		Vec3_Set(p->velocity, 0,0,0);
+		hitTerrain = true;
+		return false;
+	}
+
+	block = GetBlock((int)p->nextPos.x, y, (int)p->nextPos.z);
+	if (canPassThrough(block)) return true;
+	minBB = Blocks.MinBB[block]; maxBB = Blocks.MaxBB[block];
+
+	collideY   = y + (topFace ? maxBB.y : minBB.y);
+	collideVer = topFace ? (p->nextPos.y < collideY) : (p->nextPos.y > collideY);
+
+	if (collideVer && CollidesHor(&p->nextPos, block)) {
+		float adjust = topFace ? ENTITY_ADJUSTMENT : -ENTITY_ADJUSTMENT;
+		p->lastPos.y = collideY + adjust;
+		p->nextPos.y = p->lastPos.y;
+
+		Vec3_Set(p->velocity, 0,0,0);
+		hitTerrain = true;
+		return false;
+	}
+	return true;
+}
+
+static cc_bool IntersectsBlock(struct Particle* p, CanPassThroughFunc canPassThrough) {
+	BlockID cur = GetBlock((int)p->nextPos.x, (int)p->nextPos.y, (int)p->nextPos.z);
+	float minY  = Math_Floor(p->nextPos.y) + Blocks.MinBB[cur].y;
+	float maxY  = Math_Floor(p->nextPos.y) + Blocks.MaxBB[cur].y;
+
+	return !canPassThrough(cur) && p->nextPos.y >= minY && p->nextPos.y < maxY && CollidesHor(&p->nextPos, cur);
+}
+
+static cc_bool PhysicsTick(struct Particle* p, float gravity, CanPassThroughFunc canPassThrough, float delta) {
+	Vec3 velocity;
+	int y, begY, endY;
+
+	p->lastPos = p->nextPos;
+	if (IntersectsBlock(p, canPassThrough)) return true;
+
+	p->velocity.y -= gravity * delta;
+	begY = Math_Floor(p->nextPos.y);
+	
+	Vec3_Mul1(&velocity, &p->velocity, delta * 3.0f);
+	Vec3_Add(&p->nextPos, &p->nextPos, &velocity);
+	endY = Math_Floor(p->nextPos.y);
+
+	if (p->velocity.y > 0.0f) {
+		/* don't test block we are already in */
+		for (y = begY + 1; y <= endY && ClipY(p, y, false, canPassThrough); y++) {}
+	} else {
+		for (y = begY; y >= endY && ClipY(p, y, true, canPassThrough); y--) {}
+	}
+
+	p->lifetime -= delta;
+	return p->lifetime < 0.0f;
+}
+
+
+/*########################################################################################################################*
+*-------------------------------------------------------Rain particle-----------------------------------------------------*
+*#########################################################################################################################*/
+static struct Particle rain_Particles[PARTICLES_MAX];
+static int rain_count;
+static TextureRec rain_rec = { 2.0f/128.0f, 14.0f/128.0f, 5.0f/128.0f, 16.0f/128.0f };
+
+static cc_bool RainParticle_CanPass(BlockID block) {
+	cc_uint8 draw = Blocks.Draw[block];
+	return draw == DRAW_GAS || draw == DRAW_SPRITE;
+}
+
+static cc_bool RainParticle_Tick(struct Particle* p, float delta) {
+	hitTerrain = false;
+	return PhysicsTick(p, 3.5f, RainParticle_CanPass, delta) || hitTerrain;
+}
+
+static void RainParticle_Render(struct Particle* p, float t, struct VertexTextured* vertices) {
+	Vec3 pos;
+	Vec2 size;
+	PackedCol col;
+	int x, y, z;
+
+	Vec3_Lerp(&pos, &p->lastPos, &p->nextPos, t);
+	size.x = p->size * 0.015625f; size.y = size.x;
+
+	x = Math_Floor(pos.x); y = Math_Floor(pos.y); z = Math_Floor(pos.z);
+	col = Lighting.Color(x, y, z);
+	Particle_DoRender(&size, &pos, &rain_rec, col, vertices);
+}
+
+static void Rain_Render(float t) {
+	struct VertexTextured* data;
+	int i;
+	if (!rain_count) return;
+	
+	data = (struct VertexTextured*)Gfx_LockDynamicVb(particles_VB, 
+										VERTEX_FORMAT_TEXTURED, rain_count * 4);
+	for (i = 0; i < rain_count; i++) {
+		RainParticle_Render(&rain_Particles[i], t, data);
+		data += 4;
+	}
+
+	Gfx_BindTexture(particles_TexId);
+	Gfx_UnlockDynamicVb(particles_VB);
+	Gfx_DrawVb_IndexedTris(rain_count * 4);
+}
+
+static void Rain_RemoveAt(int i) {
+	for (; i < rain_count - 1; i++) {
+		rain_Particles[i] = rain_Particles[i + 1];
+	}
+	rain_count--;
+}
+
+static void Rain_Tick(float delta) {
+	int i;
+	for (i = 0; i < rain_count; i++) {
+		if (RainParticle_Tick(&rain_Particles[i], delta)) {
+			Rain_RemoveAt(i); i--;
+		}
+	}
+}
+
+void Particles_RainSnowEffect(float x, float y, float z) {
+	struct Particle* p;
+	int i, type;
+
+	for (i = 0; i < 2; i++) {
+		if (rain_count == PARTICLES_MAX) Rain_RemoveAt(0);
+		p = &rain_Particles[rain_count++];
+
+		p->velocity.x = Random_Float(&rnd) * 0.8f - 0.4f; /* [-0.4, 0.4] */
+		p->velocity.z = Random_Float(&rnd) * 0.8f - 0.4f;
+		p->velocity.y = Random_Float(&rnd) + 0.4f;
+
+		p->lastPos.x = x + Random_Float(&rnd); /* [0.0, 1.0] */
+		p->lastPos.y = y + Random_Float(&rnd) * 0.1f + 0.01f;
+		p->lastPos.z = z + Random_Float(&rnd);
+
+		p->nextPos  = p->lastPos;
+		p->lifetime = 40.0f;
+
+		type = Random_Next(&rnd, 30);
+		p->size = type >= 28 ? 2 : (type >= 25 ? 4 : 3);
+	}
+}
+
+
+/*########################################################################################################################*
+*------------------------------------------------------Terrain particle---------------------------------------------------*
+*#########################################################################################################################*/
+struct TerrainParticle {
+	struct Particle base;
+	TextureRec rec;
+	TextureLoc texLoc;
+	BlockID block;
+};
+
+static struct TerrainParticle terrain_particles[PARTICLES_MAX];
+static int terrain_count;
+static cc_uint16 terrain_1DCount[ATLAS1D_MAX_ATLASES];
+static cc_uint16 terrain_1DIndices[ATLAS1D_MAX_ATLASES];
+
+static cc_bool TerrainParticle_CanPass(BlockID block) {
+	cc_uint8 draw = Blocks.Draw[block];
+	return draw == DRAW_GAS || draw == DRAW_SPRITE || Blocks.IsLiquid[block];
+}
+
+static cc_bool TerrainParticle_Tick(struct TerrainParticle* p, float delta) {
+	return PhysicsTick(&p->base, Blocks.ParticleGravity[p->block], TerrainParticle_CanPass, delta);
+}
+
+static void TerrainParticle_Render(struct TerrainParticle* p, float t, struct VertexTextured* vertices) {
+	PackedCol col = PACKEDCOL_WHITE;
+	Vec3 pos;
+	Vec2 size;
+	int x, y, z;
+
+	Vec3_Lerp(&pos, &p->base.lastPos, &p->base.nextPos, t);
+	size.x = p->base.size * 0.015625f; size.y = size.x;
+	
+	if (!Blocks.Brightness[p->block]) {
+		x = Math_Floor(pos.x); y = Math_Floor(pos.y); z = Math_Floor(pos.z);
+		col = Lighting.Color_XSide(x, y, z);
+	}
+
+	Block_Tint(col, p->block);
+	Particle_DoRender(&size, &pos, &p->rec, col, vertices);
+}
+
+static void Terrain_Update1DCounts(void) {
+	int i, index;
+
+	for (i = 0; i < ATLAS1D_MAX_ATLASES; i++) {
+		terrain_1DCount[i]   = 0;
+		terrain_1DIndices[i] = 0;
+	}
+	for (i = 0; i < terrain_count; i++) {
+		index = Atlas1D_Index(terrain_particles[i].texLoc);
+		terrain_1DCount[index] += 4;
+	}
+	for (i = 1; i < Atlas1D.Count; i++) {
+		terrain_1DIndices[i] = terrain_1DIndices[i - 1] + terrain_1DCount[i - 1];
+	}
+}
+
+static void Terrain_Render(float t) {
+	struct VertexTextured* data;
+	struct VertexTextured* ptr;
+	int offset = 0;
+	int i, index;
+	if (!terrain_count) return;
+
+	data = (struct VertexTextured*)Gfx_LockDynamicVb(particles_VB, 
+										VERTEX_FORMAT_TEXTURED, terrain_count * 4);
+	Terrain_Update1DCounts();
+	for (i = 0; i < terrain_count; i++) {
+		index = Atlas1D_Index(terrain_particles[i].texLoc);
+		ptr   = data + terrain_1DIndices[index];
+
+		TerrainParticle_Render(&terrain_particles[i], t, ptr);
+		terrain_1DIndices[index] += 4;
+	}
+
+	Gfx_UnlockDynamicVb(particles_VB);
+	for (i = 0; i < Atlas1D.Count; i++) {
+		int partCount = terrain_1DCount[i];
+		if (!partCount) continue;
+
+		Gfx_BindTexture(Atlas1D.TexIds[i]);
+		Gfx_DrawVb_IndexedTris_Range(partCount, offset);
+		offset += partCount;
+	}
+}
+
+static void Terrain_RemoveAt(int i) {
+	for (; i < terrain_count - 1; i++) {
+		terrain_particles[i] = terrain_particles[i + 1];
+	}
+	terrain_count--;
+}
+
+static void Terrain_Tick(float delta) {
+	int i;
+	for (i = 0; i < terrain_count; i++) {
+		if (TerrainParticle_Tick(&terrain_particles[i], delta)) {
+			Terrain_RemoveAt(i); i--;
+		}
+	}
+}
+
+void Particles_BreakBlockEffect(IVec3 coords, BlockID old, BlockID now) {
+	struct TerrainParticle* p;
+	TextureLoc loc;
+	int texIndex;
+	TextureRec baseRec, rec;
+	Vec3 origin, minBB, maxBB;
+
+	/* texture UV variables */
+	float uScale, vScale, maxU2, maxV2;
+	int minX, minZ, maxX, maxZ;
+	int minU, minV, maxU, maxV;
+	int maxUsedU, maxUsedV;
+	
+	/* per-particle variables */
+	float cellX, cellY, cellZ;
+	Vec3 cell;
+	int x, y, z, type;
+
+	if (now != BLOCK_AIR || Blocks.Draw[old] == DRAW_GAS) return;
+	IVec3_ToVec3(&origin, &coords);
+	loc = Block_Tex(old, FACE_XMIN);
+	
+	baseRec = Atlas1D_TexRec(loc, 1, &texIndex);
+	uScale  = (1.0f/16.0f); vScale = (1.0f/16.0f) * Atlas1D.InvTileSize;
+
+	minBB = Blocks.MinBB[old];    maxBB = Blocks.MaxBB[old];
+	minX  = (int)(minBB.x * 16); maxX  = (int)(maxBB.x * 16);
+	minZ  = (int)(minBB.z * 16); maxZ  = (int)(maxBB.z * 16);
+
+	minU = min(minX, minZ); minV = (int)(16 - maxBB.y * 16);
+	maxU = min(maxX, maxZ); maxV = (int)(16 - minBB.y * 16);
+	/* This way we can avoid creating particles which outside the bounds and need to be clamped */
+	maxUsedU = maxU; maxUsedV = maxV;
+	if (minU < 12 && maxU > 12) maxUsedU = 12;
+	if (minV < 12 && maxV > 12) maxUsedV = 12;
+
+	#define GRID_SIZE 4
+	/* gridOffset gives the centre of the cell on a grid */
+	#define CELL_CENTRE ((1.0f / GRID_SIZE) * 0.5f)
+
+	maxU2 = baseRec.u1 + maxU * uScale;
+	maxV2 = baseRec.v1 + maxV * vScale;
+	for (x = 0; x < GRID_SIZE; x++) {
+		for (y = 0; y < GRID_SIZE; y++) {
+			for (z = 0; z < GRID_SIZE; z++) {
+
+				cellX = (float)x / GRID_SIZE; cellY = (float)y / GRID_SIZE; cellZ = (float)z / GRID_SIZE;
+				cell  = Vec3_Create3(CELL_CENTRE + cellX, CELL_CENTRE / 2 + cellY, CELL_CENTRE + cellZ);
+				if (cell.x < minBB.x || cell.x > maxBB.x || cell.y < minBB.y
+					|| cell.y > maxBB.y || cell.z < minBB.z || cell.z > maxBB.z) continue;
+
+				if (terrain_count == PARTICLES_MAX) Terrain_RemoveAt(0);
+				p = &terrain_particles[terrain_count++];
+
+				/* centre random offset around [-0.2, 0.2] */
+				p->base.velocity.x = CELL_CENTRE + (cellX - 0.5f) + (Random_Float(&rnd) * 0.4f - 0.2f);
+				p->base.velocity.y = CELL_CENTRE + (cellY - 0.0f) + (Random_Float(&rnd) * 0.4f - 0.2f);
+				p->base.velocity.z = CELL_CENTRE + (cellZ - 0.5f) + (Random_Float(&rnd) * 0.4f - 0.2f);
+
+				rec = baseRec;
+				rec.u1 = baseRec.u1 + Random_Range(&rnd, minU, maxUsedU) * uScale;
+				rec.v1 = baseRec.v1 + Random_Range(&rnd, minV, maxUsedV) * vScale;
+				rec.u2 = rec.u1 + 4 * uScale;
+				rec.v2 = rec.v1 + 4 * vScale;
+				rec.u2 = min(rec.u2, maxU2) - 0.01f * uScale;
+				rec.v2 = min(rec.v2, maxV2) - 0.01f * vScale;
+		
+				Vec3_Add(&p->base.lastPos, &origin, &cell);
+				p->base.nextPos  = p->base.lastPos;
+				p->base.lifetime = 0.3f + Random_Float(&rnd) * 1.2f;
+
+				p->rec    = rec;
+				p->texLoc = loc;
+				p->block  = old;
+				type = Random_Next(&rnd, 30);
+				p->base.size = type >= 28 ? 12 : (type >= 25 ? 10 : 8);
+			}
+		}
+	}
+}
+
+
+/*########################################################################################################################*
+*-------------------------------------------------------Custom particle---------------------------------------------------*
+*#########################################################################################################################*/
+#ifdef CC_BUILD_NETWORKING
+struct CustomParticle {
+	struct Particle base;
+	int effectId;
+	float totalLifespan;
+};
+
+struct CustomParticleEffect Particles_CustomEffects[256];
+static struct CustomParticle custom_particles[PARTICLES_MAX];
+static int custom_count;
+static cc_uint8 collideFlags;
+#define EXPIRES_UPON_TOUCHING_GROUND (1 << 0)
+#define SOLID_COLLIDES  (1 << 1)
+#define LIQUID_COLLIDES (1 << 2)
+#define LEAF_COLLIDES   (1 << 3)
+
+static cc_bool CustomParticle_CanPass(BlockID block) {
+	cc_uint8 draw, collide;
+	
+	draw = Blocks.Draw[block];
+	if (draw == DRAW_TRANSPARENT_THICK && !(collideFlags & LEAF_COLLIDES)) return true;
+
+	collide = Blocks.Collide[block];
+	if (collide == COLLIDE_SOLID  && (collideFlags & SOLID_COLLIDES))  return false;
+	if (collide == COLLIDE_LIQUID && (collideFlags & LIQUID_COLLIDES)) return false;
+	return true;
+}
+
+static cc_bool CustomParticle_Tick(struct CustomParticle* p, float delta) {
+	struct CustomParticleEffect* e = &Particles_CustomEffects[p->effectId];
+	hitTerrain   = false;
+	collideFlags = e->collideFlags;
+
+	return PhysicsTick(&p->base, e->gravity, CustomParticle_CanPass, delta)
+		|| (hitTerrain && (e->collideFlags & EXPIRES_UPON_TOUCHING_GROUND));
+}
+
+static void CustomParticle_Render(struct CustomParticle* p, float t, struct VertexTextured* vertices) {
+	struct CustomParticleEffect* e = &Particles_CustomEffects[p->effectId];
+	Vec3 pos;
+	Vec2 size;
+	PackedCol col;
+	TextureRec rec = e->rec;
+	int x, y, z;
+
+	float time_lived = p->totalLifespan - p->base.lifetime;
+	int curFrame = Math_Floor(e->frameCount * (time_lived / p->totalLifespan));
+	float shiftU = curFrame * (rec.u2 - rec.u1);
+
+	rec.u1 += shiftU;/* * 0.0078125f; */
+	rec.u2 += shiftU;/* * 0.0078125f; */
+
+	Vec3_Lerp(&pos, &p->base.lastPos, &p->base.nextPos, t);
+	size.x = p->base.size; size.y = size.x;
+
+	x = Math_Floor(pos.x); y = Math_Floor(pos.y); z = Math_Floor(pos.z);
+	col = e->fullBright ? PACKEDCOL_WHITE : Lighting.Color(x, y, z);
+	col = PackedCol_Tint(col, e->tintCol);
+
+	Particle_DoRender(&size, &pos, &rec, col, vertices);
+}
+
+static void Custom_Render(float t) {
+	struct VertexTextured* data;
+	int i;
+	if (!custom_count) return;
+
+	data = (struct VertexTextured*)Gfx_LockDynamicVb(particles_VB, 
+										VERTEX_FORMAT_TEXTURED, custom_count * 4);
+	for (i = 0; i < custom_count; i++) {
+		CustomParticle_Render(&custom_particles[i], t, data);
+		data += 4;
+	}
+
+	Gfx_BindTexture(particles_TexId);
+	Gfx_UnlockDynamicVb(particles_VB);
+	Gfx_DrawVb_IndexedTris(custom_count * 4);
+}
+
+static void Custom_RemoveAt(int i) {
+	for (; i < custom_count - 1; i++) {
+		custom_particles[i] = custom_particles[i + 1];
+	}
+	custom_count--;
+}
+
+static void Custom_Tick(float delta) {
+	int i;
+	for (i = 0; i < custom_count; i++) {
+		if (CustomParticle_Tick(&custom_particles[i], delta)) {
+			Custom_RemoveAt(i); i--;
+		}
+	}
+}
+
+void Particles_CustomEffect(int effectID, float x, float y, float z, float originX, float originY, float originZ) {
+	struct CustomParticle* p;
+	struct CustomParticleEffect* e = &Particles_CustomEffects[effectID];
+	int i, count = e->particleCount;
+	Vec3 offset, delta;
+	float d;
+
+	for (i = 0; i < count; i++) {
+		if (custom_count == PARTICLES_MAX) Custom_RemoveAt(0);
+		p = &custom_particles[custom_count++];
+		p->effectId = effectID;
+
+		offset.x = Random_Float(&rnd) - 0.5f;
+		offset.y = Random_Float(&rnd) - 0.5f;
+		offset.z = Random_Float(&rnd) - 0.5f;
+		Vec3_Normalise(&offset);
+
+		/* See https://karthikkaranth.me/blog/generating-random-points-in-a-sphere/ */
+		/* 'Using normally distributed random numbers' */
+		d  = Random_Float(&rnd);
+		d  = Math_Exp2(Math_Log2(d) / 3.0); /* d^1/3 for better distribution */
+		d *= e->spread;
+
+		p->base.lastPos.x = x + offset.x * d;
+		p->base.lastPos.y = y + offset.y * d;
+		p->base.lastPos.z = z + offset.z * d;
+		
+		Vec3 origin = { originX, originY, originZ };
+		Vec3_Sub(&delta, &p->base.lastPos, &origin);
+		Vec3_Normalise(&delta);
+
+		p->base.velocity.x = delta.x * e->speed;
+		p->base.velocity.y = delta.y * e->speed;
+		p->base.velocity.z = delta.z * e->speed;
+
+		p->base.nextPos  = p->base.lastPos;
+		p->base.lifetime = e->baseLifetime + (e->baseLifetime * e->lifetimeVariation) * ((Random_Float(&rnd) - 0.5f) * 2);
+		p->totalLifespan = p->base.lifetime;
+
+		p->base.size = e->size + (e->size * e->sizeVariation) * ((Random_Float(&rnd) - 0.5f) * 2);
+
+		/* Don't spawn custom particle inside a block (otherwise it appears */
+		/*   for a few frames, then disappears in first PhysicsTick call)*/
+		collideFlags = e->collideFlags;
+		if (IntersectsBlock(&p->base, CustomParticle_CanPass)) custom_count--;
+	}
+}
+#else
+static int custom_count;
+
+static void Custom_Render(float t) { }
+static void Custom_Tick(float delta) { }
+#endif
+
+
+/*########################################################################################################################*
+*--------------------------------------------------------Particles--------------------------------------------------------*
+*#########################################################################################################################*/
+void Particles_Render(float t) {
+	if (!terrain_count && !rain_count && !custom_count) return;
+
+	if (Gfx.LostContext) return;
+	if (!particles_VB)
+		particles_VB = Gfx_CreateDynamicVb(VERTEX_FORMAT_TEXTURED, PARTICLES_MAX * 4);
+
+	Gfx_SetAlphaTest(true);
+
+	Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED);
+	Terrain_Render(t);
+	Rain_Render(t);
+	Custom_Render(t);
+
+	Gfx_SetAlphaTest(false);
+}
+
+static void Particles_Tick(struct ScheduledTask* task) {
+	float delta = task->interval;
+	Terrain_Tick(delta);
+	Rain_Tick(delta);
+	Custom_Tick(delta);
+}
+
+
+/*########################################################################################################################*
+*---------------------------------------------------Particles component---------------------------------------------------*
+*#########################################################################################################################*/
+static void ParticlesPngProcess(struct Stream* stream, const cc_string* name) {
+	Game_UpdateTexture(&particles_TexId, stream, name, NULL, NULL);
+}
+static struct TextureEntry particles_entry = { "particles.png", ParticlesPngProcess };
+
+
+static void OnContextLost(void* obj) {
+	Gfx_DeleteDynamicVb(&particles_VB); 
+
+	if (Gfx.ManagedTextures) return;
+	Gfx_DeleteTexture(&particles_TexId);
+}
+
+static void OnBreakBlockEffect_Handler(void* obj, IVec3 coords, BlockID old, BlockID now) {
+	Particles_BreakBlockEffect(coords, old, now);
+}
+
+static void OnInit(void) {
+	ScheduledTask_Add(GAME_DEF_TICKS, Particles_Tick);
+	Random_SeedFromCurrentTime(&rnd);
+	TextureEntry_Register(&particles_entry);
+
+	Event_Register_(&UserEvents.BlockChanged, NULL, OnBreakBlockEffect_Handler);
+	Event_Register_(&GfxEvents.ContextLost,   NULL, OnContextLost);
+}
+
+static void OnFree(void) { OnContextLost(NULL); }
+
+static void OnReset(void) { rain_count = 0; terrain_count = 0; custom_count = 0; }
+
+struct IGameComponent Particles_Component = {
+	OnInit,  /* Init  */
+	OnFree,  /* Free  */
+	OnReset, /* Reset */
+	OnReset  /* OnNewMap */
+};