const std = @import("std"); const core = @import("core.zig"); const ArrayListUnmanaged = std.ArrayListUnmanaged; const tracy = core.tracy; pub const Transform = core.Mat; const SceneObjectSet = core.SparseMultiSet(SceneObject); const SceneSet = core.SparseSet(Scene); pub const SceneAttachMode = enum { none, // default, parent attachment is irrelevant and not used relativePositionOnly, // only position is relative to parent transform relativeRotationOnly, // only rotation is relative to parent transform relative, // position and rotation are relative to parent transform snapToParentPositionOnly, // snaps to the parent's position, maintaining independent rotation (not implemented) snapToParentRotationOnly, // copys the parent's rotation, maintaining independent rotation (not implemented) snapToParent, // snaps to parent rotation and position }; pub const SceneMobilityMode = enum { static, // scene is updated once and never again moveable, // sceneobject is moveable and has it's final transform updated }; pub const ScenePosRot = struct { position: core.Vectorf = .{ .x = 0, .y = 0, .z = 0 }, rotation: core.Rotation = core.Rotation.init(), scale: core.Vectorf = core.Vectorf.new(1.0, 1.0, 1.0), pub inline fn toTransform(self: @This()) core.Transform { var transform = core.zm.mul( core.zm.matFromQuat(self.rotation.quat), core.zm.scalingV(self.scale.toZm()), ); transform = core.zm.mul( transform, core.zm.translationV(self.position.toZm()), ); return transform; } }; pub const SceneObjectSettings = struct { attachmentMode: SceneAttachMode = .none, sceneMode: SceneMobilityMode = .static, }; pub const SceneObjectRepr = struct { // fields intended to be internally used, don't touch them // unless you know what you're doing transform: core.Mat = core.zm.identity(), // relative transform against parent, if no parent, then this is world transform parent: ?core.ObjectHandle = null, attachmentMode: SceneAttachMode = .relative, // doesn't do anything yet, only support relative right now transformOverride: ?*core.Transform = null, lastUpdate: u32 = 0, merge: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), // set to true if we've merged already, false if we havent }; pub const SceneObject = struct { _repr: SceneObjectRepr = .{}, // not public posRot: ScenePosRot = .{}, // position and rotation settings: SceneObjectSettings = .{}, // children: ArrayListUnmanaged(core.ObjectHandle) = .{}, pub fn init(params: SceneObjectInitParams) @This() { // Hmm thinking in the future we could have scene objects be f64s then crunch them down to f32s when we are submiting to gpu var self = @This(){ .posRot = .{}, ._repr = .{}, .settings = .{}, .children = .{}, }; const shouldUpdate: bool = false; // should mutate switch (params) { .transform => { self._repr.transform = params.transform; self.posRot.position = core.Vectorf.fromZm(core.zm.mul(params.transform, core.Vectorf.zero().toZm())); self.posRot.rotation = .{ .quat = core.zm.matToQuat(params.transform) }; }, .position => { @panic("todo: implement position only initialization"); }, .rotation => { @panic("todo: implement rotation only initialization"); }, .positionRotAngles => { @panic("todo: implement position + rotation initialization (angles) "); }, .positionRot => { @panic("todo: implement position + rotation initialization"); }, } if (shouldUpdate) { self.update(); } return self; } }; pub const Scene = struct { handle: core.ObjectHandle = .{ .generation = 0, .index = 0, .alive = false }, pub var BaseContainer: *SceneSet = undefined; pub var SceneObjectContainer: *SceneObjectSet = undefined; pub const ComponentName = "Scene"; pub const ScriptExports: []const []const u8 = &.{ "setPosition", "setRotation", "setScale", "setScaleV", "getPosition", "getRotation", "getParent", "printHandleIndex", // "getTransform", not implemented yet // "setMobility", gonna need special setup for this one }; pub fn initECS(self: *@This(), handle: core.ObjectHandle) void { self.handle = handle; _ = SceneObjectContainer.createWithHandleECS(handle); } pub fn deinitECS(self: *@This(), handle: core.ObjectHandle) void { _ = self; _ = SceneObjectContainer.destroyObject(handle); } pub fn printHandleIndex(self: @This()) void { core.engine_log("handle.index = 0x{x} generation = {d} alive={any}", .{ self.handle.index, self.handle.generation, self.handle.alive }); } pub fn setPosRot(self: @This(), newPosRot: ScenePosRot) void { if (SceneObjectContainer.get(self.handle, .posRot)) |posRot| { posRot.* = newPosRot; } else { core.engine_log("setposRot failed handle.index = 0x{x} generation = {d} alive={any}", .{ self.handle.index, self.handle.generation, self.handle.alive }); } } pub fn setPosition(self: @This(), position: core.Vectorf) void { if (SceneObjectContainer.get(self.handle, .posRot)) |posRot| { posRot.*.position = position; } else { core.engine_log("setposition failed handle.index = 0x{x} generation = {d} alive={any}", .{ self.handle.index, self.handle.generation, self.handle.alive }); } } pub fn getPosRot(self: *@This()) *ScenePosRot { return SceneObjectContainer.get(self.handle, .posRot).?; } pub fn setRotation(self: @This(), rotation: core.Rotation) void { SceneObjectContainer.get(self.handle, .posRot).?.*.rotation = rotation; } pub fn setScale(self: @This(), x: f32, y: f32, z: f32) void { SceneObjectContainer.get(self.handle, .posRot).?.*.scale = .{ .x = x, .y = y, .z = z }; } pub fn setScaleV(self: @This(), scale: core.Vectorf) void { SceneObjectContainer.get(self.handle, .posRot).?.*.scale = scale; } pub fn getPosition(self: @This()) core.Vectorf { // core.engine_logs("getPosition called"); return SceneObjectContainer.get(self.handle, .posRot).?.position; } pub fn getRotation(self: @This()) core.Rotation { return SceneObjectContainer.get(self.handle, .posRot).?.rotation; } pub fn getScaleV(self: @This()) core.Vectorf { return SceneObjectContainer.get(self.handle, .posRot).?.scale; } pub fn getParent(self: @This()) core.Entity { return core.Entity{ .handle = SceneObjectContainer.get(self.handle, ._repr).?.parent orelse .{} }; } pub fn setParent(self: @This(), newParent: core.Entity) void { core.engine_log("parenting entity {d} -> {d}", .{ self.handle.index, newParent.handle.index }); const repr: *SceneObjectRepr = SceneObjectContainer.get(self.handle, ._repr).?; const thisParent = repr.parent; if (thisParent) |p| { const parentRef = @This(){ .handle = p }; parentRef.removeChild(self.handle); } repr.parent = newParent.handle; const children = SceneObjectContainer.get(newParent.handle, .children).?; children.append(childAllocator(), newParent.handle) catch unreachable; } pub fn clearParent(self: @This()) void { const repr = SceneObjectContainer.get(self.handle, ._repr).?; const thisParent = &repr.parent; if (thisParent) |p| { const parentRef = @This(){ .handle = p }; parentRef.removeChild(self.handle); } repr.parent = null; } pub fn removeChild(self: @This(), child: core.ObjectHandle) void { const children = SceneObjectContainer.get(self.handle, .children).?; for (children.items, 0..) |search, i| { if (child.eql(search)) { _ = children.swapRemove(i); break; } } } pub fn getTickCount() u32 {} pub fn getAndResolveTransform(self: @This()) core.Transform { const repr: *SceneObjectRepr = SceneObjectContainer.get(self.handle, ._repr).?; if (repr.lastUpdate != getSceneSystem().tickCount) { getSceneSystem().updateTransform(repr, SceneObjectContainer.get(self.handle, .posRot).?); } return repr.transform; } pub fn getTransform(self: @This()) core.Transform { return SceneObjectContainer.get(self.handle, ._repr).?.transform; } pub fn updateTransform(self: @This()) void { const repr = SceneObjectContainer.get(self.handle, ._repr).?; const posRot = SceneObjectContainer.get(self.handle, .posRot).?; getSceneSystem().updateTransform(repr, posRot); } // you MUST clearTransfomRefUnsafe() before destroying this transform pub fn setTransformRefUnsafe(self: @This(), ref: *core.Transform) void { SceneObjectContainer.get(self.handle, ._repr).?.transformOverride = ref; } pub fn clearTransformRefUnsafe(self: @This(), ref: *core.Transform) void { SceneObjectContainer.get(self.handle, ._repr).?.transformOverride = ref; } pub fn setMobility(self: @This(), mobility: SceneMobilityMode) void { const settings = Scene.SceneObjectContainer.get(self.handle, .settings).?; if (settings.sceneMode == .static) { if (mobility == .moveable) { const gScene = getSceneSystem(); gScene.dynamicObjects.append(gScene.allocator, self.handle) catch unreachable; } } if (settings.sceneMode == .moveable) { if (mobility == .static) { @panic("todo unable to change scene mobility back to static"); } } settings.*.sceneMode = mobility; } }; pub const SceneObjectInitParams = union(enum) { transform: core.Transform, position: core.Vectorf, rotation: core.Quat, positionRotAngles: struct { position: core.Vectorf = .{ .x = 0.0, .y = 0.0, .z = 0.0 }, angles: core.Vectorf = .{ .x = 0.0, .y = 0.0, .z = 0.0 }, }, positionRot: struct { position: core.Vectorf = .{ .x = 0.0, .y = 0.0, .z = 0.0 }, angles: core.Quat = core.zm.qidentity(), }, }; pub const getSceneSystem = core.EngineObject(SceneSystem).get; fn childAllocator() std.mem.Allocator { return getSceneSystem().childrenArena.allocator(); } pub const SceneSystem = struct { pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This(), "core.SceneSystem"); allocator: std.mem.Allocator, dynamicObjects: ArrayListUnmanaged(core.ObjectHandle) = .{}, childrenArena: std.heap.ArenaAllocator, tickCount: u32 = 0, sceneObjectContainer: *SceneObjectSet = undefined, cachedOutputs: std.ArrayList(std.ArrayList(core.Transform)) = .{}, writeOutList: std.ArrayList(std.ArrayList(usize)) = .{}, lastUpdateTransformTime: f64 = 0.0, pub const Field = SceneObjectSet.Field; pub const FieldType = SceneObjectSet.FieldType; // internal update transform function fn updateTransform(self: *@This(), repr: *SceneObjectRepr, posRot: *const ScenePosRot) void { if (repr.lastUpdate == self.tickCount) { return; } if (repr.transformOverride) |override| { repr.lastUpdate = self.tickCount; repr.transform = override.*; return; } var final: core.Transform = core.zm.identity(); if (repr.parent) |parent| { if (Scene.SceneObjectContainer.get(parent, ._repr)) |parentRepr| { const parentPosRot = Scene.SceneObjectContainer.get(parent, .posRot).?; self.updateTransform(parentRepr, parentPosRot); const parentTransform = parentRepr.transform; final = core.zm.mul(parentTransform, final); } else {} } repr.transform = core.zm.mul( core.zm.mul( core.zm.mul( core.zm.scalingV(posRot.scale.toZm()), core.zm.matFromQuat(posRot.rotation.quat), ), core.zm.translationV(posRot.position.toZm()), ), final, ); repr.lastUpdate = self.tickCount; } const useParallelJob = true; pub fn updateTransforms(self: *@This()) void { // todo. calculate a running load factor for the number of movable objects // vs static objects // if we have a small amount of movable vs static AND if we have > 1000 objects, // then iterate over dynamicObjects array instead if (useParallelJob) { // we use 6 workers if the last timing scope ran > 2ms // otherwise use 1 worker var scope = core.engineTime.takeProfilingStamp(); var workerCount: u32 = 1; if (self.lastUpdateTransformTime > 0.001) // 1ms { workerCount = 6; } core.parallelJob(UpdateWorldTransformsJob{}, false, workerCount) catch unreachable; scope.end(); if (scope.duration()) |duration| { self.lastUpdateTransformTime = duration; } } else { for (Scene.SceneObjectContainer.denseItems(._repr), 0..) |*repr, i| { const settings = Scene.SceneObjectContainer.readDense(i, .settings); if (settings.sceneMode == .moveable or repr.lastUpdate == 0) { const posRot = Scene.SceneObjectContainer.readDense(i, .posRot); self.updateTransform(repr, posRot); } } } } pub const MaxWorkerCount = 24; // ----- NeonObject interace ---- pub fn init(allocator: std.mem.Allocator) !*@This() { const self = try allocator.create(@This()); self.* = .{ .allocator = allocator, .childrenArena = std.heap.ArenaAllocator.init(allocator), }; core.EngineObject(@This()).gInstance = self; // try core.defineComponent(Scene, allocator); Scene.SceneObjectContainer = try SceneObjectSet.create(allocator); self.sceneObjectContainer = Scene.SceneObjectContainer; for (0..MaxWorkerCount) |i| { _ = i; try self.cachedOutputs.append(self.allocator, .{}); try self.writeOutList.append(self.allocator, .{}); } return self; } pub fn getOutputList(self: *@This(), threadId: u32) !*std.ArrayList(usize) { const outputLen = self.sceneObjectContainer.denseItems(._repr).len; try self.writeOutList.items[threadId].ensureTotalCapacity(self.allocator, outputLen); self.writeOutList.items[threadId].shrinkRetainingCapacity(0); return &self.writeOutList.items[threadId]; } pub fn getOutputForWorker(self: *@This(), threadId: u32) ![]core.Transform { const outputLen = self.sceneObjectContainer.denseItems(._repr).len; try self.cachedOutputs.items[threadId].resize(self.allocator, outputLen); return self.cachedOutputs.items[threadId].items; } pub fn preTick(self: *@This(), dt: f64) !void { _ = dt; self.tickCount +%= 1; if (self.tickCount == 0) { self.tickCount += 1; } } pub fn tick(self: *@This(), deltaTime: f64) void { var z = tracy.ZoneNC(@src(), "Scene System Tick", 0xAABBDD); defer z.End(); self.updateTransforms(); _ = deltaTime; } pub fn deinit(self: *@This()) void { for (self.cachedOutputs.items) |*i| { i.deinit(self.allocator); } for (self.writeOutList.items) |*i| { i.deinit(self.allocator); } self.dynamicObjects.deinit(self.allocator); self.childrenArena.deinit(); Scene.SceneObjectContainer.destroy(); self.cachedOutputs.deinit(self.allocator); self.writeOutList.deinit(self.allocator); self.allocator.destroy(self); } }; const UpdateWorldTransformsJob = struct { world: ?*anyopaque = null, pub fn func(self: @This(), thread: *core.ThreadContext) void { const z = core.tracy.ZoneN(@src(), "Transform Hierarchy - wide"); defer z.End(); thread.barrier(@src()) catch unreachable; const outputs = core.get(core.SceneSystem).getOutputForWorker(thread.threadId) catch return; const outputList = core.get(core.SceneSystem).getOutputList(thread.threadId) catch return; self.updateTransformsHierarchy(thread, outputs, outputList) catch |err| switch (err) { error.OutOfMemory => { unreachable; }, // else => { // thread.abort(@src(), "unknown error", err); // return; // }, }; const denseRepr = core.Scene.SceneObjectContainer.denseItems(._repr); const z3 = core.tracy.ZoneN(@src(), "Merge Outputs"); // merge outputs for (outputList.items) |outIndex| { if (denseRepr[outIndex].merge.cmpxchgStrong(false, true, .seq_cst, .acquire) == null) { denseRepr[outIndex].transform = outputs[outIndex]; } } z3.End(); thread.barrier(@src()) catch unreachable; } fn updateTransform( self: @This(), thread: *core.ThreadContext, index: usize, densePosRot: []core.scene.ScenePosRot, denseRepr: []core.scene.SceneObjectRepr, outputs: []core.math.Transform, outputList: *std.ArrayList(usize), ) void { var final: core.Transform = core.zm.identity(); const repr = denseRepr[index]; if (repr.parent) |parent| { if (core.Scene.SceneObjectContainer.sparseToDense(parent)) |parentIndex| { self.updateTransform(thread, parentIndex, densePosRot, denseRepr, outputs, outputList); const parentTransform = outputs[parentIndex]; final = parentTransform; } else {} } const posRot = densePosRot[index]; outputs[index] = core.zm.mul( core.zm.mul( core.zm.mul( core.zm.scalingV(posRot.scale.toZm()), core.zm.matFromQuat(posRot.rotation.quat), ), core.zm.translationV(posRot.position.toZm()), ), final, ); outputList.appendAssumeCapacity(index); } pub fn updateTransformsHierarchy(self: @This(), thread: *core.ThreadContext, outputs: []core.math.Transform, outputList: *std.ArrayList(usize)) !void { const z = core.tracy.ZoneN(@src(), "updateTransformsHierarchy"); defer z.End(); //const dense = self.world.denseScenes(); const densePosRot = core.Scene.SceneObjectContainer.denseItems(.posRot); const denseRepr = core.Scene.SceneObjectContainer.denseItems(._repr); // everything allocated with thread.scratch is blown away when the thread is complete // try outputs.resize(thread.scratch(), densePosRot.len); // scan and mark all root nodes for update const split = thread.splitSlice(core.scene.SceneObjectRepr, denseRepr); // const locals:[]core.math.Transform = try thread.scratch().alloc(core.math.Transform, split.slice.len); const z2 = core.tracy.ZoneN(@src(), "WalkAndResolve"); for (split.slice, 0..) |*repr, i| { repr.merge.store(false, .seq_cst); // reset the merge big const index = split.startIndex + i; self.updateTransform(thread, index, densePosRot, denseRepr, outputs, outputList); } z2.End(); } }; // LUA_BEGIN // because scene objects are a special sparse-multiset type, // they do not have a fixed representation in the sparse set. // as a result this type requires a special implementation to operate properly. // multi-set systems should only ever modify values via functions // we really need a way to deal with multi-set handles. // idea - in the component registration. if the container type is a sparse multiset // then the pointer type shall be a pointer to the set handle. // and the component acquisition shall do absolutely nothing but grab the sparse index of the set handle // // man... that shit sounds like so much work... // LUA_END