bullet trail particles

This commit is contained in:
jacob 2026-02-16 23:55:11 -06:00
parent 2f1a146c20
commit 53bcacb044
4 changed files with 105 additions and 150 deletions

View File

@ -2182,78 +2182,59 @@ void V_TickForever(WaveLaneCtx *lane)
// TODO: Not like this // TODO: Not like this
// for (P_Ent *bullet = P_FirstEnt(local_frame); !P_IsEntNil(bullet); bullet = P_NextEnt(bullet)) for (P_Ent *bullet = P_FirstEnt(local_frame); !P_IsEntNil(bullet); bullet = P_NextEnt(bullet))
// { {
// if (bullet->is_bullet) if (bullet->is_bullet)
// { {
// Vec2 start = bullet->bullet_start; Vec2 start = bullet->bullet_start;
// Vec2 end = bullet->bullet_end; Vec2 end = bullet->bullet_end;
// b32 skip = 0; b32 skip = 0;
// if (bullet->has_hit) if (bullet->has_hit)
// { {
// Vec2 hit_pos = bullet->hit_entry; Vec2 hit_pos = bullet->hit_entry;
// if (DotVec2(SubVec2(hit_pos, start), SubVec2(end, start)) < 0) if (DotVec2(SubVec2(hit_pos, start), SubVec2(end, start)) < 0)
// { {
// skip = 1; skip = 1;
// } }
// // V_DrawPoint(MulAffineVec2(frame->af.world_to_screen, start), Color_Red); // V_DrawPoint(MulAffineVec2(frame->af.world_to_screen, start), Color_Red);
// // V_DrawPoint(MulAffineVec2(frame->af.world_to_screen, end), Color_Purple); // V_DrawPoint(MulAffineVec2(frame->af.world_to_screen, end), Color_Purple);
// end = hit_pos; end = hit_pos;
// } }
// if (!skip) if (!skip)
// { {
// f32 trail_len = Vec2Len(SubVec2(end, start)); f32 trail_len = Vec2Len(SubVec2(end, start));
// f32 particles_count = trail_len * frame->dt * Kibi(8); // Particles per meter per second f32 particles_count = trail_len * frame->dt * Kibi(8); // Particles per meter per second
// particles_count = MaxF32(particles_count, 1); particles_count = MaxF32(particles_count, 1);
// f32 angle = AngleFromVec2(PerpVec2(SubVec2(end, start))); f32 angle = AngleFromVec2(PerpVec2(SubVec2(end, start)));
// // f32 angle = AngleFromVec2(NegVec2(SubVec2(end, start))); // f32 angle = AngleFromVec2(NegVec2(SubVec2(end, start)));
// V_Emitter emitter = Zi; V_Emitter emitter = Zi;
// { {
// // emitter.flags |= V_ParticleFlag_StainOnPrune; emitter.kind = V_ParticleKind_BulletTrail;
// // emitter.flags |= V_ParticleFlag_StainTrail; emitter.count = particles_count;
// // emitter.lifetime = 1; f32 angle_spread = Tau / 4.0;
// // emitter.lifetime_spread = 2;
// emitter.count = particles_count; emitter.angle.min = angle - angle_spread / 2;
emitter.angle.max = angle + angle_spread / 2;
// // emitter.lifetime = 1; emitter.pos.p0 = start;
// // emitter.lifetime = 0.15; emitter.pos.p1 = end;
// emitter.lifetime = 0.075;
// // emitter.lifetime = 0.05;
// // emitter.lifetime = 0.04;
// emitter.lifetime_spread = emitter.lifetime * 2;
// emitter.angle = angle; // emitter.color_lin = LinearFromSrgb(VEC4(0, 1, 0, 1));
// // emitter.angle_spread = Tau / 4;
// emitter.angle_spread = Tau / 4;
// emitter.start = start;
// emitter.end = end;
// // emitter.color_lin = LinearFromSrgb(VEC4(0, 1, 0, 1));
// // emitter.color_lin = LinearFromSrgb(VEC4(0.8, 0.8, 0.8, 0.25));
// emitter.color_lin = LinearFromSrgb(VEC4(0.8, 0.6, 0.2, 1)); // emitter.color_lin = LinearFromSrgb(VEC4(0.8, 0.6, 0.2, 1));
// // emitter.color_spread = LinearFromSrgb(VEC4(0, 0, 0, 0.2));
// emitter.speed = 0; emitter.speed.min = 0;
// emitter.speed_spread = 1; emitter.speed.max = 1;
}
// // emitter.speed = 1; V_PushParticles(emitter);
// // emitter.speed_spread = 1; }
}
// // emitter.velocity_falloff = 1; }
// // emitter.velocity_falloff_spread = 0;
// }
// V_PushParticles(emitter);
// }
// }
// }
@ -2385,7 +2366,7 @@ void V_TickForever(WaveLaneCtx *lane)
emitter.speed.max = speed + speed_spread * 0.5; emitter.speed.max = speed + speed_spread * 0.5;
emitter.angle.min = angle - angle_spread * 0.5; emitter.angle.min = angle - angle_spread * 0.5;
emitter.angle.max = angle + angle_spread * 0.5; emitter.angle.max = angle + angle_spread * 0.5;
emitter.count = TweakFloat("Emitter count", 1, 0, 100) * (Kibi(32) * frame->dt); emitter.count = TweakFloat("Emitter count", 1, 0, 100) * (Kibi(1) * frame->dt);
V_PushParticles(emitter); V_PushParticles(emitter);
} }
} }

