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 migrations_zig = @import("migrations.zig");
const blueprints_zig = @import("blueprint.zig"); const blueprints_zig = @import("blueprint.zig");
const Blueprint = blueprints_zig.Blueprint; const Blueprint = blueprints_zig.Blueprint;
const particles_zig = @import("particles.zig");
const ZonElement = @import("zon.zig").ZonElement; const ZonElement = @import("zon.zig").ZonElement;
const main = @import("main"); const main = @import("main");
const biomes_zig = main.server.terrain.biomes; const biomes_zig = main.server.terrain.biomes;
@ -33,6 +34,7 @@ pub const Assets = struct {
models: BytesHashMap, models: BytesHashMap,
structureBuildingBlocks: ZonHashMap, structureBuildingBlocks: ZonHashMap,
blueprints: BytesHashMap, blueprints: BytesHashMap,
particles: ZonHashMap,
fn init() Assets { fn init() Assets {
return .{ return .{
@ -46,6 +48,7 @@ pub const Assets = struct {
.models = .{}, .models = .{},
.structureBuildingBlocks = .{}, .structureBuildingBlocks = .{},
.blueprints = .{}, .blueprints = .{},
.particles = .{},
}; };
} }
fn deinit(self: *Assets, allocator: NeverFailingAllocator) void { fn deinit(self: *Assets, allocator: NeverFailingAllocator) void {
@ -59,6 +62,7 @@ pub const Assets = struct {
self.models.deinit(allocator.allocator); self.models.deinit(allocator.allocator);
self.structureBuildingBlocks.deinit(allocator.allocator); self.structureBuildingBlocks.deinit(allocator.allocator);
self.blueprints.deinit(allocator.allocator); self.blueprints.deinit(allocator.allocator);
self.particles.deinit(allocator.allocator);
} }
fn clone(self: Assets, allocator: NeverFailingAllocator) Assets { fn clone(self: Assets, allocator: NeverFailingAllocator) Assets {
return .{ return .{
@ -72,6 +76,7 @@ pub const Assets = struct {
.models = self.models.clone(allocator.allocator) catch unreachable, .models = self.models.clone(allocator.allocator) catch unreachable,
.structureBuildingBlocks = self.structureBuildingBlocks.clone(allocator.allocator) catch unreachable, .structureBuildingBlocks = self.structureBuildingBlocks.clone(allocator.allocator) catch unreachable,
.blueprints = self.blueprints.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 { 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.readAllZon(allocator, "sbb", true, &self.structureBuildingBlocks, null);
addon.readAllBlueprints(allocator, "sbb", &self.blueprints); addon.readAllBlueprints(allocator, "sbb", &self.blueprints);
addon.readAllModels(allocator, &self.models); addon.readAllModels(allocator, &self.models);
addon.readAllZon(allocator, "particles", true, &self.particles, null);
} }
} }
fn log(self: *Assets, typ: enum {common, world}) void { fn log(self: *Assets, typ: enum {common, world}) void {
std.log.info( std.log.info(
"Finished {s} assets reading with {} blocks ({} migrations), {} items, {} tools, {} biomes ({} migrations), {} recipes, {} structure building blocks and {} blueprints", "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()}, .{@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.registerBlueprints(&worldAssets.blueprints);
try sbb.registerSBB(&worldAssets.structureBuildingBlocks); 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: // Biomes:
var nextBiomeNumericId: u32 = 0; var nextBiomeNumericId: u32 = 0;
for(biomePalette.palette.items) |id| { for(biomePalette.palette.items) |id| {

View File

@ -11,6 +11,7 @@ const ZonElement = @import("zon.zig").ZonElement;
const main = @import("main"); const main = @import("main");
const KeyBoard = main.KeyBoard; const KeyBoard = main.KeyBoard;
const network = @import("network.zig"); const network = @import("network.zig");
const particles = @import("particles.zig");
const Connection = network.Connection; const Connection = network.Connection;
const ConnectionManager = network.ConnectionManager; const ConnectionManager = network.ConnectionManager;
const vec = @import("vec.zig"); const vec = @import("vec.zig");
@ -674,6 +675,7 @@ pub const World = struct { // MARK: World
main.Window.setMouseGrabbed(true); main.Window.setMouseGrabbed(true);
main.blocks.meshes.generateTextureArray(); main.blocks.meshes.generateTextureArray();
main.particles.ParticleManager.generateTextureArray();
main.models.uploadModels(); main.models.uploadModels();
} }
@ -1219,4 +1221,5 @@ pub fn update(deltaTime: f64) void { // MARK: update()
fog.fogHigher = (biome.fogHigher - fog.fogHigher)*t + fog.fogHigher; fog.fogHigher = (biome.fogHigher - fog.fogHigher)*t + fog.fogHigher;
world.?.update(); 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); 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.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); c.glBindBuffer(c.GL_SHADER_STORAGE_BUFFER, 0);
} }
}; };
@ -2159,8 +2165,8 @@ pub const TextureArray = struct { // MARK: TextureArray
/// (Re-)Generates the GPU buffer. /// (Re-)Generates the GPU buffer.
pub fn generate(self: TextureArray, images: []Image, mipmapping: bool, alphaCorrectMipmapping: bool) void { pub fn generate(self: TextureArray, images: []Image, mipmapping: bool, alphaCorrectMipmapping: bool) void {
var maxWidth: u31 = 0; var maxWidth: u31 = 1;
var maxHeight: u31 = 0; var maxHeight: u31 = 1;
for(images) |image| { for(images) |image| {
maxWidth = @max(maxWidth, image.width); maxWidth = @max(maxWidth, image.width);
maxHeight = @max(maxHeight, image.height); 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); draw.print("Opaque faces: {}, Transparent faces: {}", .{main.renderer.chunk_meshing.quadsDrawn, main.renderer.chunk_meshing.transparentQuadsDrawn}, 0, y, 8, .left);
y += 8; 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_occlusion_test,
chunk_rendering_new_visible, chunk_rendering_new_visible,
entity_rendering, entity_rendering,
particle_rendering,
transparent_rendering_preparation, transparent_rendering_preparation,
transparent_rendering_occlusion_test, transparent_rendering_occlusion_test,
transparent_rendering, transparent_rendering,
@ -41,6 +42,7 @@ const names = [_][]const u8{
"Chunk Rendering Occlusion Test", "Chunk Rendering Occlusion Test",
"Chunk Rendering New Visible", "Chunk Rendering New Visible",
"Entity Rendering", "Entity Rendering",
"Particle Rendering",
"Transparent Rendering Preparation", "Transparent Rendering Preparation",
"Transparent Rendering Occlusion Test", "Transparent Rendering Occlusion Test",
"Transparent Rendering", "Transparent Rendering",

View File

@ -23,6 +23,7 @@ pub const random = @import("random.zig");
pub const renderer = @import("renderer.zig"); pub const renderer = @import("renderer.zig");
pub const rotation = @import("rotation.zig"); pub const rotation = @import("rotation.zig");
pub const settings = @import("settings.zig"); pub const settings = @import("settings.zig");
pub const particles = @import("particles.zig");
const tag = @import("tag.zig"); const tag = @import("tag.zig");
pub const Tag = tag.Tag; pub const Tag = tag.Tag;
pub const utils = @import("utils.zig"); pub const utils = @import("utils.zig");
@ -644,6 +645,9 @@ pub fn main() void { // MARK: main()
gui.init(); gui.init();
defer gui.deinit(); defer gui.deinit();
particles.ParticleManager.init();
defer particles.ParticleManager.deinit();
if(settings.playerName.len == 0) { if(settings.playerName.len == 0) {
gui.openWindow("change_name"); gui.openWindow("change_name");
} else { } 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 chunk = @import("chunk.zig");
const entity = @import("entity.zig"); const entity = @import("entity.zig");
const graphics = @import("graphics.zig"); const graphics = @import("graphics.zig");
const particles = @import("particles.zig");
const c = graphics.c; const c = graphics.c;
const game = @import("game.zig"); const game = @import("game.zig");
const World = game.World; 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); itemdrop.ItemDropRenderer.renderItemDrops(game.projectionMatrix, ambientLight, playerPos);
gpu_performance_measuring.stopQuery(); 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); MeshSelection.render(game.projectionMatrix, game.camera.viewMatrix, playerPos);
// Render transparent chunk meshes: // 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 chunkIDBuffer: graphics.LargeBuffer(u32) = undefined;
pub var quadsDrawn: usize = 0; pub var quadsDrawn: usize = 0;
pub var transparentQuadsDrawn: usize = 0; pub var transparentQuadsDrawn: usize = 0;
pub const maxQuadsInIndexBuffer = 3 << (3*chunk.chunkShift); // maximum 3 faces/block
pub fn init() void { pub fn init() void {
lighting.init(); 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}; const lut = [_]u32{0, 2, 1, 1, 2, 3};
for(0..rawData.len) |i| { for(0..rawData.len) |i| {
rawData[i] = @as(u32, @intCast(i))/6*4 + lut[i%6]; rawData[i] = @as(u32, @intCast(i))/6*4 + lut[i%6];