diff --git a/engine/core/src/math.zig b/engine/core/src/math.zig index e9770f2..9144439 100644 --- a/engine/core/src/math.zig +++ b/engine/core/src/math.zig @@ -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 { diff --git a/engine/rend/src/particles/particles.zig b/engine/rend/src/particles/particles.zig new file mode 100644 index 0000000..db01462 --- /dev/null +++ b/engine/rend/src/particles/particles.zig @@ -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; diff --git a/engine/rend/src/rend.zig b/engine/rend/src/rend.zig index 66e27a2..ab3abbe 100644 --- a/engine/rend/src/rend.zig +++ b/engine/rend/src/rend.zig @@ -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); diff --git a/engine/rend/src/sgpu/renderer.zig b/engine/rend/src/sgpu/renderer.zig index cb02bc5..37bd171 100644 --- a/engine/rend/src/sgpu/renderer.zig +++ b/engine/rend/src/sgpu/renderer.zig @@ -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; diff --git a/extras/gameExtras/src/debuggers/RendererDebug.zig b/extras/gameExtras/src/debuggers/RendererDebug.zig index c107803..57aae81 100644 --- a/extras/gameExtras/src/debuggers/RendererDebug.zig +++ b/extras/gameExtras/src/debuggers/RendererDebug.zig @@ -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); } diff --git a/projects/content/textures/DarkGrey.png b/projects/content/textures/DarkGrey.png new file mode 100644 index 0000000..9a0b169 Binary files /dev/null and b/projects/content/textures/DarkGrey.png differ diff --git a/projects/sampleGame/externGame/externGame.zig b/projects/sampleGame/externGame/externGame.zig index fb9bd3c..c05f6e5 100644 --- a/projects/sampleGame/externGame/externGame.zig +++ b/projects/sampleGame/externGame/externGame.zig @@ -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(); } } diff --git a/projects/sampleGame/main.zig b/projects/sampleGame/main.zig index 335606e..42d0e52 100644 --- a/projects/sampleGame/main.zig +++ b/projects/sampleGame/main.zig @@ -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);