created particle system

This commit is contained in:
peterino2 2025-09-20 19:27:59 -07:00
parent d42db622e7
commit bba99d4d91
8 changed files with 498 additions and 1 deletions

View File

@ -9,6 +9,15 @@ const lua = @import("lua");
const pod = lua.pod;
// LUA END
// vector versions.
pub const f32x4 = @Vector(4, f32);
pub const f32x8 = @Vector(8, f32);
pub const f32x16 = @Vector(16, f32);
pub const f32x4_zero = std.mem.zeroes(@Vector(4, f32));
pub const f32x8_zero = std.mem.zeroes(@Vector(8, f32));
pub const f32x16_zero = std.mem.zeroes(@Vector(16, f32));
pub const Rayf = RayType(Vectorf);
pub fn matToScalef(mat: anytype) Vectorf {

View File

@ -0,0 +1,335 @@
// 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,
};
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) = .{},
renderInfo: std.ArrayListUnmanaged(RenderInfo) = .{},
particleAcceleration: ParticleRandRangef = .{ .min = 0.0, .max = 0.0 },
particleVelocity: ParticleRandRangef = .{ .min = 8, .max = 10 },
particleLife: ParticleRandRangef = .{ .min = 2, .max = 2 },
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: std.ArrayListUnmanaged(rend.IndexedMesh) = .{},
// empty list means will use t_white
texture: std.ArrayListUnmanaged(*rend.Texture) = .{},
emitterLife: f64 = 0.0, // 0.0 means this emitter lives forever.
emitterMaxLife: f64 = 0.0,
emitterShape: EmitterShape = .{ .spherical = .{} },
_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);
}
}
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.renderInfo.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();
const 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(),
};
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(gParticleAllocator, .{ .current = life, .max = life });
try self.particlesPVA.append(gParticleAllocator, pva);
// renderinfo not used rn
try self.renderInfo.append(gParticleAllocator, .{});
try self.finals.append(gParticleAllocator, std.mem.zeroes(core.Mat));
}
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 != .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);
}
pub fn initECS(self: *@This(), handle: core.SetHandle) void {
// get the mesh component
self.entity = core.Entity{ .handle = handle };
if (self.entity.fetch(core.Scene)) |scene| {
_ = scene;
} else {
_ = self.entity.addComponent(core.Scene);
}
}
pub fn deinit(self: *@This()) void {
_ = self;
}
};
var gParticleAllocator: std.mem.Allocator = undefined;
pub const Effect = struct {};
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 create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.particleArena = std.heap.ArenaAllocator.init(allocator),
.allocator = allocator,
};
gParticleAllocator = self.particleArena.allocator();
ParticleRandRangef.randomEngine = std.Random.DefaultPrng.init(0x1234);
ParticleRandRangef.randomFunc = ParticleRandRangef.randomEngine.random();
try core.defineComponent(ParticleEmitter, allocator);
return self;
}
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 {
core.undefineComponent(ParticleEmitter);
self.particleArena.deinit();
self.allocator.destroy(self);
}
};
const core = @import("core");
const rend = @import("../rend.zig");
const std = @import("std");
const tracy = core.tracy;

View File

