From de304e49d34086d0c58a07198d52ef195b3fccf1 Mon Sep 17 00:00:00 2001 From: MnHs <68966087+RanPix@users.noreply.github.com> Date: Sun, 25 May 2025 20:53:05 +0200 Subject: [PATCH] Particles (#1367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/user-attachments/assets/de82e298-acfb-431d-973a-2d37b9a707ae resolves #293 --------- Co-authored-by: Krzysztof Wiśniewski Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com> Co-authored-by: Krzysztof Wiśniewski --- assets/cubyz/particles/poof.zig.zon | 3 + assets/cubyz/particles/textures/poof.png | Bin 0 -> 132 bytes assets/cubyz/shaders/particles/particles.frag | 17 + assets/cubyz/shaders/particles/particles.vert | 82 ++++ src/assets.zig | 15 +- src/game.zig | 3 + src/graphics.zig | 14 +- src/gui/windows/debug.zig | 2 + src/gui/windows/gpu_performance_measuring.zig | 2 + src/main.zig | 4 + src/particles.zig | 450 ++++++++++++++++++ src/renderer.zig | 11 + src/renderer/chunk_meshing.zig | 3 +- 13 files changed, 599 insertions(+), 7 deletions(-) create mode 100644 assets/cubyz/particles/poof.zig.zon create mode 100644 assets/cubyz/particles/textures/poof.png create mode 100644 assets/cubyz/shaders/particles/particles.frag create mode 100644 assets/cubyz/shaders/particles/particles.vert create mode 100644 src/particles.zig diff --git a/assets/cubyz/particles/poof.zig.zon b/assets/cubyz/particles/poof.zig.zon new file mode 100644 index 00000000..9e799ed8 --- /dev/null +++ b/assets/cubyz/particles/poof.zig.zon @@ -0,0 +1,3 @@ +.{ + .texture = "cubyz:poof", +} diff --git a/assets/cubyz/particles/textures/poof.png b/assets/cubyz/particles/textures/poof.png new file mode 100644 index 0000000000000000000000000000000000000000..10147b0ded9d9bf53243307eb9bc56686f5d6240 GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^EI=&40U~Gb7pnkLjKx9jP7LeL$-D$|>^xl@LpWxp zb{TRt7>FDV65Lh!-}u_um!(~Y+r12vg%Ttdy1uw0!$0>Vt3bPm;JK;=jpy{=xSW1z euXk> 2; + int vertexID = gl_VertexID & 3; + ParticleData particle = particleData[particleID]; + ParticleTypeData particleType = particleTypeData[particle.type]; + + uint fullLight = particle.light; + vec3 sunLight = vec3( + fullLight >> 25 & 31u, + fullLight >> 20 & 31u, + fullLight >> 15 & 31u + ); + vec3 blockLight = vec3( + fullLight >> 10 & 31u, + fullLight >> 5 & 31u, + fullLight >> 0 & 31u + ); + light = max(sunLight*ambientLight, blockLight)/31; + + float rotation = particle.rotation; + vec3 faceVertPos = facePositions[vertexID]; + float sn = sin(rotation); + float cs = cos(rotation); + const vec3 vertexRotationPos = vec3( + faceVertPos.x*cs - faceVertPos.y*sn, + faceVertPos.x*sn + faceVertPos.y*cs, + 0 + ); + + const vec3 vertexPos = (billboardMatrix*vec4(particleType.size*vertexRotationPos, 1)).xyz + particle.pos; + gl_Position = projectionAndViewMatrix*vec4(vertexPos, 1); + + float textureIndex = floor(particle.lifeRatio*particleType.animationFrames + particleType.startFrame); + textureCoords = vec3(uvPositions[vertexID], textureIndex); +} diff --git a/src/assets.zig b/src/assets.zig index 3f463355..7ab164ba 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -5,6 +5,7 @@ const items_zig = @import("items.zig"); const migrations_zig = @import("migrations.zig"); const blueprints_zig = @import("blueprint.zig"); const Blueprint = blueprints_zig.Blueprint; +const particles_zig = @import("particles.zig"); const ZonElement = @import("zon.zig").ZonElement; const main = @import("main"); const biomes_zig = main.server.terrain.biomes; @@ -33,6 +34,7 @@ pub const Assets = struct { models: BytesHashMap, structureBuildingBlocks: ZonHashMap, blueprints: BytesHashMap, + particles: ZonHashMap, fn init() Assets { return .{ @@ -46,6 +48,7 @@ pub const Assets = struct { .models = .{}, .structureBuildingBlocks = .{}, .blueprints = .{}, + .particles = .{}, }; } fn deinit(self: *Assets, allocator: NeverFailingAllocator) void { @@ -59,6 +62,7 @@ pub const Assets = struct { self.models.deinit(allocator.allocator); self.structureBuildingBlocks.deinit(allocator.allocator); self.blueprints.deinit(allocator.allocator); + self.particles.deinit(allocator.allocator); } fn clone(self: Assets, allocator: NeverFailingAllocator) Assets { return .{ @@ -72,6 +76,7 @@ pub const Assets = struct { .models = self.models.clone(allocator.allocator) catch unreachable, .structureBuildingBlocks = self.structureBuildingBlocks.clone(allocator.allocator) catch unreachable, .blueprints = self.blueprints.clone(allocator.allocator) catch unreachable, + .particles = self.particles.clone(allocator.allocator) catch unreachable, }; } fn read(self: *Assets, allocator: NeverFailingAllocator, assetPath: []const u8) void { @@ -88,12 +93,13 @@ pub const Assets = struct { addon.readAllZon(allocator, "sbb", true, &self.structureBuildingBlocks, null); addon.readAllBlueprints(allocator, "sbb", &self.blueprints); addon.readAllModels(allocator, &self.models); + addon.readAllZon(allocator, "particles", true, &self.particles, null); } } fn log(self: *Assets, typ: enum {common, world}) void { std.log.info( - "Finished {s} assets reading with {} blocks ({} migrations), {} items, {} tools, {} biomes ({} migrations), {} recipes, {} structure building blocks and {} blueprints", - .{@tagName(typ), self.blocks.count(), self.blockMigrations.count(), self.items.count(), self.tools.count(), self.biomes.count(), self.biomeMigrations.count(), self.recipes.count(), self.structureBuildingBlocks.count(), self.blueprints.count()}, + "Finished {s} assets reading with {} blocks ({} migrations), {} items, {} tools, {} biomes ({} migrations), {} recipes, {} structure building blocks, {} blueprints and {} particles", + .{@tagName(typ), self.blocks.count(), self.blockMigrations.count(), self.items.count(), self.tools.count(), self.biomes.count(), self.biomeMigrations.count(), self.recipes.count(), self.structureBuildingBlocks.count(), self.blueprints.count(), self.particles.count()}, ); } @@ -598,6 +604,11 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPale try sbb.registerBlueprints(&worldAssets.blueprints); try sbb.registerSBB(&worldAssets.structureBuildingBlocks); + iterator = worldAssets.particles.iterator(); + while(iterator.next()) |entry| { + particles_zig.ParticleManager.register(assetFolder, entry.key_ptr.*, entry.value_ptr.*); + } + // Biomes: var nextBiomeNumericId: u32 = 0; for(biomePalette.palette.items) |id| { diff --git a/src/game.zig b/src/game.zig index 969d63a6..d49ec23d 100644 --- a/src/game.zig +++ b/src/game.zig @@ -11,6 +11,7 @@ const ZonElement = @import("zon.zig").ZonElement; const main = @import("main"); const KeyBoard = main.KeyBoard; const network = @import("network.zig"); +const particles = @import("particles.zig"); const Connection = network.Connection; const ConnectionManager = network.ConnectionManager; const vec = @import("vec.zig"); @@ -674,6 +675,7 @@ pub const World = struct { // MARK: World main.Window.setMouseGrabbed(true); main.blocks.meshes.generateTextureArray(); + main.particles.ParticleManager.generateTextureArray(); main.models.uploadModels(); } @@ -1219,4 +1221,5 @@ pub fn update(deltaTime: f64) void { // MARK: update() fog.fogHigher = (biome.fogHigher - fog.fogHigher)*t + fog.fogHigher; world.?.update(); + particles.ParticleSystem.update(@floatCast(deltaTime)); } diff --git a/src/graphics.zig b/src/graphics.zig index fdb2ac15..4ab8107e 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -1829,9 +1829,15 @@ pub const SSBO = struct { // MARK: SSBO c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); } - pub fn createDynamicBuffer(self: SSBO, size: usize) void { + pub fn bufferSubData(self: SSBO, comptime T: type, data: []const T, length: usize) void { c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); - c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(size), null, c.GL_DYNAMIC_DRAW); + c.glBufferSubData(c.GL_SHADER_STORAGE_BUFFER, 0, @intCast(length*@sizeOf(T)), data.ptr); + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); + } + + pub fn createDynamicBuffer(self: SSBO, comptime T: type, size: usize) void { + c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, self.bufferID); + c.glBufferData(c.GL_SHADER_STORAGE_BUFFER, @intCast(size*@sizeOf(T)), null, c.GL_DYNAMIC_DRAW); c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0); } }; @@ -2159,8 +2165,8 @@ pub const TextureArray = struct { // MARK: TextureArray /// (Re-)Generates the GPU buffer. pub fn generate(self: TextureArray, images: []Image, mipmapping: bool, alphaCorrectMipmapping: bool) void { - var maxWidth: u31 = 0; - var maxHeight: u31 = 0; + var maxWidth: u31 = 1; + var maxHeight: u31 = 1; for(images) |image| { maxWidth = @max(maxWidth, image.width); maxHeight = @max(maxHeight, image.height); diff --git a/src/gui/windows/debug.zig b/src/gui/windows/debug.zig index a8fc1a24..441d014b 100644 --- a/src/gui/windows/debug.zig +++ b/src/gui/windows/debug.zig @@ -124,5 +124,7 @@ pub fn render() void { } draw.print("Opaque faces: {}, Transparent faces: {}", .{main.renderer.chunk_meshing.quadsDrawn, main.renderer.chunk_meshing.transparentQuadsDrawn}, 0, y, 8, .left); y += 8; + draw.print("Particle count: {}/{}", .{main.particles.ParticleSystem.getParticleCount(), main.particles.ParticleSystem.maxCapacity}, 0, y, 8, .left); + y += 8; } } diff --git a/src/gui/windows/gpu_performance_measuring.zig b/src/gui/windows/gpu_performance_measuring.zig index 1033af5c..4aca9f8c 100644 --- a/src/gui/windows/gpu_performance_measuring.zig +++ b/src/gui/windows/gpu_performance_measuring.zig @@ -21,6 +21,7 @@ pub const Samples = enum(u8) { chunk_rendering_occlusion_test, chunk_rendering_new_visible, entity_rendering, + particle_rendering, transparent_rendering_preparation, transparent_rendering_occlusion_test, transparent_rendering, @@ -41,6 +42,7 @@ const names = [_][]const u8{ "Chunk Rendering Occlusion Test", "Chunk Rendering New Visible", "Entity Rendering", + "Particle Rendering", "Transparent Rendering Preparation", "Transparent Rendering Occlusion Test", "Transparent Rendering", diff --git a/src/main.zig b/src/main.zig index 337b2a2e..5c2f5e8b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -23,6 +23,7 @@ pub const random = @import("random.zig"); pub const renderer = @import("renderer.zig"); pub const rotation = @import("rotation.zig"); pub const settings = @import("settings.zig"); +pub const particles = @import("particles.zig"); const tag = @import("tag.zig"); pub const Tag = tag.Tag; pub const utils = @import("utils.zig"); @@ -644,6 +645,9 @@ pub fn main() void { // MARK: main() gui.init(); defer gui.deinit(); + particles.ParticleManager.init(); + defer particles.ParticleManager.deinit(); + if(settings.playerName.len == 0) { gui.openWindow("change_name"); } else { diff --git a/src/particles.zig b/src/particles.zig new file mode 100644 index 00000000..fc2aad7e --- /dev/null +++ b/src/particles.zig @@ -0,0 +1,450 @@ +const std = @import("std"); + +const main = @import("main"); +const chunk_meshing = @import("renderer/chunk_meshing.zig"); +const graphics = @import("graphics.zig"); +const SSBO = graphics.SSBO; +const TextureArray = graphics.TextureArray; +const Shader = graphics.Shader; +const Image = graphics.Image; +const c = graphics.c; +const game = @import("game.zig"); +const ZonElement = @import("zon.zig").ZonElement; +const random = @import("random.zig"); +const vec = @import("vec.zig"); +const Mat4f = vec.Mat4f; +const Vec3d = vec.Vec3d; +const Vec4d = vec.Vec4d; +const Vec3f = vec.Vec3f; +const Vec4f = vec.Vec4f; +const Vec3i = vec.Vec3i; + +var seed: u64 = undefined; + +var arena = main.heap.NeverFailingArenaAllocator.init(main.globalAllocator); +const arenaAllocator = arena.allocator(); + +pub const ParticleManager = struct { + var particleTypesSSBO: SSBO = undefined; + var types: main.List(ParticleType) = undefined; + var textures: main.List(Image) = undefined; + var emissionTextures: main.List(Image) = undefined; + + var textureArray: TextureArray = undefined; + var emissionTextureArray: TextureArray = undefined; + + const ParticleIndex = u16; + var particleTypeHashmap: std.StringHashMapUnmanaged(ParticleIndex) = undefined; + + pub fn init() void { + types = .init(arenaAllocator); + textures = .init(arenaAllocator); + emissionTextures = .init(arenaAllocator); + textureArray = .init(); + emissionTextureArray = .init(); + particleTypesSSBO = SSBO.init(); + ParticleSystem.init(); + } + + pub fn deinit() void { + types.deinit(); + textures.deinit(); + emissionTextures.deinit(); + textureArray.deinit(); + emissionTextureArray.deinit(); + particleTypeHashmap.deinit(arenaAllocator.allocator); + ParticleSystem.deinit(); + particleTypesSSBO.deinit(); + arena.deinit(); + } + + pub fn register(assetsFolder: []const u8, id: []const u8, zon: ZonElement) void { + const textureId = zon.get(?[]const u8, "texture", null) orelse { + std.log.err("Particle texture id was not specified for {s} ({s})", .{id, assetsFolder}); + return; + }; + + const particleType = readTextureDataAndParticleType(assetsFolder, textureId); + + particleTypeHashmap.put(arenaAllocator.allocator, id, @intCast(types.items.len)) catch unreachable; + types.append(particleType); + + std.log.debug("Registered particle type: {s}", .{id}); + } + fn readTextureDataAndParticleType(assetsFolder: []const u8, textureId: []const u8) ParticleType { + var typ: ParticleType = undefined; + + const base = readTexture(assetsFolder, textureId, ".png", Image.defaultImage, .isMandatory); + const emission = readTexture(assetsFolder, textureId, "_emission.png", Image.emptyImage, .isOptional); + const hasEmission = (emission.imageData.ptr != Image.emptyImage.imageData.ptr); + const baseAnimationFrameCount = base.height/base.width; + const emissionAnimationFrameCount = emission.height/emission.width; + + typ.frameCount = @floatFromInt(baseAnimationFrameCount); + typ.startFrame = @floatFromInt(textures.items.len); + typ.size = @as(f32, @floatFromInt(base.width))/16; + + var isBaseBroken = false; + var isEmissionBroken = false; + + if(base.height%base.width != 0) { + std.log.err("Particle base texture has incorrect dimensions ({}x{}) expected height to be multiple of width for {s} ({s})", .{base.width, base.height, textureId, assetsFolder}); + isBaseBroken = true; + } + if(hasEmission and emission.height%emission.width != 0) { + std.log.err("Particle emission texture has incorrect dimensions ({}x{}) expected height to be multiple of width for {s} ({s})", .{base.width, base.height, textureId, assetsFolder}); + isEmissionBroken = true; + } + if(hasEmission and baseAnimationFrameCount != emissionAnimationFrameCount) { + std.log.err("Particle base texture and emission texture frame count mismatch ({} vs {}) for {s} ({s})", .{baseAnimationFrameCount, emissionAnimationFrameCount, textureId, assetsFolder}); + isEmissionBroken = true; + } + + createAnimationFrames(&textures, baseAnimationFrameCount, base, isBaseBroken); + createAnimationFrames(&emissionTextures, baseAnimationFrameCount, emission, isBaseBroken or isEmissionBroken or !hasEmission); + + return typ; + } + + fn readTexture(assetsFolder: []const u8, textureId: []const u8, suffix: []const u8, default: graphics.Image, status: enum {isOptional, isMandatory}) graphics.Image { + var splitter = std.mem.splitScalar(u8, textureId, ':'); + const mod = splitter.first(); + const id = splitter.rest(); + + const gameAssetsPath = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/{s}/particles/textures/{s}{s}", .{mod, id, suffix}) catch unreachable; + defer main.stackAllocator.free(gameAssetsPath); + + const worldAssetsPath = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{s}/particles/textures/{s}{s}", .{assetsFolder, mod, id, suffix}) catch unreachable; + defer main.stackAllocator.free(worldAssetsPath); + + return graphics.Image.readFromFile(arenaAllocator, worldAssetsPath) catch graphics.Image.readFromFile(arenaAllocator, gameAssetsPath) catch { + if(status == .isMandatory) std.log.err("Particle texture not found in {s} and {s}.", .{worldAssetsPath, gameAssetsPath}); + return default; + }; + } + + fn createAnimationFrames(container: *main.List(Image), frameCount: usize, image: Image, isBroken: bool) void { + for(0..frameCount) |i| { + container.append(if(isBroken) image else extractAnimationSlice(image, i)); + } + } + + fn extractAnimationSlice(image: Image, frameIndex: usize) Image { + const frameCount = image.height/image.width; + const frameHeight = image.height/frameCount; + const startHeight = frameHeight*frameIndex; + const endHeight = frameHeight*(frameIndex + 1); + var result = image; + result.height = @intCast(frameHeight); + result.imageData = result.imageData[startHeight*image.width .. endHeight*image.width]; + return result; + } + + pub fn generateTextureArray() void { + textureArray.generate(textures.items, true, true); + emissionTextureArray.generate(emissionTextures.items, true, false); + + particleTypesSSBO.bufferData(ParticleType, ParticleManager.types.items); + particleTypesSSBO.bind(14); + } +}; + +pub const ParticleSystem = struct { + pub const maxCapacity: u32 = 524288; + var particleCount: u32 = 0; + var particles: [maxCapacity]Particle = undefined; + var particlesLocal: [maxCapacity]ParticleLocal = undefined; + var properties: EmitterProperties = undefined; + var previousPlayerPos: Vec3d = undefined; + + var particlesSSBO: SSBO = undefined; + + var pipeline: graphics.Pipeline = undefined; + const UniformStruct = struct { + projectionAndViewMatrix: c_int, + billboardMatrix: c_int, + ambientLight: c_int, + }; + var uniforms: UniformStruct = undefined; + + pub fn init() void { + pipeline = graphics.Pipeline.init( + "assets/cubyz/shaders/particles/particles.vert", + "assets/cubyz/shaders/particles/particles.frag", + "", + &uniforms, + .{}, + .{.depthTest = true, .depthWrite = true}, + .{.attachments = &.{.noBlending}}, + ); + + properties = EmitterProperties{ + .gravity = .{0, 0, -2}, + .drag = 0.2, + .lifeTimeMin = 10, + .lifeTimeMax = 10, + .velMin = 0.1, + .velMax = 0.3, + .rotVelMin = std.math.pi*0.2, + .rotVelMax = std.math.pi*0.6, + .randomizeRotationOnSpawn = true, + }; + particlesSSBO = SSBO.init(); + particlesSSBO.createDynamicBuffer(Particle, maxCapacity); + particlesSSBO.bind(13); + + seed = @bitCast(@as(i64, @truncate(std.time.nanoTimestamp()))); + } + + pub fn deinit() void { + pipeline.deinit(); + particlesSSBO.deinit(); + } + + pub fn update(deltaTime: f32) void { + const vecDeltaTime: Vec4f = @as(Vec4f, @splat(deltaTime)); + const playerPos = game.Player.getEyePosBlocking(); + const prevPlayerPosDifference: Vec3f = @floatCast(previousPlayerPos - playerPos); + + var i: u32 = 0; + while(i < particleCount) { + const particle = &particles[i]; + const particleLocal = &particlesLocal[i]; + particle.lifeRatio -= particleLocal.lifeVelocity*deltaTime; + if(particle.lifeRatio < 0) { + particleCount -= 1; + particles[i] = particles[particleCount]; + particlesLocal[i] = particlesLocal[particleCount]; + continue; + } + + var rot = particle.posAndRotation[3]; + const rotVel = particleLocal.velAndRotationVel[3]; + rot += rotVel*deltaTime; + + particleLocal.velAndRotationVel += vec.combine(properties.gravity, 0)*vecDeltaTime; + particleLocal.velAndRotationVel *= @splat(@exp(-properties.drag*deltaTime)); + const posDelta = particleLocal.velAndRotationVel*vecDeltaTime; + + if(particleLocal.collides) { + const size = ParticleManager.types.items[particle.typ].size; + const hitBox: game.collision.Box = .{.min = @splat(size*-0.5), .max = @splat(size*0.5)}; + var v3Pos = playerPos + @as(Vec3d, @floatCast(Vec3f{particle.posAndRotation[0], particle.posAndRotation[1], particle.posAndRotation[2]} + prevPlayerPosDifference)); + v3Pos[0] += posDelta[0]; + if(game.collision.collides(.client, .x, -posDelta[0], v3Pos, hitBox)) |box| { + v3Pos[0] = if(posDelta[0] < 0) + box.max[0] - hitBox.min[0] + else + box.min[0] - hitBox.max[0]; + } + v3Pos[1] += posDelta[1]; + if(game.collision.collides(.client, .y, -posDelta[1], v3Pos, hitBox)) |box| { + v3Pos[1] = if(posDelta[1] < 0) + box.max[1] - hitBox.min[1] + else + box.min[1] - hitBox.max[1]; + } + v3Pos[2] += posDelta[2]; + if(game.collision.collides(.client, .z, -posDelta[2], v3Pos, hitBox)) |box| { + v3Pos[2] = if(posDelta[2] < 0) + box.max[2] - hitBox.min[2] + else + box.min[2] - hitBox.max[2]; + } + particle.posAndRotation = vec.combine(@as(Vec3f, @floatCast(v3Pos - playerPos)), 0); + } else { + particle.posAndRotation += posDelta + vec.combine(prevPlayerPosDifference, 0); + } + + particle.posAndRotation[3] = rot; + particleLocal.velAndRotationVel[3] = rotVel; + + const positionf64 = @as(Vec4d, @floatCast(particle.posAndRotation)) + Vec4d{playerPos[0], playerPos[1], playerPos[2], 0}; + const intPos: vec.Vec4i = @intFromFloat(@floor(positionf64)); + const light: [6]u8 = main.renderer.mesh_storage.getLight(intPos[0], intPos[1], intPos[2]) orelse @splat(0); + const compressedLight = + @as(u32, light[0] >> 3) << 25 | + @as(u32, light[1] >> 3) << 20 | + @as(u32, light[2] >> 3) << 15 | + @as(u32, light[3] >> 3) << 10 | + @as(u32, light[4] >> 3) << 5 | + @as(u32, light[5] >> 3); + particle.light = compressedLight; + + i += 1; + } + previousPlayerPos = playerPos; + } + + fn addParticle(typ: u32, pos: Vec3d, vel: Vec3f, collides: bool) void { + const lifeTime = properties.lifeTimeMin + random.nextFloat(&seed)*properties.lifeTimeMax; + const rot = if(properties.randomizeRotationOnSpawn) random.nextFloat(&seed)*std.math.pi*2 else 0; + + particles[particleCount] = Particle{ + .posAndRotation = vec.combine(@as(Vec3f, @floatCast(pos - previousPlayerPos)), rot), + .typ = typ, + }; + particlesLocal[particleCount] = ParticleLocal{ + .velAndRotationVel = vec.combine(vel, properties.rotVelMin + random.nextFloatSigned(&seed)*properties.rotVelMax), + .lifeVelocity = 1/lifeTime, + .collides = collides, + }; + particleCount += 1; + } + + pub fn render(projectionMatrix: Mat4f, viewMatrix: Mat4f, ambientLight: Vec3f) void { + particlesSSBO.bufferSubData(Particle, &particles, particleCount); + + pipeline.bind(null); + + const projectionAndViewMatrix = Mat4f.mul(projectionMatrix, viewMatrix); + c.glUniformMatrix4fv(uniforms.projectionAndViewMatrix, 1, c.GL_TRUE, @ptrCast(&projectionAndViewMatrix)); + c.glUniform3fv(uniforms.ambientLight, 1, @ptrCast(&ambientLight)); + + const billboardMatrix = Mat4f.rotationZ(-game.camera.rotation[2] + std.math.pi*0.5) + .mul(Mat4f.rotationY(game.camera.rotation[0] - std.math.pi*0.5)); + c.glUniformMatrix4fv(uniforms.billboardMatrix, 1, c.GL_TRUE, @ptrCast(&billboardMatrix)); + + c.glActiveTexture(c.GL_TEXTURE0); + ParticleManager.textureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + ParticleManager.emissionTextureArray.bind(); + + c.glBindVertexArray(chunk_meshing.vao); + + for(0..std.math.divCeil(u32, particleCount, chunk_meshing.maxQuadsInIndexBuffer) catch unreachable) |_| { + c.glDrawElements(c.GL_TRIANGLES, @intCast(particleCount*6), c.GL_UNSIGNED_INT, null); + } + } + + pub fn getParticleCount() u32 { + return particleCount; + } +}; + +pub const EmitterProperties = struct { + gravity: Vec3f = @splat(0), + drag: f32 = 0, + velMin: f32 = 0, + velMax: f32 = 0, + rotVelMin: f32 = 0, + rotVelMax: f32 = 0, + lifeTimeMin: f32 = 0, + lifeTimeMax: f32 = 0, + randomizeRotationOnSpawn: bool = false, +}; + +pub const DirectionMode = union(enum(u8)) { + // The particle goes in the direction away from the center + spread: void, + // The particle goes in a random direction + scatter: void, + // The particle goes in the specified direction + direction: Vec3f, +}; + +pub const Emitter = struct { + typ: u16 = 0, + collides: bool, + + pub const SpawnPoint = struct { + mode: DirectionMode, + position: Vec3d, + + pub fn spawn(self: SpawnPoint) struct {Vec3d, Vec3f} { + const particlePos = self.position; + const speed: Vec3f = @splat(ParticleSystem.properties.velMin + random.nextFloat(&seed)*ParticleSystem.properties.velMax); + const dir: Vec3f = switch(self.mode) { + .direction => |dir| dir, + .scatter, .spread => vec.normalize(random.nextFloatVectorSigned(3, &seed)), + }; + const particleVel = dir*speed; + + return .{particlePos, particleVel}; + } + }; + + pub const SpawnSphere = struct { + radius: f32, + mode: DirectionMode, + position: Vec3d, + + pub fn spawn(self: SpawnSphere) struct {Vec3d, Vec3f} { + const spawnPos: Vec3f = @splat(self.radius); + var offsetPos: Vec3f = undefined; + while(true) { + offsetPos = random.nextFloatVectorSigned(3, &seed); + if(vec.lengthSquare(offsetPos) <= 1) break; + } + const particlePos = self.position + @as(Vec3d, @floatCast(offsetPos*spawnPos)); + const speed: Vec3f = @splat(ParticleSystem.properties.velMin + random.nextFloat(&seed)*ParticleSystem.properties.velMax); + const dir: Vec3f = switch(self.mode) { + .direction => |dir| dir, + .scatter => vec.normalize(random.nextFloatVectorSigned(3, &seed)), + .spread => @floatCast(offsetPos), + }; + const particleVel = dir*speed; + + return .{particlePos, particleVel}; + } + }; + + pub const SpawnCube = struct { + size: Vec3f, + mode: DirectionMode, + position: Vec3d, + + pub fn spawn(self: SpawnCube) struct {Vec3d, Vec3f} { + const spawnPos: Vec3f = self.size; + const offsetPos: Vec3f = random.nextFloatVectorSigned(3, &seed); + const particlePos = self.position + @as(Vec3d, @floatCast(offsetPos*spawnPos)); + const speed: Vec3f = @splat(ParticleSystem.properties.velMin + random.nextFloat(&seed)*ParticleSystem.properties.velMax); + const dir: Vec3f = switch(self.mode) { + .direction => |dir| dir, + .scatter => vec.normalize(random.nextFloatVectorSigned(3, &seed)), + .spread => vec.normalize(@as(Vec3f, @floatCast(offsetPos))), + }; + const particleVel = dir*speed; + + return .{particlePos, particleVel}; + } + }; + + pub fn init(id: []const u8, collides: bool) Emitter { + const emitter = Emitter{ + .typ = ParticleManager.particleTypeHashmap.get(id) orelse 0, + .collides = collides, + }; + + return emitter; + } + + pub fn spawnParticles(self: Emitter, spawnCount: u32, comptime T: type, spawnRules: T) void { + const count = @min(spawnCount, ParticleSystem.maxCapacity - ParticleSystem.particleCount); + for(0..count) |_| { + const particlePos, const particleVel = spawnRules.spawn(); + + ParticleSystem.addParticle(self.typ, particlePos, particleVel, self.collides); + } + } +}; + +pub const ParticleType = struct { + frameCount: f32, + startFrame: f32, + size: f32, +}; + +pub const Particle = struct { + posAndRotation: Vec4f, + lifeRatio: f32 = 1, + light: u32 = 0, + typ: u32, + // 4 bytes left for use +}; + +pub const ParticleLocal = struct { + velAndRotationVel: Vec4f, + lifeVelocity: f32, + collides: bool, +}; diff --git a/src/renderer.zig b/src/renderer.zig index 18f4430e..38e427db 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -5,6 +5,7 @@ const blocks = @import("blocks.zig"); const chunk = @import("chunk.zig"); const entity = @import("entity.zig"); const graphics = @import("graphics.zig"); +const particles = @import("particles.zig"); const c = graphics.c; const game = @import("game.zig"); const World = game.World; @@ -240,6 +241,16 @@ pub fn renderWorld(world: *World, ambientLight: Vec3f, skyColor: Vec3f, playerPo itemdrop.ItemDropRenderer.renderItemDrops(game.projectionMatrix, ambientLight, playerPos); gpu_performance_measuring.stopQuery(); + gpu_performance_measuring.startQuery(.particle_rendering); + particles.ParticleSystem.render(game.projectionMatrix, game.camera.viewMatrix, ambientLight); + gpu_performance_measuring.stopQuery(); + + // Rebind block textures back to their original slots + c.glActiveTexture(c.GL_TEXTURE0); + blocks.meshes.blockTextureArray.bind(); + c.glActiveTexture(c.GL_TEXTURE1); + blocks.meshes.emissionTextureArray.bind(); + MeshSelection.render(game.projectionMatrix, game.camera.viewMatrix, playerPos); // Render transparent chunk meshes: diff --git a/src/renderer/chunk_meshing.zig b/src/renderer/chunk_meshing.zig index 8fecfd0d..935bca7e 100644 --- a/src/renderer/chunk_meshing.zig +++ b/src/renderer/chunk_meshing.zig @@ -71,6 +71,7 @@ pub var commandBuffer: graphics.LargeBuffer(IndirectData) = undefined; pub var chunkIDBuffer: graphics.LargeBuffer(u32) = undefined; pub var quadsDrawn: usize = 0; pub var transparentQuadsDrawn: usize = 0; +pub const maxQuadsInIndexBuffer = 3 << (3*chunk.chunkShift); // maximum 3 faces/block pub fn init() void { lighting.init(); @@ -119,7 +120,7 @@ pub fn init() void { }}}, ); - var rawData: [6*3 << (3*chunk.chunkShift)]u32 = undefined; // 6 vertices per face, maximum 3 faces/block + var rawData: [6*maxQuadsInIndexBuffer]u32 = undefined; const lut = [_]u32{0, 2, 1, 1, 2, 3}; for(0..rawData.len) |i| { rawData[i] = @as(u32, @intCast(i))/6*4 + lut[i%6];