446 lines
14 KiB
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;
|