https://github.com/user-attachments/assets/de82e298-acfb-431d-973a-2d37b9a707ae

resolves #293

---------

Co-authored-by: Krzysztof Wiśniewski <argmaster.world@gmail.com>
Co-authored-by: IntegratedQuantum <43880493+IntegratedQuantum@users.noreply.github.com>
Co-authored-by: Krzysztof Wiśniewski <Argmaster@users.noreply.github.com>
This commit is contained in:
MnHs 2025-05-25 20:53:05 +02:00 committed by GitHub
parent 14c7fafce9
commit de304e49d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 599 additions and 7 deletions

View File

@ -0,0 +1,3 @@
.{
.texture = "cubyz:poof",
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

View File

@ -0,0 +1,17 @@
#version 430
layout(location = 0) out vec4 fragColor;
layout(location = 0) in vec3 textureCoords;
layout(location = 1) flat in vec3 light;
layout(binding = 0) uniform sampler2DArray textureSampler;
layout(binding = 1) uniform sampler2DArray emissionTextureSampler;
void main() {
const vec4 texColor = texture(textureSampler, textureCoords);
if(texColor.a != 1) discard;
const vec3 pixelLight = max(light, texture(emissionTextureSampler, textureCoords).r*4);
fragColor = texColor*vec4(pixelLight, 1);
}

View File

@ -0,0 +1,82 @@
#version 460
layout(location = 0) out vec3 textureCoords;
layout(location = 1) flat out vec3 light;
layout(location = 0) uniform vec3 ambientLight;
layout(location = 1) uniform mat4 projectionAndViewMatrix;
layout(location = 2) uniform mat4 billboardMatrix;
struct ParticleData {
vec3 pos;
float rotation;
float lifeRatio;
uint light;
uint type;
};
layout(std430, binding = 13) restrict readonly buffer _particleData
{
ParticleData particleData[];
};
struct ParticleTypeData {
float animationFrames;
float startFrame;
float size;
};
layout(std430, binding = 14) restrict readonly buffer _particleTypeData
{
ParticleTypeData particleTypeData[];
};
const vec2 uvPositions[4] = vec2[4]
(
vec2(0.0f, 0.0f),
vec2(0.0f, 1.0f),
vec2(1.0f, 0.0f),
vec2(1.0f, 1.0f)
);
const vec3 facePositions[4] = vec3[4]
(
vec3(-0.5f, -0.5f, 0.0f),
vec3(-0.5f, 0.5f, 0.0f),
vec3(0.5f, -0.5f, 0.0f),
vec3(0.5f, 0.5f, 0.0f)
);
void main() {
int particleID = gl_VertexID >> 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);
}

View File

@ -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| {

View File

@ -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));
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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",

View File

@ -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 {

450
src/particles.zig Normal file
View File

@ -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,
};

View File

@ -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:

View File

@ -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];