View File

@ -20,7 +20,7 @@ Vec4 V_ColorFromParticle(V_ParticleDesc desc, u32 particle_idx, u32 density)
// Apply density // Apply density
{ {
if (desc.kind == V_ParticleKind_Smoke) if (desc.layer == V_ParticleLayer_Air)
{ {
// f32 t = saturate(density / 10.0); // f32 t = saturate(density / 10.0);
f32 t = smoothstep(-10, 32, density); f32 t = smoothstep(-10, 32, density);
@ -297,17 +297,6 @@ ComputeShader(V_SimParticlesCS, 64)
particle.life = 0; particle.life = 0;
particle.pos = lerp(emitter.pos.p0, emitter.pos.p1, rand_offset); particle.pos = lerp(emitter.pos.p0, emitter.pos.p1, rand_offset);
particle.velocity = Vec2(cos(initial_angle), sin(initial_angle)) * initial_speed; particle.velocity = Vec2(cos(initial_angle), sin(initial_angle)) * initial_speed;
Vec2 cell_pos = mul(frame.af.world_to_cell, Vec3(particle.pos, 1));
if (IsInside(cell_pos, P_WorldCellsDims))
{
particle.origin_occluder = occluders[cell_pos];
particle.prev_occluder = particle.origin_occluder;
}
else
{
prune = 1;
}
} }
if (particle.kind > V_ParticleKind_None && particle.kind < V_ParticleKind_COUNT && !prune) if (particle.kind > V_ParticleKind_None && particle.kind < V_ParticleKind_COUNT && !prune)
@ -322,17 +311,23 @@ ComputeShader(V_SimParticlesCS, 64)
StaticAssert(V_ParticlesCap <= (1 << 24)); // particle idx must fit in 24 bits StaticAssert(V_ParticlesCap <= (1 << 24)); // particle idx must fit in 24 bits
StaticAssert(V_ParticleKind_COUNT <= 0x7F); // particle kind must fit in 7 bits StaticAssert(V_ParticleKind_COUNT <= 0x7F); // particle kind must fit in 7 bits
u32 start_occluder = 0; if (particle.life == 0)
{ {
Vec2 cell_pos = mul(frame.af.world_to_cell, Vec3(particle.pos, 1)); Vec2 cell_pos = mul(frame.af.world_to_cell, Vec3(particle.pos, 1));
if (IsInside(cell_pos, P_WorldCellsDims)) if (IsInside(cell_pos, P_WorldCellsDims))
{ {
start_occluder = occluders[cell_pos]; u32 occluder = occluders[cell_pos];
if (particle.life == 0) b32 occluder_is_wall = occluder == 0xFFFFFFFF;
if (!(AnyBit(desc.flags, V_ParticleFlag_OnlyCollideWithWalls) && !occluder_is_wall))
{ {
particle.origin_occluder = start_occluder; particle.origin_occluder = occluders[cell_pos];
particle.prev_occluder = particle.origin_occluder;
} }
} }
else
{
prune = 1;
}
} }
////////////////////////////// //////////////////////////////
@ -341,6 +336,7 @@ ComputeShader(V_SimParticlesCS, 64)
b32 collision = 0; b32 collision = 0;
// TODO: Clip to avoid unnecessary iterations outside of world bounds // TODO: Clip to avoid unnecessary iterations outside of world bounds
if (!prune)
{ {
Vec2 p0 = particle.pos; Vec2 p0 = particle.pos;
Vec2 p1 = particle.pos + particle.velocity * frame.dt; Vec2 p1 = particle.pos + particle.velocity * frame.dt;
@ -415,11 +411,16 @@ ComputeShader(V_SimParticlesCS, 64)
//- Handle collision //- Handle collision
{ {
u32 occluder = occluders[cell_pos]; u32 occluder = occluders[cell_pos];
b32 occluder_is_wall = occluder == 0xFFFFFFFF;
if (occluder != particle.origin_occluder) if (occluder != particle.origin_occluder)
{ {
particle.origin_occluder = 0; particle.origin_occluder = 0;
} }
if (occluder != 0 && occluder != particle.origin_occluder) if (
occluder != 0 &&
!(AnyBit(desc.flags, V_ParticleFlag_OnlyCollideWithWalls) && !occluder_is_wall) &&
occluder != particle.origin_occluder
)
{ {
u64 collision_seed = MixU64(V_ParticleCellBasis ^ seed0 ^ particle.cells_count); u64 collision_seed = MixU64(V_ParticleCellBasis ^ seed0 ^ particle.cells_count);
f32 rand_collision_angle = Norm16(collision_seed >> 0); f32 rand_collision_angle = Norm16(collision_seed >> 0);
@ -481,7 +482,7 @@ ComputeShader(V_SimParticlesCS, 64)
} }
} }
if (!collision) if (!collision && particle.origin_occluder != 0xFFFFFFFF)
{ {
u32 stain_count = floor(particle.stain_accum); u32 stain_count = floor(particle.stain_accum);
u32 density = 1 + stain_count; u32 density = 1 + stain_count;
@ -702,7 +703,8 @@ ComputeShader2D(V_CompositeCS, 8, 8)
stain_color = orig_stain; stain_color = orig_stain;
} }
Vec4 particle_color = 0; Vec4 ground_particle_color = 0;
Vec4 air_particle_color = 0;
for (V_ParticleLayer layer = (V_ParticleLayer)0; layer < V_ParticleLayer_COUNT; layer += (V_ParticleLayer)1) for (V_ParticleLayer layer = (V_ParticleLayer)0; layer < V_ParticleLayer_COUNT; layer += (V_ParticleLayer)1)
@ -718,70 +720,26 @@ ComputeShader2D(V_CompositeCS, 8, 8)
u32 particle_idx = packed & ((1 << 24) - 1); u32 particle_idx = packed & ((1 << 24) - 1);
Vec4 cell_color = V_ColorFromParticle(desc, particle_idx, density); Vec4 cell_color = V_ColorFromParticle(desc, particle_idx, density);
cell_color.rgb *= cell_color.a; cell_color.rgb *= cell_color.a;
particle_color = BlendPremul(cell_color, particle_color);
if (layer == V_ParticleLayer_Ground)
{
ground_particle_color = BlendPremul(cell_color, ground_particle_color);
}
else
{
air_particle_color = BlendPremul(cell_color, air_particle_color);
}
} }
} }
// Darken wall particles / stains // Darken wall particles / stains
if (tile == P_TileKind_Wall) if (tile == P_TileKind_Wall)
{ {
particle_color *= 0.25; ground_particle_color *= 0.25;
air_particle_color *= 0.25;
stain_color *= 0.25; stain_color *= 0.25;
} }
// Vec4 stain_particle_color = 0;
// Vec4 ground_particle_color = 0;
// Vec4 air_particle_color = 0;
// {
// //- Stain
// {
// {
// u32 packed = stain_cells[cell_pos];
// V_ParticleKind particle_kind = (V_ParticleKind)((packed >> 24) & 0x7F);
// if (particle_kind != V_ParticleKind_None)
// {
// u32 particle_idx = packed & ((1 << 24) - 1);
// u32 density = stain_densities[cell_pos];
// f32 dryness = drynesses[cell_pos];
// stain_particle_color = V_ColorFromParticle(particle_kind, particle_idx, density, dryness);
// }
// }
// stain_particle_color.rgb *= 1.0 - (0.30 * tile_is_wall); // Darken wall stains
// stain_particle_color.rgb *= stain_particle_color.a;
// }
// //- Ground
// {
// {
// u32 packed = ground_cells[cell_pos];
// V_ParticleKind particle_kind = (V_ParticleKind)((packed >> 24) & 0x7F);
// if (particle_kind != V_ParticleKind_None)
// {
// u32 particle_idx = packed & ((1 << 24) - 1);
// u32 density = ground_densities[cell_pos];
// ground_particle_color = V_ColorFromParticle(particle_kind, particle_idx, density, 0);
// }
// }
// ground_particle_color.rgb *= ground_particle_color.a;
// }
// //- Air
// {
// {
// u32 packed = air_cells[cell_pos];
// V_ParticleKind particle_kind = (V_ParticleKind)((packed >> 24) & 0x7F);
// if (particle_kind != V_ParticleKind_None)
// {
// u32 particle_idx = packed & ((1 << 24) - 1);
// u32 density = air_densities[cell_pos];
// air_particle_color = V_ColorFromParticle(particle_kind, particle_idx, density, 0);
// }
// }
// air_particle_color.rgb *= air_particle_color.a;
// }
// }
////////////////////////////// //////////////////////////////
//- Compose world //- Compose world
@ -792,15 +750,16 @@ ComputeShader2D(V_CompositeCS, 8, 8)
{ {
world_color = BlendPremul(tile_color, world_color); // Blend ground tile world_color = BlendPremul(tile_color, world_color); // Blend ground tile
world_color = BlendPremul(stain_color, world_color); // Blend ground stain world_color = BlendPremul(stain_color, world_color); // Blend ground stain
world_color = BlendPremul(particle_color, world_color); // Blend ground particle world_color = BlendPremul(ground_particle_color, world_color); // Blend ground particle
} }
world_color = BlendPremul(albedo_tex_color, world_color); world_color = BlendPremul(albedo_tex_color, world_color);
if (tile_is_wall) if (tile_is_wall)
{ {
world_color = BlendPremul(tile_color, world_color); // Blend wall tile world_color = BlendPremul(tile_color, world_color); // Blend wall tile
world_color = BlendPremul(stain_color, world_color); // Blend wall stain world_color = BlendPremul(stain_color, world_color); // Blend wall stain
world_color = BlendPremul(particle_color, world_color); // Blend wall particle world_color = BlendPremul(ground_particle_color, world_color); // Blend wall particle
} }
world_color = BlendPremul(air_particle_color, world_color); // Blend air particle

