// big main sy.itemsstem for animation const ozz = @import("ozz"); const core = @import("core"); const std = @import("std"); pub const BoneHandle = enum(u8) { _ }; pub const Skeleton = struct { sk: *ozz.Skeleton, inverseBinds: std.ArrayListUnmanaged(core.Mat) = .{}, jointMapping: std.StringHashMapUnmanaged(u8) = .{}, pub fn buildJointMap(self: *@This(), allocator: std.mem.Allocator) !void { for (self.sk.getJointsList(), 0..) |jointName, i| { // std.debug.print("jointName {d} {s}\n", .{ i, jointName }); const str = std.mem.span(jointName); try self.jointMapping.put(allocator, str, @intCast(i)); } } pub fn getBoneHandleByName(self: @This(), string: []const u8) ?BoneHandle { if (self.jointMapping.get(string)) |x| { return @enumFromInt(x); } else { return null; } } pub fn deinit(self: *@This()) void { self.sk.destroy(); } }; pub const AnimationTrack = struct { animation: *ozz.Animation, endTime: f32 = 1.0, pub fn deinit(self: *@This()) void { self.animation.destroy(); } }; pub const PlaybackTrack = struct { track: ?*AnimationTrack = null, playback: f32 = 0.0, playbackRate: f32 = 1.0, }; pub const Animator = struct { jointRemap: ?[]u8 = null, animationName: ?core.Name = null, skeleton: ?*Skeleton = null, skeletonName: ?core.Name = null, sjc: *ozz.SamplingJobContext = undefined, track: ?*AnimationTrack = null, playback: f32 = 0.0, playbackRate: f32 = 1.0, // todo.. implement blending // animations: [4]*ozz.Animation = undefined, // timelines: [4]f32 = .{ 0, 0, 0, 0 }, animationCount: u32 = 0, locals: std.ArrayListUnmanaged(ozz.SoaTransform) = .{}, models: std.ArrayListUnmanaged(ozz.Float4x4) = .{}, finals: std.ArrayListUnmanaged(core.Mat) = .{}, finalsSpan: core.Span = undefined, entity: core.Entity = undefined, jointLength: usize = 0, resolverRef: ?AnimResolverRef = null, pub var allocator: std.mem.Allocator = undefined; // oh god if I want to support multiple animation blending... // maybe the kernel should contain a fixed amount of animations? pub fn initECS(self: *@This(), handle: core.SetHandle) void { // get the mesh component self.entity = core.Entity{ .handle = handle }; if (self.entity.fetch(rend.MeshComponent)) |mesh| { mesh.animated = true; //todo mesh.animator = self; self.sjc = ozz.SamplingJobContext.createMaxTracks(256); } else { @panic("animator added to an entity that does not have a mesh component"); } } pub fn getBoneTransform(self: *@This(), handle: BoneHandle) core.Mat { return @bitCast(self.models.items[@intFromEnum(handle)]); } pub fn setSkeletonByName(self: *@This(), skName: core.Name) !void { if (self.skeleton != null) { // return the previous span and allocate a new one. gAnimationSys.slots.removeSpan(self.finalsSpan); } self.skeletonName = skName; self.skeleton = gAnimationSys.skeletons.get(self.skeletonName.?.handle()).?; const numJoints = self.skeleton.?.sk.numJoints(); self.jointLength = numJoints; self.sjc.resize(@intCast(numJoints)); try self.locals.resize(allocator, self.skeleton.?.sk.numSoaJoints()); try self.models.resize(allocator, numJoints); try self.finals.resize(allocator, numJoints); self.finalsSpan = try gAnimationSys.slots.allocate(@intCast(numJoints)); if (self.resolverRef) |ref| { if (ref.vtable.onSkeletonSet) |f| { f(ref.ptr, self); } } self.jointRemap = null; } pub fn setSkeleton(self: *@This(), skeleton: []const u8) void { self.setSkeletonByName(core.MakeName(skeleton)) catch unreachable; } pub fn addResolver(self: *@This(), comptime Resolver: type) !*Resolver { const resolver = try Resolver.create(allocator); try resolver.onSkeletonSet(self); self.resolverRef = core.refFromPtr(AnimResolverInterface, resolver); return resolver; } pub fn removeResolver(self: *@This()) void { if (self.resolverRef) |ref| { ref.vtable.destroy(ref.ptr); self.resolverRef = null; } } pub fn update(self: *@This(), dt: f64) void { if (self.skeleton == null) { return; } // if a resolver is present, use that to update my the finals instead of the default function below if (self.resolverRef) |ref| { ref.vtable.resolve(ref.ptr, dt, self); return; } if (!self.defaultSample(dt)) { return; } self.modelToFinal(); } fn defaultSample(self: *@This(), dt: f64) bool { if (self.track == null) return false; const track = self.track.?; if (track.endTime < 0.01) return false; const skeleton = self.skeleton.?; self.playback += @as(f32, @floatCast(dt)) * self.playbackRate; while (self.playback > track.endTime) { self.playback -= track.endTime; } var samplingJob: ozz.SamplingJob = .{ .ratio = self.playback / track.endTime, .animation = track.animation, .context = self.sjc, .output = ozz.makeSpan(self.locals.items), }; if (!samplingJob.run()) { core.engine_errs("sampling job failed"); return false; } var ltmJob: ozz.LocalToModelJob = .{ .skeleton = skeleton.sk, .input = ozz.makeSpan(self.locals.items), .output = ozz.makeSpan(self.models.items), }; if (!ltmJob.run()) { core.engine_errs("local to model job failed"); return false; } return true; } pub fn commitLocalToModel(self: *@This(), input: []ozz.SoaTransform) void { self.localToModel(input, self.models.items); } pub fn localToModel(self: *@This(), input: []ozz.SoaTransform, output: []ozz.Float4x4) void { var ltmJob: ozz.LocalToModelJob = .{ .skeleton = self.skeleton.?.sk, .input = ozz.makeSpan(input), .output = ozz.makeSpan(output), }; if (!ltmJob.run()) { core.engine_errs("local to model job failed"); return; } } pub fn sampleAnimation(self: *@This(), time: f32, track: *AnimationTrack, output: []ozz.SoaTransform) void { var samplingJob: ozz.SamplingJob = .{ .ratio = time / track.endTime, .animation = track.animation, .context = self.sjc, .output = ozz.makeSpan(output), }; if (!samplingJob.run()) { core.engine_errs("sampling job failed"); return; } } pub fn modelToFinal(self: *@This()) void { if (self.jointRemap == null) { if (self.entity.fetch(rend.MeshComponent)) |meshComponent| { if (meshComponent.mesh) |mesh| { self.jointRemap = mesh.jointRemap; } } } const skeleton = self.skeleton.?; for (self.models.items, 0..) |model, i| { const transform: core.Mat = @bitCast(model); // const p: core.zm.Vec = .{ 0, 0, 0, 1 }; // graphics.debugSphere(core.Vectorf.fromZm(core.zm.mul(p, transform)), 0.03, .{ // .color = if (i == 15) .{ .x = 1 } else .{ .y = 1 }, // }); const final = core.zm.mul(skeleton.inverseBinds.items[i], transform); // joint remap ozz -> gltf if (self.jointRemap) |jr| { // core.engine_log("{d} xx {d}", .{ i, jr[i] }); self.finals.items[@intCast(jr[i])] = final; } else { self.finals.items[i] = final; } // core.engine_log( // "[{d}] {d} {d} {d} {d}, {d} {d} {d} {d}", // .{ i, transform[0][0], transform[0][1], transform[0][2], transform[0][3], transform[1][0], transform[1][1], transform[1][2], transform[1][3] }, // ); } } pub fn setAnimationByName(self: *@This(), _name: core.Name) !void { var name = _name; self.track = gAnimationSys.animTracks.get(name.handle()); } pub fn setAnimation(self: *@This(), path: []const u8) void { const name = core.MakeName(path); self.setAnimationByName(name) catch unreachable; } pub fn deinitECS(self: *@This(), handle: core.SetHandle) void { _ = handle; self.deinit(); } pub fn deinit(self: *@This()) void { core.engine_logs("destroying animator!!"); self.sjc.destroy(); self.removeResolver(); self.finals.deinit(allocator); self.locals.deinit(allocator); self.models.deinit(allocator); } pub var BaseContainer: *core.SparseMap(@This()) = undefined; pub const ComponentName = "Animator"; pub const ScriptExports: []const []const u8 = &.{}; }; pub const AnimationSystem = struct { backingAllocator: std.mem.Allocator, arena: std.heap.ArenaAllocator, slots: MergedSpans, // Only AnimationTrack and Skeletons are made using the ArenaAllocator animTracks: std.AutoHashMapUnmanaged(u32, *AnimationTrack) = .{}, skeletons: std.AutoHashMapUnmanaged(u32, *Skeleton) = .{}, sharedArena: [2]std.heap.ArenaAllocator, // could be a good usecase for a fat bump arena shared: [2]std.ArrayListUnmanaged(MatrixUploads) = .{ .{}, .{} }, sharedLocks: [2]std.Thread.Mutex = .{ .{}, .{} }, // could be a good usecase for a fat bump arena pub const MatrixUploads = struct { offset: u32, matrices: std.ArrayListUnmanaged(core.Mat) = .{}, }; pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This(), "rend.AnimationSystem"); pub fn preTick(self: *@This(), dt: f64) !void { _ = self; var z1 = core.tracy.ZoneN(@src(), "animation system tick"); defer z1.End(); for (Animator.BaseContainer.list.items) |animator| { animator.update(dt); } } pub fn newAnimTrack(self: *@This(), _name: core.Name, anim: *ozz.Animation) !void { var name = _name; const new = try self.arenaAllocator().create(AnimationTrack); new.* = .{ .animation = anim, .endTime = anim.getDuration(), }; try self.animTracks.put(self.backingAllocator, name.handle(), new); } pub fn newSkeleton(self: *@This(), _name: core.Name, sk: *ozz.Skeleton) !void { const new = try self.arenaAllocator().create(Skeleton); new.* = .{ .sk = sk, }; var name = _name; try new.buildJointMap(self.arenaAllocator()); try new.inverseBinds.resize(self.arenaAllocator(), new.sk.numJoints()); if (new.inverseBinds.items.len > 256) { @panic("too many bones in skeleton, not supported"); } var bindModels = std.ArrayList(ozz.Float4x4){}; // .init(self.backingAllocator); defer bindModels.deinit(self.backingAllocator); try bindModels.resize(self.backingAllocator, new.sk.numJoints()); var ltmJob: ozz.LocalToModelJob = .{ .skeleton = new.sk, .input = new.sk.getRestPoseModel(), .output = ozz.makeSpan(bindModels.items), }; core.engine_log("creating bind pose {d} joints", .{new.inverseBinds.items.len}); if (!ltmJob.run()) { core.engine_logs("unable to get bind pose"); return error.UnableToLoad; } for (bindModels.items, 0..) |bind, i| { // const p: core.zm.Vec = .{ 0, 0, 0, 1 }; // graphics.debugSphere(core.Vectorf.fromZm(core.zm.mul(p, @as(core.Mat, @bitCast(bind)))), 0.1, .{ .duration = 100 }); new.inverseBinds.items[i] = core.zm.inverse(@as(core.Mat, @bitCast(bind))); } try self.skeletons.put(self.backingAllocator, name.handle(), new); } pub fn arenaAllocator(self: *@This()) std.mem.Allocator { return self.arena.allocator(); } pub fn getShared(self: @This(), fi: u32) []const MatrixUploads { return self.shared[fi].items; } pub fn sendShared(self: *@This(), frameIndex: u32) void { const fi: usize = @intCast(frameIndex); self.sharedLocks[fi].lock(); defer self.sharedLocks[fi].unlock(); _ = self.sharedArena[fi].reset(.retain_capacity); const allocator = self.sharedArena[fi].allocator(); const shared = &self.shared[fi]; shared.* = .{}; for (Animator.BaseContainer.list.items) |animator| { var upload: MatrixUploads = .{ .offset = animator.finalsSpan.start }; // core.engine_log( // "finalsSpan size offset{d} {d} animator finals {d}\n", // .{ // animator.finalsSpan.start, // animator.finalsSpan.size, // animator.finals.items.len // }); upload.matrices.resize(allocator, animator.finalsSpan.size) catch unreachable; for (animator.finals.items, 0..) |final, i| { upload.matrices.items[i] = final; } shared.append(allocator, upload) catch unreachable; } } pub fn init(self: *@This(), alloc: std.mem.Allocator, first: bool) !void { if (!first) return; self.* = .{ .backingAllocator = alloc, .arena = std.heap.ArenaAllocator.init(alloc), .sharedArena = .{ std.heap.ArenaAllocator.init(alloc), std.heap.ArenaAllocator.init(alloc), }, .slots = try MergedSpans.init(alloc, max_skin_slots), }; gAnimationSys = self; Animator.allocator = alloc; core.engine_logs("Animation System initialized"); } pub fn deinit(self: *@This()) void { core.engine_logs("deinitializing animation system"); { core.engine_log("skeleton count {d}", .{self.skeletons.count()}); var iter = self.skeletons.iterator(); while (iter.next()) |i| { i.value_ptr.*.deinit(); } } { core.engine_log("animTracks count {d}", .{self.animTracks.count()}); var iter = self.animTracks.iterator(); while (iter.next()) |i| { i.value_ptr.*.deinit(); } } for (self.sharedArena) |arena| { arena.deinit(); } for (Animator.BaseContainer.list.items) |animator| { animator.deinit(); } self.slots.deinit(); self.arena.deinit(); self.skeletons.deinit(self.backingAllocator); self.animTracks.deinit(self.backingAllocator); } }; pub var gAnimationSys: *AnimationSystem = undefined; pub fn getAnimationSystem() *AnimationSystem { return gAnimationSys; } pub fn getSkeletonByName(_name: core.Name) ?*Skeleton { var name = _name; return gAnimationSys.skeletons.get(name.handle()); } const rend = @import("../rend.zig"); const MergedSpans = core.MergedSpans; pub const max_skin_slots = 100_000; const anim_resolver = @import("animationResolver.zig"); const AnimResolverRef = anim_resolver.AnimResolverRef; const AnimResolverInterface = anim_resolver.AnimResolverInterface;