Add a block breaking animation

fixes #620
This commit is contained in:
IntegratedQuantum 2025-01-22 21:32:10 +01:00
parent b1dc5cacf0
commit faaae6ff4a
10 changed files with 165 additions and 49 deletions

View File

@ -49,12 +49,11 @@ struct ChunkData {
vec4 minPos;
vec4 maxPos;
int voxelSize;
uint lightStart;
uint vertexStartOpaque;
uint faceCountsByNormalOpaque[14];
uint lightStartOpaque;
uint vertexStartTransparent;
uint vertexCountTransparent;
uint lightStartTransparent;
uint visibilityState;
uint oldVisibilityState;
};
@ -72,11 +71,7 @@ void main() {
vec3 modelPosition = vec3(chunks[chunkID].position.xyz - playerPositionInteger) - playerPositionFraction;
int encodedPositionAndLightIndex = faceData[faceID].encodedPositionAndLightIndex;
int textureAndQuad = faceData[faceID].textureAndQuad;
#ifdef transparent
uint lightIndex = chunks[chunkID].lightStartTransparent + 4*(encodedPositionAndLightIndex >> 16);
#else
uint lightIndex = chunks[chunkID].lightStartOpaque + 4*(encodedPositionAndLightIndex >> 16);
#endif
uint lightIndex = chunks[chunkID].lightStart + 4*(encodedPositionAndLightIndex >> 16);
uint fullLight = lightData[lightIndex + vertexID];
vec3 sunLight = vec3(
fullLight >> 25 & 31u,

View File

@ -12,12 +12,11 @@ struct ChunkData {
vec4 minPos;
vec4 maxPos;
int voxelSize;
uint lightStart;
uint vertexStartOpaque;
uint faceCountsByNormalOpaque[14];
uint lightStartOpaque;
uint vertexStartTransparent;
uint vertexCountTransparent;
uint lightStartTransparent;
uint visibilityState;
uint oldVisibilityState;
};

View File

@ -9,12 +9,11 @@ struct ChunkData {
vec4 minPos;
vec4 maxPos;
int voxelSize;
uint lightStart;
uint vertexStartOpaque;
uint faceCountsByNormalOpaque[14];
uint lightStartOpaque;
uint vertexStartTransparent;
uint vertexCountTransparent;
uint lightStartTransparent;
uint visibilityState;
uint oldVisibilityState;
};

View File

@ -7,12 +7,11 @@ struct ChunkData {
vec4 minPos;
vec4 maxPos;
int voxelSize;
uint lightStart;
uint vertexStartOpaque;
uint faceCountsByNormalOpaque[14];
uint lightStartOpaque;
uint vertexStartTransparent;
uint vertexCountTransparent;
uint lightStartTransparent;
uint visibilityState;
uint oldVisibilityState;
};

View File

@ -604,9 +604,9 @@ pub const meshes = struct { // MARK: meshes
pub fn registerBlockBreakingAnimation(assetFolder: []const u8) void {
var i: usize = 0;
while(true) : (i += 1) {
const path1 = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/cubyz/blocks/textures/{}.png", .{i}) catch unreachable;
const path1 = std.fmt.allocPrint(main.stackAllocator.allocator, "assets/cubyz/blocks/textures/breaking/{}.png", .{i}) catch unreachable;
defer main.stackAllocator.free(path1);
const path2 = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/cubyz/blocks/textures/{}.png", .{assetFolder, i}) catch unreachable;
const path2 = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/cubyz/blocks/textures/breaking/{}.png", .{assetFolder, i}) catch unreachable;
defer main.stackAllocator.free(path2);
if(!main.files.hasFile(path1) and !main.files.hasFile(path2)) break;

View File

@ -43,6 +43,10 @@ pub const Neighbor = enum(u3) { // MARK: Neighbor
const arr = [_]i32 {1, -1, 0, 0, 0, 0};
return arr[@intFromEnum(self)];
}
/// Index to relative position
pub fn relPos(self: Neighbor) Vec3i {
return .{self.relX(), self.relY(), self.relZ()};
}
/// Index to bitMask for bitmap direction data
pub inline fn bitMask(self: Neighbor) u6 {
return @as(u6, 1) << @intFromEnum(self);

View File

@ -2071,12 +2071,11 @@ pub fn generateBlockTexture(blockType: u16) Texture {
.min = undefined,
.max = undefined,
.voxelSize = 1,
.lightStart = lightAllocation.start,
.vertexStartOpaque = undefined,
.faceCountsByNormalOpaque = undefined,
.lightStartOpaque = lightAllocation.start,
.vertexStartTransparent = undefined,
.vertexCountTransparent = undefined,
.lightStartTransparent = lightAllocation.start,
.visibilityState = 0,
.oldVisibilityState = 0,
}}, &chunkAllocation);

View File

@ -809,6 +809,7 @@ pub const MeshSelection = struct { // MARK: MeshSelection
pub fn breakBlock(inventory: main.items.Inventory, slot: u32, deltaTime: f64) void {
if(selectedBlockPos) |selectedPos| {
if(@reduce(.Or, lastSelectedBlockPos != selectedPos)) {
mesh_storage.removeBreakingAnimation(lastSelectedBlockPos);
lastSelectedBlockPos = selectedPos;
currentBlockProgress = 0;
}
@ -830,9 +831,13 @@ pub const MeshSelection = struct { // MARK: MeshSelection
}
currentBlockProgress += @as(f32, @floatCast(deltaTime))/breakTime;
if(currentBlockProgress < 1) {
mesh_storage.removeBreakingAnimation(lastSelectedBlockPos);
mesh_storage.addBreakingAnimation(lastSelectedBlockPos, currentBlockProgress);
main.items.Inventory.Sync.ClientSide.mutex.unlock();
return;
} else {
mesh_storage.removeBreakingAnimation(lastSelectedBlockPos);
currentBlockProgress = 0;
}
} else {

View File

@ -264,12 +264,11 @@ pub const ChunkData = extern struct {
min: Vec3f align(16),
max: Vec3f align(16),
voxelSize: i32,
lightStart: u32,
vertexStartOpaque: u32,
faceCountsByNormalOpaque: [14]u32,
lightStartOpaque: u32,
vertexStartTransparent: u32,
vertexCountTransparent: u32,
lightStartTransparent: u32,
visibilityState: u32,
oldVisibilityState: u32,
};
@ -288,15 +287,12 @@ const PrimitiveMesh = struct { // MARK: PrimitiveMesh
neighborFacesHigherLod: [6]main.ListUnmanaged(FaceData) = [_]main.ListUnmanaged(FaceData){.{}} ** 6,
optionalFaces: main.ListUnmanaged(FaceData) = .{},
completeList: []FaceData = &.{},
lightList: []u32 = &.{},
lightListNeedsUpload: bool = false,
coreLen: u32 = 0,
sameLodLens: [6]u32 = .{0} ** 6,
higherLodLens: [6]u32 = .{0} ** 6,
optionalLen: u32 = 0,
mutex: std.Thread.Mutex = .{},
bufferAllocation: graphics.SubAllocation = .{.start = 0, .len = 0},
lightAllocation: graphics.SubAllocation = .{.start = 0, .len = 0},
vertexCount: u31 = 0,
byNormalCount: [14]u32 = .{0} ** 14,
wasChanged: bool = false,
@ -305,7 +301,6 @@ const PrimitiveMesh = struct { // MARK: PrimitiveMesh
fn deinit(self: *PrimitiveMesh) void {
faceBuffer.free(self.bufferAllocation);
lightBuffer.free(self.lightAllocation);
self.coreFaces.deinit(main.globalAllocator);
self.optionalFaces.deinit(main.globalAllocator);
for(&self.neighborFacesSameLod) |*neighborFaces| {
@ -315,7 +310,6 @@ const PrimitiveMesh = struct { // MARK: PrimitiveMesh
neighborFaces.deinit(main.globalAllocator);
}
main.globalAllocator.free(self.completeList);
main.globalAllocator.free(self.lightList);
}
fn reset(self: *PrimitiveMesh) void {
@ -356,7 +350,7 @@ const PrimitiveMesh = struct { // MARK: PrimitiveMesh
}
}
fn finish(self: *PrimitiveMesh, parent: *ChunkMesh) void {
fn finish(self: *PrimitiveMesh, parent: *ChunkMesh, lightList: *main.List(u32), lightMap: *std.AutoHashMap([4]u32, u16)) void {
var len: usize = self.coreFaces.items.len;
for(self.neighborFacesSameLod) |neighborFaces| {
len += neighborFaces.items.len;
@ -379,10 +373,6 @@ const PrimitiveMesh = struct { // MARK: PrimitiveMesh
}
@memcpy(completeList[i..][0..self.optionalFaces.items.len], self.optionalFaces.items);
i += self.optionalFaces.items.len;
var lightList = main.List(u32).init(main.stackAllocator);
defer lightList.deinit();
var lightMap = std.AutoHashMap([4]u32, u16).init(main.stackAllocator.allocator);
defer lightMap.deinit();
self.min = @splat(std.math.floatMax(f32));
self.max = @splat(-std.math.floatMax(f32));
@ -411,13 +401,7 @@ const PrimitiveMesh = struct { // MARK: PrimitiveMesh
parent.lightingData[0].lock.unlockRead();
parent.lightingData[1].lock.unlockRead();
const completeLightList = main.globalAllocator.alloc(u32, lightList.items.len);
@memcpy(completeLightList, lightList.items);
self.mutex.lock();
const oldLightList = self.lightList;
self.lightList = completeLightList;
self.lightListNeedsUpload = true;
const oldList = self.completeList;
self.completeList = completeList;
self.coreLen = @intCast(self.coreFaces.items.len);
@ -430,7 +414,6 @@ const PrimitiveMesh = struct { // MARK: PrimitiveMesh
self.optionalLen = @intCast(self.optionalFaces.items.len);
self.mutex.unlock();
main.globalAllocator.free(oldList);
main.globalAllocator.free(oldLightList);
}
fn getValues(mesh: *ChunkMesh, wx: i32, wy: i32, wz: i32) [6]u8 {
@ -657,10 +640,6 @@ const PrimitiveMesh = struct { // MARK: PrimitiveMesh
std.debug.assert(i == fullBuffer.len);
self.vertexCount = @intCast(6*fullBuffer.len);
self.wasChanged = true;
if(self.lightListNeedsUpload) {
self.lightListNeedsUpload = false;
lightBuffer.uploadData(self.lightList, &self.lightAllocation);
}
}
};
@ -694,6 +673,10 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
lightingData: [2]*lighting.ChannelChunk,
opaqueMesh: PrimitiveMesh,
transparentMesh: PrimitiveMesh,
lightList: []u32 = &.{},
lightListNeedsUpload: bool = false,
lightAllocation: graphics.SubAllocation = .{.start = 0, .len = 0},
lastNeighborsSameLod: [6]?*const ChunkMesh = [_]?*const ChunkMesh{null} ** 6,
lastNeighborsHigherLod: [6]?*const ChunkMesh = [_]?*const ChunkMesh{null} ** 6,
isNeighborLod: [6]bool = .{false} ** 6,
@ -712,6 +695,10 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
min: Vec3f = undefined,
max: Vec3f = undefined,
blockBreakingFaces: main.List(FaceData),
blockBreakingFacesSortingData: []SortingData = &.{},
blockBreakingFacesChanged: bool = false,
pub fn init(self: *ChunkMesh, pos: chunk.ChunkPosition, ch: *chunk.Chunk) void {
self.* = ChunkMesh{
.pos = pos,
@ -723,6 +710,7 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
lighting.ChannelChunk.init(ch, false),
lighting.ChannelChunk.init(ch, true),
},
.blockBreakingFaces = .init(main.globalAllocator),
};
}
@ -737,6 +725,10 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
for(self.lightingData) |lightingChunk| {
lightingChunk.deinit();
}
self.blockBreakingFaces.deinit();
main.globalAllocator.free(self.blockBreakingFacesSortingData);
main.globalAllocator.free(self.lightList);
lightBuffer.free(self.lightAllocation);
}
pub fn increaseRefCount(self: *ChunkMesh) void {
@ -1332,8 +1324,19 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
pub fn finishData(self: *ChunkMesh) void {
main.utils.assertLocked(&self.mutex);
self.opaqueMesh.finish(self);
self.transparentMesh.finish(self);
var lightList = main.List(u32).init(main.stackAllocator);
defer lightList.deinit();
var lightMap = std.AutoHashMap([4]u32, u16).init(main.stackAllocator.allocator);
defer lightMap.deinit();
self.opaqueMesh.finish(self, &lightList, &lightMap);
self.transparentMesh.finish(self, &lightList, &lightMap);
self.lightList = main.globalAllocator.realloc(self.lightList, lightList.items.len);
@memcpy(self.lightList, lightList.items);
self.lightListNeedsUpload = true;
self.min = @min(self.opaqueMesh.min, self.transparentMesh.min);
self.max = @max(self.opaqueMesh.max, self.transparentMesh.max);
}
@ -1341,6 +1344,12 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
pub fn uploadData(self: *ChunkMesh) void {
self.opaqueMesh.uploadData(self.isNeighborLod);
self.transparentMesh.uploadData(self.isNeighborLod);
if(self.lightListNeedsUpload) {
self.lightListNeedsUpload = false;
lightBuffer.uploadData(self.lightList, &self.lightAllocation);
}
self.uploadChunkPosition();
}
@ -1504,12 +1513,11 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
chunkBuffer.uploadData(&.{ChunkData{
.position = .{self.pos.wx, self.pos.wy, self.pos.wz},
.voxelSize = self.pos.voxelSize,
.lightStart = self.lightAllocation.start,
.vertexStartOpaque = self.opaqueMesh.bufferAllocation.start*4,
.faceCountsByNormalOpaque = self.opaqueMesh.byNormalCount,
.lightStartOpaque = self.opaqueMesh.lightAllocation.start,
.vertexStartTransparent = self.transparentMesh.bufferAllocation.start*4,
.vertexCountTransparent = self.transparentMesh.bufferAllocation.len*6,
.lightStartTransparent = self.transparentMesh.lightAllocation.start,
.min = self.min,
.max = self.max,
.visibilityState = 0,
@ -1526,7 +1534,7 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
}
pub fn prepareTransparentRendering(self: *ChunkMesh, playerPosition: Vec3d, chunkList: *main.List(u32)) void {
if(self.transparentMesh.vertexCount == 0) return;
if(self.transparentMesh.vertexCount == 0 and self.blockBreakingFaces.items.len == 0) return;
var needsUpdate: bool = false;
if(self.transparentMesh.wasChanged) {
@ -1552,8 +1560,8 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
}
offset += neighborLen;
}
self.sortingOutputBuffer = main.globalAllocator.realloc(self.sortingOutputBuffer, len);
self.currentSorting = main.globalAllocator.realloc(self.currentSorting, len);
self.sortingOutputBuffer = main.globalAllocator.realloc(self.sortingOutputBuffer, len + self.blockBreakingFaces.items.len);
for(0..self.transparentMesh.coreLen) |i| {
self.currentSorting[i].face = self.transparentMesh.completeList[i];
}
@ -1580,6 +1588,15 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
self.lastTransparentUpdatePos = updatePos;
needsUpdate = true;
}
if(self.blockBreakingFacesChanged) {
self.blockBreakingFacesChanged = false;
self.sortingOutputBuffer = main.globalAllocator.realloc(self.sortingOutputBuffer, self.currentSorting.len + self.blockBreakingFaces.items.len);
self.blockBreakingFacesSortingData = main.globalAllocator.realloc(self.blockBreakingFacesSortingData, self.blockBreakingFaces.items.len);
for(0..self.blockBreakingFaces.items.len) |i| {
self.blockBreakingFacesSortingData[i].face = self.blockBreakingFaces.items[i];
}
needsUpdate = true;
}
if(needsUpdate) {
for(self.currentSorting) |*val| {
val.update(
@ -1588,10 +1605,13 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
updatePos[2],
);
}
for(0..self.blockBreakingFaces.items.len) |i| {
self.blockBreakingFacesSortingData[i].update(updatePos[0], updatePos[1], updatePos[2]);
}
// Sort by back vs front face:
{
var backFaceStart: usize = 0;
{
var i: usize = 0;
var culledStart: usize = self.currentSorting.len;
while(culledStart > 0) {
@ -1622,6 +1642,9 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
// Sort it using bucket sort:
var buckets: [34*3]u32 = undefined;
@memset(&buckets, 0);
for(self.blockBreakingFacesSortingData) |val| {
buckets[34*3 - 1 - val.distance] += 1;
}
for(self.currentSorting[0..self.culledSortingCount]) |val| {
buckets[34*3 - 1 - val.distance] += 1;
}
@ -1632,12 +1655,23 @@ pub const ChunkMesh = struct { // MARK: ChunkMesh
prefixSum += copy;
}
// Move it over into a new buffer:
for(0..self.culledSortingCount) |i| {
for(0..backFaceStart) |i| {
const bucket = 34*3 - 1 - self.currentSorting[i].distance;
self.sortingOutputBuffer[buckets[bucket]] = self.currentSorting[i].face;
buckets[bucket] += 1;
}
// Block breaking faces should be drawn after front faces, but before the corresponding backfaces.
for(self.blockBreakingFacesSortingData) |val| {
const bucket = 34*3 - 1 - val.distance;
self.sortingOutputBuffer[buckets[bucket]] = val.face;
buckets[bucket] += 1;
}
for(backFaceStart..self.culledSortingCount) |i| {
const bucket = 34*3 - 1 - self.currentSorting[i].distance;
self.sortingOutputBuffer[buckets[bucket]] = self.currentSorting[i].face;
buckets[bucket] += 1;
}
self.culledSortingCount += @intCast(self.blockBreakingFaces.items.len);
// Upload:
faceBuffer.uploadData(self.sortingOutputBuffer[0..self.culledSortingCount], &self.transparentMesh.bufferAllocation);
self.uploadChunkPosition();

View File

@ -942,3 +942,85 @@ pub fn updateChunkMesh(mesh: *chunk.Chunk) void {
pub fn updateLightMap(map: *LightMap.LightMapFragment) void {
mapUpdatableList.enqueue(map);
}
// MARK: Block breaking animation
pub fn addBreakingAnimation(pos: Vec3i, breakingProgress: f32) void {
const animationFrame: usize = @intFromFloat(breakingProgress*@as(f32, @floatFromInt(main.blocks.meshes.blockBreakingTextures.items.len)));
const texture = main.blocks.meshes.blockBreakingTextures.items[animationFrame];
const block = getBlock(pos[0], pos[1], pos[2]) orelse return;
const modelIndex = main.blocks.meshes.model(block);
const model = &main.models.models.items[modelIndex];
for(model.internalQuads) |quadIndex| {
addBreakingAnimationFace(pos, quadIndex, texture, null, block.transparent());
}
for(&model.neighborFacingQuads, 0..) |quads, n| {
for(quads) |quadIndex| {
addBreakingAnimationFace(pos, quadIndex, texture, @enumFromInt(n), block.transparent());
}
}
}
fn addBreakingAnimationFace(pos: Vec3i, quadIndex: u16, texture: u16, neighbor: ?chunk.Neighbor, isTransparent: bool) void {
const worldPos = pos +% if(neighbor) |n| n.relPos() else Vec3i{0, 0, 0};
const relPos = worldPos & @as(Vec3i, @splat(main.chunk.chunkMask));
const mesh = getMeshAndIncreaseRefCount(.{.wx = worldPos[0], .wy = worldPos[1], .wz = worldPos[2], .voxelSize = 1}) orelse return;
defer mesh.decreaseRefCount();
mesh.mutex.lock();
defer mesh.mutex.unlock();
const lightIndex = blk: {
const meshData = if(isTransparent) &mesh.transparentMesh else &mesh.opaqueMesh;
for(meshData.completeList) |face| {
if(face.position.x == relPos[0] and face.position.y == relPos[1] and face.position.z == relPos[2] and face.blockAndQuad.quadIndex == quadIndex) {
break :blk face.position.lightIndex;
}
}
// The face doesn't exist.
return;
};
mesh.blockBreakingFacesChanged = true;
mesh.blockBreakingFaces.append(.{
.position = .{
.x = @intCast(relPos[0]),
.y = @intCast(relPos[1]),
.z = @intCast(relPos[2]),
.isBackFace = false,
.lightIndex = lightIndex,
},
.blockAndQuad = .{
.texture = texture,
.quadIndex = quadIndex,
},
});
}
fn removeBreakingAnimationFace(pos: Vec3i, quadIndex: u16, neighbor: ?chunk.Neighbor) void {
const worldPos = pos +% if(neighbor) |n| n.relPos() else Vec3i{0, 0, 0};
const relPos = worldPos & @as(Vec3i, @splat(main.chunk.chunkMask));
const mesh = getMeshAndIncreaseRefCount(.{.wx = worldPos[0], .wy = worldPos[1], .wz = worldPos[2], .voxelSize = 1}) orelse return;
defer mesh.decreaseRefCount();
for(mesh.blockBreakingFaces.items, 0..) |face, i| {
if(face.position.x == relPos[0] and face.position.y == relPos[1] and face.position.z == relPos[2] and face.blockAndQuad.quadIndex == quadIndex) {
_ = mesh.blockBreakingFaces.swapRemove(i);
mesh.blockBreakingFacesChanged = true;
break;
}
}
}
pub fn removeBreakingAnimation(pos: Vec3i) void {
const block = getBlock(pos[0], pos[1], pos[2]) orelse return;
const modelIndex = main.blocks.meshes.model(block);
const model = &main.models.models.items[modelIndex];
for(model.internalQuads) |quadIndex| {
removeBreakingAnimationFace(pos, quadIndex, null);
}
for(&model.neighborFacingQuads, 0..) |quads, n| {
for(quads) |quadIndex| {
removeBreakingAnimationFace(pos, quadIndex, @enumFromInt(n));
}
}
}