mirror of
https://github.com/PixelGuys/Cubyz.git
synced 2025-08-03 11:17:05 -04:00
Particles (#1367)
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:
parent
14c7fafce9
commit
de304e49d3
3
assets/cubyz/particles/poof.zig.zon
Normal file
3
assets/cubyz/particles/poof.zig.zon
Normal file
@ -0,0 +1,3 @@
|
||||
.{
|
||||
.texture = "cubyz:poof",
|
||||
}
|
BIN
assets/cubyz/particles/textures/poof.png
Normal file
BIN
assets/cubyz/particles/textures/poof.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 132 B |
17
assets/cubyz/shaders/particles/particles.frag
Normal file
17
assets/cubyz/shaders/particles/particles.frag
Normal 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);
|
||||
}
|
82
assets/cubyz/shaders/particles/particles.vert
Normal file
82
assets/cubyz/shaders/particles/particles.vert
Normal 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);
|
||||
}
|
@ -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| {
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
450
src/particles.zig
Normal 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,
|
||||
};
|
@ -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:
|
||||
|
@ -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];
|
||||
|
Loading…
x
Reference in New Issue
Block a user