View File

@ -11,42 +11,47 @@ V_ParticleDesc V_DescFromParticleKind(V_ParticleKind kind)
V_ParticleDesc result; V_ParticleDesc result;
{ {
PERSIST Readonly V_ParticleFlag flags[V_ParticleKind_COUNT] = { PERSIST Readonly V_ParticleFlag flags[V_ParticleKind_COUNT] = {
#define X(name, flags, layer, stain_rate, pen_rate, r, g, b, a) flags, #define X(name, flags, layer, stain_rate, pen_rate, lifetime, r, g, b, a) flags,
V_ParticlesXList(X) V_ParticlesXList(X)
#undef X #undef X
}; };
PERSIST Readonly V_ParticleLayer layers[V_ParticleKind_COUNT] = { PERSIST Readonly V_ParticleLayer layers[V_ParticleKind_COUNT] = {
#define X(name, flags, layer, stain_rate, pen_rate, r, g, b, a) layer, #define X(name, flags, layer, stain_rate, pen_rate, lifetime, r, g, b, a) layer,
V_ParticlesXList(X) V_ParticlesXList(X)
#undef X #undef X
}; };
PERSIST Readonly f32 stain_rates[V_ParticleKind_COUNT] = { PERSIST Readonly f32 stain_rates[V_ParticleKind_COUNT] = {
#define X(name, flags, layer, stain_rate, pen_rate, r, g, b, a) stain_rate, #define X(name, flags, layer, stain_rate, pen_rate, lifetime, r, g, b, a) stain_rate,
V_ParticlesXList(X) V_ParticlesXList(X)
#undef X #undef X
}; };
PERSIST Readonly f32 pen_rates[V_ParticleKind_COUNT] = { PERSIST Readonly f32 pen_rates[V_ParticleKind_COUNT] = {
#define X(name, flags, layer, stain_rate, pen_rate, r, g, b, a) pen_rate, #define X(name, flags, layer, stain_rate, pen_rate, lifetime, r, g, b, a) pen_rate,
V_ParticlesXList(X)
#undef X
};
PERSIST Readonly f32 lifetimes[V_ParticleKind_COUNT] = {
#define X(name, flags, layer, stain_rate, pen_rate, lifetime, r, g, b, a) lifetime,
V_ParticlesXList(X) V_ParticlesXList(X)
#undef X #undef X
}; };
PERSIST Readonly f32 r[V_ParticleKind_COUNT] = { PERSIST Readonly f32 r[V_ParticleKind_COUNT] = {
#define X(name, flags, layer, stain_rate, pen_rate, r, g, b, a) r, #define X(name, flags, layer, stain_rate, pen_rate, lifetime, r, g, b, a) r,
V_ParticlesXList(X) V_ParticlesXList(X)
#undef X #undef X
}; };
PERSIST Readonly f32 g[V_ParticleKind_COUNT] = { PERSIST Readonly f32 g[V_ParticleKind_COUNT] = {
#define X(name, flags, layer, stain_rate, pen_rate, r, g, b, a) g, #define X(name, flags, layer, stain_rate, pen_rate, lifetime, r, g, b, a) g,
V_ParticlesXList(X) V_ParticlesXList(X)
#undef X #undef X
}; };
PERSIST Readonly f32 b[V_ParticleKind_COUNT] = { PERSIST Readonly f32 b[V_ParticleKind_COUNT] = {
#define X(name, flags, layer, stain_rate, pen_rate, r, g, b, a) b, #define X(name, flags, layer, stain_rate, pen_rate, lifetime, r, g, b, a) b,
V_ParticlesXList(X) V_ParticlesXList(X)
#undef X #undef X
}; };
PERSIST Readonly f32 a[V_ParticleKind_COUNT] = { PERSIST Readonly f32 a[V_ParticleKind_COUNT] = {
#define X(name, flags, layer, stain_rate, pen_rate, r, g, b, a) a, #define X(name, flags, layer, stain_rate, pen_rate, lifetime, r, g, b, a) a,
V_ParticlesXList(X) V_ParticlesXList(X)
#undef X #undef X
}; };
@ -55,6 +60,7 @@ V_ParticleDesc V_DescFromParticleKind(V_ParticleKind kind)
result.layer = layers[kind]; result.layer = layers[kind];
result.stain_rate = stain_rates[kind]; result.stain_rate = stain_rates[kind];
result.pen_rate = pen_rates[kind]; result.pen_rate = pen_rates[kind];
result.lifetime = lifetimes[kind];
result.color = LinearFromSrgb(VEC4(r[kind], g[kind], b[kind], a[kind])); result.color = LinearFromSrgb(VEC4(r[kind], g[kind], b[kind], a[kind]));
} }
return result; return result;