@ -44,6 +44,11 @@ pub const Animator = animationSystem.Animator;
pub const setSkyboxTexture = renderer.setSkyboxTexture;
pub const particles = @import("particles/particles.zig");
pub const ParticleSystem = particles.ParticleSystem;
pub const ParticleEmitter = particles.ParticleEmitter;
var rendAllocator: std.mem.Allocator = undefined;
pub fn getAllocator() std.mem.Allocator {
return rendAllocator;
@ -66,6 +71,7 @@ pub fn start_module(spec: *core.SpecVariantMap, args: anytype, allocator: std.me
try renderer.start();
_ = try context().createRendererEngineObject(animationSystem.AnimationSystem);
_ = try context().createRendererEngineObject(particles.ParticleSystem);
try animationLoaders.initLoaders();
try core.defineComponentList(ComponentList, allocator);

View File

@ -1092,6 +1092,7 @@ pub fn registerRendererObject(comptime T: type, object: *anyopaque) !void {
pub fn setSkyboxTexture(name: []const u8) void {
context().skyboxSystem.skyboxTextureName = core.MakeName(name);
context().skyboxSystem.skyboxTexture = null;
}
pub const getMesh = mesh_pool.getMesh;

View File

@ -4,6 +4,8 @@ allocator: std.mem.Allocator,
directionalLightSettings: bool = false,
ssaoSettings: bool = false,
skyboxes: std.ArrayListUnmanaged(core.Name) = .{},
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This(), "extras.RendererDebug");
pub fn create(allocator: std.mem.Allocator) !*@This() {
@ -52,11 +54,24 @@ pub fn windowOpen(entry: *igu.MenuEntry, dt: f64) void {
_ = ig.sliderFloat("bias", &ssao.bias, 0.0025, 1.0, null, .{});
_ = ig.checkbox("enable SSAO", &ssao.enable);
}
var buffer: [256]u8 = undefined;
ig.textFmt("skyboxes", .{}) catch unreachable;
ig.separator();
for (self.skyboxes.items) |*name| {
const x = std.fmt.bufPrintZ(&buffer, "{s}##button", .{name.utf8()}) catch unreachable;
if (ig.smallButton(x)) {
rend.setSkyboxTexture(name.utf8());
}
}
}
ig.end();
}
pub fn destroy(self: *@This()) void {
self.skyboxes.deinit(self.allocator);
self.allocator.destroy(self);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

View File

@ -54,6 +54,8 @@ pub const ExternGameObject = struct {
a: bool = false,
// lib: bool = false,
//s: u32 = 0x42,
editParticle: ?core.Entity = null,
savedGravity: core.Vectorf = .{},
pub fn loadMap2(self: *@This()) !void {
if (self.tbMap) |tbMap| {
@ -69,6 +71,81 @@ pub const ExternGameObject = struct {
core.engine_log("map loaded", .{});
}
const ItemWidth = 80;
pub fn displayRandRangeImgui(comptime Name: []const u8, range: *rend.particles.ParticleRandRangef) void {
ig.textFmt(Name, .{}) catch {};
ig.sameLine(0, 10);
ig.setNextItemWidth(ItemWidth);
_ = ig.inputFloat("Min##particleEd" ++ Name, &range.min, 1.0, 5.0, null, .{});
ig.sameLine(0, 10);
ig.setNextItemWidth(ItemWidth);
_ = ig.inputFloat("Max##particleEd" ++ Name, &range.max, 1.0, 5.0, null, .{});
}
pub fn editVector(comptime Name: []const u8, range: *core.Vectorf) void {
ig.textFmt(Name, .{}) catch {};
ig.sameLine(0, 10);
ig.setNextItemWidth(ItemWidth);
_ = ig.inputFloat("X##particleEd" ++ Name, &range.x, 1.0, 5.0, null, .{});
ig.sameLine(0, 10);
ig.setNextItemWidth(ItemWidth);
_ = ig.inputFloat("Y##particleEd" ++ Name, &range.y, 1.0, 5.0, null, .{});
ig.sameLine(0, 10);
ig.setNextItemWidth(ItemWidth);
_ = ig.inputFloat("Z##particleEd" ++ Name, &range.z, 1.0, 5.0, null, .{});
}
pub fn particleDebug(self: *@This()) void {
if (ig.begin("particleDebugger", null, .{})) {
var buffer: [256]u8 = undefined;
for (rend.ParticleEmitter.BaseContainer.dense.items, 0..) |*emitter, i| {
ig.textFmt("emitter {d}", .{emitter.sparseIndex.index}) catch {};
if (ig.smallButton(std.fmt.bufPrintZ(&buffer, "edit##{d}", .{i}) catch unreachable)) {
self.editParticle = emitter.value.entity;
}
}
}
ig.end();
var checkbox: bool = true;
if (self.editParticle) |edit| {
if (edit.fetch(rend.ParticleEmitter)) |emitter| {
if (ig.begin("editParticle", null, .{})) {
displayRandRangeImgui("Acceleration", &emitter.particleAcceleration);
displayRandRangeImgui("Velocity", &emitter.particleVelocity);
displayRandRangeImgui("Life", &emitter.particleLife);
displayRandRangeImgui("spawnRate", &emitter.spawnRate);
if (emitter.gravity != null) {
checkbox = true;
} else {
checkbox = false;
}
if (ig.checkbox("hasGravity", &checkbox)) {
if (!checkbox) {
self.savedGravity = emitter.gravity.?;
emitter.gravity = null;
}
if (checkbox) {
emitter.gravity = self.savedGravity;
}
}
if (emitter.gravity != null) {
editVector("gravity", &emitter.gravity.?);
}
_ = ig.inputInt("maaxParticles##particleEd", @ptrCast(&emitter.maxParticles), 1, 5, .{});
}
ig.end();
}
}
}
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try Slack.create(allocator);
self.* = .{
@ -220,6 +297,7 @@ pub const ExternGameObject = struct {
}
}
}
self.particleDebug();
}
}

View File

@ -76,6 +76,27 @@ const assetReferences = [_]assets.AssetImportReference{
"m_crate",
.{ .path = "meshes/Crate.obj" },
),
assets.MakeImportRefOptions(
"Texture",
"t_dark_skybox",
.{
.textureCube = true,
.textureList = &.{
"textures/DarkGrey.png", // cubemapfacePositivex, right
"textures/DarkGrey.png", // cubemapfacePositivex, right
"textures/DarkGrey.png", // cubemapfacePositivex, right
"textures/DarkGrey.png", // cubemapfacePositivex, right
"textures/DarkGrey.png", // cubemapfacePositivex, right
"textures/DarkGrey.png", // cubemapfacePositivex, right
//"sky_air/cube_right.png", // cubemapfacePositivex, right
//"sky_air/cube_left.png", // cubemapfacePositivex, right
//"sky_air/cube_up.png", // cubemapfacePositivex, right
//"sky_air/cube_down.png", // cubemapfacePositivex, right
//"sky_air/cube_back.png", // cubemapfacePositivex, right
//"sky_air/cube_front.png", // cubemapfacePositivex, right
},
},
),
assets.MakeImportRefOptions(
"Texture",
"t_skybox",
@ -93,6 +114,34 @@ const assetReferences = [_]assets.AssetImportReference{
),
};
pub const ParticleObject = struct {
data: core.GameObjectData = undefined,
entity: core.Entity,
pub fn create(alloc: std.mem.Allocator) !*@This() {
const self = try alloc.create(@This());
const e = try core.createEntity();
const particle = e.addComponent(rend.ParticleEmitter).?;
particle._showDebug = true;
particle.start();
particle.gravity = .{ .y = -9.8 };
self.entity = e;
return self;
}
pub fn getEntity(self: *@This()) core.Entity {
return self.entity;
}
pub fn destroy(self: *@This()) void {
self.entity.destroy();
self.data.release(self);
}
};
pub const FoxObject = struct {
data: core.GameObjectData = undefined,
entity: core.Entity,
@ -106,6 +155,7 @@ pub const FoxObject = struct {
const mesh = fox.addComponent(rend.MeshComponent).?;
mesh.setMesh("m_fox");
mesh.setTexture("t_fox");
const animator = fox.addComponent(rend.Animator).?;
animator.setSkeleton("sk_fox");
animator.setAnimation("a_fox_survey");
@ -187,11 +237,14 @@ pub fn prepare(self: *@This()) !void {
// try self.tryLoadExtern("externGame");
// self.rendererDebugger = try extras.RendererDebug.create(self.allocator); //try core.createObject(extras.RendererDebug, .{});
_ = try core.createObject(extras.RendererDebug, .{});
const rendererDebug = try core.createObject(extras.RendererDebug, .{});
try rendererDebug.skyboxes.append(rendererDebug.allocator, core.MakeName("t_skybox"));
try rendererDebug.skyboxes.append(rendererDebug.allocator, core.MakeName("t_dark_skybox"));
//self.objectSpawner = try extras.ObjectSpawner.create(self.allocator);
self.objectSpawner = try core.createObject(extras.ObjectSpawner, .{});
try self.objectSpawner.addSpawnFunction("fox", FoxObject);
try self.objectSpawner.addSpawnFunction("particle", ParticleObject);
try self.objectSpawner.addSpawnFunction("doomplayer", DoomPlayer.DoomCanvas);
try self.objectSpawner.addSpawnFunction("empire", @import("empire.zig"));
try self.objectSpawner.addSpawnFunction("DamagedHelmet", DamagedHelmet);