#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 */ };