View File

@ -32,6 +32,7 @@ Enum(V_ParticleFlag)
V_ParticleFlag_NoPruneWhenStill = (1 << 0), V_ParticleFlag_NoPruneWhenStill = (1 << 0),
V_ParticleFlag_StainWhenPruned = (1 << 1), V_ParticleFlag_StainWhenPruned = (1 << 1),
V_ParticleFlag_NoReflect = (1 << 2), V_ParticleFlag_NoReflect = (1 << 2),
V_ParticleFlag_OnlyCollideWithWalls = (1 << 3),
}; };
Enum(V_ParticleLayer) Enum(V_ParticleLayer)
@ -50,6 +51,7 @@ Enum(V_ParticleLayer)
/* Flags */ V_ParticleFlag_None, \ /* Flags */ V_ParticleFlag_None, \
/* Layer */ V_ParticleLayer_Ground, \ /* Layer */ V_ParticleLayer_Ground, \
/* Stain rate, pen chance */ 30, 0, \ /* Stain rate, pen chance */ 30, 0, \
/* Lifetime */ Inf, \
/* Base color */ 0, 0, 0, 0 \ /* Base color */ 0, 0, 0, 0 \
) \ ) \
\ \
@ -59,6 +61,7 @@ Enum(V_ParticleLayer)
/* Flags */ V_ParticleFlag_NoReflect | V_ParticleFlag_StainWhenPruned, \ /* Flags */ V_ParticleFlag_NoReflect | V_ParticleFlag_StainWhenPruned, \
/* Layer */ V_ParticleLayer_Ground, \ /* Layer */ V_ParticleLayer_Ground, \
/* Stain rate, pen chance */ 100, 0.25, \ /* Stain rate, pen chance */ 100, 0.25, \
/* Lifetime */ Inf, \
/* Base color */ 0.5, 0.1, 0.1, 0.05 \ /* Base color */ 0.5, 0.1, 0.1, 0.05 \
) \ ) \
X( \ X( \
@ -66,6 +69,7 @@ Enum(V_ParticleLayer)
/* Flags */ V_ParticleFlag_StainWhenPruned, \ /* Flags */ V_ParticleFlag_StainWhenPruned, \
/* Layer */ V_ParticleLayer_Mid, \ /* Layer */ V_ParticleLayer_Mid, \
/* Stain rate, pen chance */ 30, 0, \ /* Stain rate, pen chance */ 30, 0, \
/* Lifetime */ Inf, \
/* Base color */ 0.5, 0.1, 0.1, 0.8 \ /* Base color */ 0.5, 0.1, 0.1, 0.8 \
) \ ) \
X( \ X( \
@ -73,23 +77,26 @@ Enum(V_ParticleLayer)
/* Flags */ V_ParticleFlag_StainWhenPruned, \ /* Flags */ V_ParticleFlag_StainWhenPruned, \
/* Layer */ V_ParticleLayer_Mid, \ /* Layer */ V_ParticleLayer_Mid, \
/* Stain rate, pen chance */ 0, 0, \ /* Stain rate, pen chance */ 0, 0, \
/* Lifetime */ Inf, \
/* Base color */ 2, 0.5, 0, 1 \ /* Base color */ 2, 0.5, 0, 1 \
) \ ) \
\ \
/* Air particles */ \ /* Air particles */ \
X( \ X( \
/* Name */ Smoke, \ /* Name */ Smoke, \
/* Flags */ V_ParticleFlag_None, \ /* Flags */ V_ParticleFlag_OnlyCollideWithWalls, \
/* Layer */ V_ParticleLayer_Air, \ /* Layer */ V_ParticleLayer_Air, \
/* Stain rate, pen chance */ 0, 0, \ /* Stain rate, pen chance */ 0, 0, \
/* Lifetime */ Inf, \
/* Base color */ 0.15, 0.15, 0.15, 0.5 \ /* Base color */ 0.15, 0.15, 0.15, 0.5 \
) \ ) \
X( \ X( \
/* Name */ BulletTrail, \ /* Name */ BulletTrail, \
/* Flags */ V_ParticleFlag_None, \ /* Flags */ V_ParticleFlag_OnlyCollideWithWalls, \
/* Layer */ V_ParticleLayer_Air, \ /* Layer */ V_ParticleLayer_Air, \
/* Stain rate, pen chance */ 0, 0, \ /* Stain rate, pen chance */ 0, 0, \
/* Base color */ 1, 0, 1, 1 \ /* Lifetime */ 0.075, \
/* Base color */ 0.8, 0.6, 0.2, 0.25 \
) \ ) \
\ \
/* Test particles */ \ /* Test particles */ \
@ -98,6 +105,7 @@ Enum(V_ParticleLayer)
/* Flags */ V_ParticleFlag_None, \ /* Flags */ V_ParticleFlag_None, \
/* Layer */ V_ParticleLayer_Mid, \ /* Layer */ V_ParticleLayer_Mid, \
/* Stain rate, pen chance */ 0, 0, \ /* Stain rate, pen chance */ 0, 0, \
/* Lifetime */ Inf, \
/* Base color */ 1, 1, 0, 1 \ /* Base color */ 1, 1, 0, 1 \
) \ ) \
/* ----------------------------------------------------------------------------------------------------------------------------------- */ /* ----------------------------------------------------------------------------------------------------------------------------------- */
@ -142,6 +150,7 @@ Struct(V_ParticleDesc)
V_ParticleLayer layer; V_ParticleLayer layer;
f32 stain_rate; f32 stain_rate;
f32 pen_rate; f32 pen_rate;
f32 lifetime;
Vec4 color; Vec4 color;
}; };