Backlog/engine/rend/src/particles/particles.zig

446 lines
14 KiB
Zig

// particle system
//
// last one we need to implement is a ParticleComposite,
// this is a component which owns a particle emitter component (adding one if it does not exist)
//
// and generates/modifies particles system properties over time.
//
// just thinking... with meta reflection. I wonder if it is possible to codegen
// a generic "timeline" runner capable of modifying any arbitrary float, bool etc... over time.
//
// that could be
pub const EmitterState = enum {
dead, // emitter is not creating any particles
alive, // emitter is creating particles up to max_porticles
paused, // emitter is not updating
};
pub const ParticleRandRangef = struct {
min: f32 = 0,
max: f32 = 0,
pub var randomFunc: std.Random = undefined;
pub var randomEngine: std.Random.DefaultPrng = undefined;
pub fn set(self: *@This(), v: f32) void {
self.min = v;
self.max = v;
}
pub fn getRange(self: @This()) f32 {
if (std.math.approxEqAbs(f32, self.min, self.max, 0.0001))
return self.min;
const x = (randomFunc.float(f32) +
randomFunc.float(f32) +
randomFunc.float(f32) +
randomFunc.float(f32)) / 4;
return x * (self.max - self.min) + self.min;
}
};
pub const EmitterShape = union(enum(u8)) {
spherical: struct {
radius: f32 = 50.0,
innerRadius: f32 = 0,
// distrobution: =
},
conal: struct {
direction: core.Vectorf,
angle: f32,
innerRadius: f32 = 0,
},
};
pub const Life = struct {
current: f32,
max: f32,
};
pub const ParticlePVA = struct {
position: core.f32x4 = core.f32x4_zero,
velocity: core.f32x4 = core.f32x4_zero,
acceleration: core.f32x4 = core.f32x4_zero,
};
pub const Rotationals = struct {
quat: core.f32x4 = .{},
spinVector: core.f32x4 = .{},
angularMomentum: f32 = 0.0,
};
pub const RenderInfo = struct {
meshIndex: u32 = 0,
textureIndex: u32 = 0,
var quad_mesh: ?rend.IndexedMesh = null;
var default_texture: ?*rend.Texture = null;
pub fn getMesh(self: @This(), emitter: *const ParticleEmitter) rend.IndexedMesh {
if (emitter.mesh.items.len == 0) {
if (quad_mesh == null) {
quad_mesh = rend.getMesh("m_screenPlane").?;
}
return quad_mesh.?;
}
return emitter.mesh.items[self.meshIndex];
}
pub fn getTexture(self: @This(), emitter: *const ParticleEmitter) *rend.Texture {
if (emitter.texture.items.len == 0) {
if (default_texture == null) {
default_texture = rend.getTexture("t_white").?;
}
return default_texture.?;
}
return emitter.texture.items[self.textureIndex];
}
};
pub const ParticleTimelineBurst = struct {
count: u32,
};
pub const ParticleTimeline = struct { func: ParticleTimelineFunc = .{} };
pub const ParticleTimelineFunc = union(enum(u8)) { burst: ParticleTimelineBurst };
pub const VfxEmitter = struct {
emitter: ParticleEmitter,
timeline: ParticleTimeline,
parent: u32,
transform: core.Mat,
};
pub const VfxComponent = struct {
// aims to replace the ParticleEmitter
// instead of having just one particle emitter, the VfxComponent manages scenes for multiple particle emitters
pub var BaseContainer: *core.SparseSet(VfxComponent) = undefined;
pub const ComponentName = "rend.VfxComponent";
pub const ScriptExports: []const []const u8 = &.{};
entity: core.Entity = undefined,
emitterList: std.MultiArrayList(VfxEmitter) = .{},
pub fn initECS(self: *@This(), handle: core.SetHandle) void {
self.entity = core.Entity{ .handle = handle };
}
pub fn addEmitter(self: *@This()) *ParticleEmitter {
_ = self;
}
pub fn update(self: *@This(), dt: f64) void {
_ = dt;
_ = self;
}
pub fn deinitECS(self: *@This(), handle: core.SetHandle) void {
for (self.emitters.items) |emitter| {
emitter.deinitECS(handle);
}
}
};
pub const ParticleEmitter = struct {
pub var BaseContainer: *core.SparseSet(ParticleEmitter) = undefined;
pub const ComponentName = "ParticleEmitter";
pub const ScriptExports: []const []const u8 = &.{};
// per particle information
life: std.ArrayListUnmanaged(Life) = .{},
particlesPVA: std.ArrayListUnmanaged(ParticlePVA) = .{},
finals: std.ArrayListUnmanaged(core.Mat) = .{},
size: std.ArrayListUnmanaged(f32) = .{},
particleAcceleration: ParticleRandRangef = .{ .min = 0.0, .max = 0.0 },
particleVelocity: ParticleRandRangef = .{ .min = 8, .max = 10 },
particleLife: ParticleRandRangef = .{ .min = 2, .max = 2 },
particleSizeSpawn: ParticleRandRangef = .{ .min = 0.8, .max = 1.0 },
spawnRate: ParticleRandRangef = .{ .min = 10, .max = 10 },
nextSpawn: f64 = 0.0,
gravity: ?core.Vectorf = null,
maxParticles: u32 = 100,
// globalScale: f32 = 1.0, not implemented
state: EmitterState = .dead,
billboard: bool = true,
// if billboard is set, the billboard bit will be set
// and only position and scale will be used to render the particle.
// an empty list means that this will use quad_mesh
mesh: rend.IndexedMesh = undefined,
// empty list means will use t_white
texture: *rend.Texture = undefined,
emitterLife: f64 = 0.0, // 0.0 means this emitter lives forever.
emitterMaxLife: f64 = 0.0,
emitterShape: EmitterShape = .{ .spherical = .{} },
useLocalScene: bool = false,
_showDebug: bool = false,
entity: core.Entity = undefined,
pub fn updatePVA(self: *@This(), dt: f64) void {
// should generate SIMD operations
const gravity = if (self.gravity) |gravity| gravity.toZm() else core.f32x4_zero;
for (self.particlesPVA.items) |*pva| {
pva.velocity = pva.acceleration * @as(core.f32x4, @splat(@floatCast(dt))) + gravity * @as(core.f32x4, @splat(@floatCast(dt))) + pva.velocity;
pva.position = pva.velocity * @as(core.f32x4, @splat(@floatCast(dt))) + pva.position;
if (self._showDebug) {
core.debugSphere(core.Vectorf.fromArray(pva.position), 0.1, .{ .color = .{ .y = 1.0, .x = positionLength(pva.velocity) / 10 } });
// core.debugSphere(core.Vectorf.Zeroes, 0.1, .{});
}
}
}
inline fn positionLength(f: core.f32x4) f32 {
return @sqrt((f[0] * f[0]) + (f[1] * f[1]) + (f[2] * f[2]) + (f[3] * f[3]));
}
pub fn updateParticleLife(self: *@This(), dt: f64) void {
// should generate SIMD operations
const dt32: f32 = @floatCast(dt);
for (self.life.items) |*life| {
if (life.max > 0)
life.current -= dt32;
}
var i: usize = 0;
while (i < self.life.items.len) : (i += 1) {
if (self.life.items[i].current < 0) {
self.removeParticle(i);
}
}
if (self.useLocalScene) {
switch (self.emitterShape) {
.spherical => |spherical| {
i = 0;
while (i < self.particlesPVA.items.len) : (i += 1) {
if (positionLength(self.particlesPVA.items[i].position) > spherical.radius) {
self.removeParticle(i);
}
}
},
.conal => {
@panic("not implemented");
},
}
}
}
pub fn removeParticle(self: *@This(), i: usize) void {
_ = self.life.swapRemove(i);
_ = self.particlesPVA.swapRemove(i);
_ = self.finals.swapRemove(i);
_ = self.size.swapRemove(i);
}
pub fn updateSpawn(self: *@This(), dt: f64) void {
self.nextSpawn -= dt;
if (self.nextSpawn > 0) {
return;
}
if (self.nextSpawn < 0 and self.life.items.len >= self.maxParticles) {
self.nextSpawn = 0.0;
return;
}
while (self.nextSpawn < 0) {
self.spawnParticle() catch {
core.engine_err(" UNABLE TO SPAWN PARTICLE", .{});
};
// generate random values for the next spawn
self.nextSpawn += 1.0 / @as(f64, @floatCast(self.spawnRate.getRange()));
}
}
pub fn start(self: *@This()) void {
self.state = .alive;
}
pub fn generatePVA(self: *@This()) ParticlePVA {
switch (self.emitterShape) {
.spherical => |spherical| {
const radius = ParticleRandRangef{ .min = -spherical.innerRadius, .max = spherical.innerRadius };
const oneRand = ParticleRandRangef{ .min = -1.0, .max = 1.0 };
const v = core.Vectorf{ .x = oneRand.getRange(), .y = oneRand.getRange(), .z = oneRand.getRange() };
const v2 = v.normalize();
var pva: ParticlePVA = .{
.velocity = v2.fmul(self.particleVelocity.getRange()).toZm(),
.position = .{
radius.getRange(),
radius.getRange(),
radius.getRange(),
1.0,
},
.acceleration = v2.fmul(self.particleAcceleration.getRange()).toZm(),
};
if (!self.useLocalScene) {
const scenePos = core.zm.mul(pva.position, self.entity.fetch(core.Scene).?.getPosRot().toTransform());
pva.position = scenePos;
}
return pva;
},
.conal => |conal| {
_ = conal;
@panic("not implemented");
},
}
}
fn spawnParticle(self: *@This()) !void {
const pva = self.generatePVA();
const life = self.particleLife.getRange();
try self.life.append(particleAllocator(), .{ .current = life, .max = life });
try self.particlesPVA.append(particleAllocator(), pva);
try self.finals.append(particleAllocator(), std.mem.zeroes(core.Mat));
try self.size.append(particleAllocator(), self.particleSizeSpawn.getRange());
}
fn updateLife(self: *@This(), dt: f64) void {
if (self.emitterMaxLife <= 0) {
return;
}
self.emitterLife -= dt;
if (self.emitterLife <= 0) {
self.state = .dead;
}
}
pub fn update(self: *@This(), dt: f64) void {
if (self.state == .paused) {
return;
}
if (self.state != .dead) {
self.updateSpawn(dt);
}
// core.debugSphere(self.entity.fetch(core.Scene).?.getPosition(), self.emitterShape.spherical.radius, .{});
self.updatePVA(dt);
self.updateParticleLife(dt);
self.updateLife(dt);
self.updateFinals();
}
pub fn updateFinals(self: *@This()) void {
if (self.useLocalScene) {
const scene = self.entity.fetch(core.Scene).?;
const transform = scene.getPosRot().toTransform();
for (self.particlesPVA.items, 0..) |pva, i| {
const p = core.zm.translationV(pva.position);
self.finals.items[i] = core.zm.mul(p, transform);
}
} else {
for (self.particlesPVA.items, 0..) |pva, i| {
const p = core.zm.translationV(pva.position);
self.finals.items[i] = p;
}
}
}
var t_whiteName: core.Name = core.DefineName("t_white");
var m_quad: core.Name = core.DefineName("m_screenPlane");
pub fn initECS(self: *@This(), handle: core.ObjectHandle) void {
// get the mesh component
self.entity = core.Entity{ .handle = handle };
self.texture = rend.getTexture(&t_whiteName).?;
core.engine_logs("wtf");
self.mesh = rend.getMeshByName(&m_quad).?;
if (self.entity.fetch(core.Scene)) |scene| {
_ = scene;
} else {
_ = self.entity.addComponent(core.Scene);
}
}
pub fn deinitECS(self: *@This(), handle: core.ObjectHandle) void {
_ = handle;
self.deinit();
}
pub fn deinit(self: *@This()) void {
self.life.deinit(particleAllocator());
self.particlesPVA.deinit(particleAllocator());
self.finals.deinit(particleAllocator());
self.size.deinit(particleAllocator());
}
};
fn particleAllocator() std.mem.Allocator {
return core.get(ParticleSystem).particleArena.allocator();
}
// var gParticleAllocator: std.mem.Allocator = undefined;
pub const ParticleSystem = struct {
particleArena: std.heap.ArenaAllocator,
allocator: std.mem.Allocator,
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This(), "rend.ParticleSystem");
pub fn init(self: *@This(), allocator: std.mem.Allocator, first: bool) !void {
if (!first)
return;
self.* = .{
.particleArena = std.heap.ArenaAllocator.init(allocator),
.allocator = allocator,
};
ParticleRandRangef.randomEngine = std.Random.DefaultPrng.init(0x1234);
ParticleRandRangef.randomFunc = ParticleRandRangef.randomEngine.random();
}
pub fn tick(self: *@This(), dt: f64) void {
var z = tracy.ZoneN(@src(), "Particle System");
defer z.End();
_ = self;
for (ParticleEmitter.BaseContainer.dense.items) |*emitter| {
emitter.value.update(dt);
}
}
pub fn destroy(self: *@This()) void {
self.particleArena.deinit();
}
};
const core = @import("core");
const rend = @import("../rend.zig");
const std = @import("std");
const tracy = core.tracy;