From 3db95510bbc932140f8d57132f329711e88cc748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Wi=C5=9Bniewski?= Date: Mon, 17 Aug 2026 00:06:59 +0200 Subject: [PATCH] Extend texture_pile --- assets/cubyz/blocks/castilleja.zig.zon | 22 +- assets/cubyz/blocks/dead_leaf_pile.zig.zon | 8 +- assets/cubyz/blocks/red_leaf_pile.zig.zon | 8 +- assets/cubyz/blocks/yellow_leaf_pile.zig.zon | 8 +- assets/cubyz/models/layer/1.obj | 45 ++++ assets/cubyz/models/layer/2.obj | 40 ++++ assets/cubyz/models/layer/3.obj | 40 ++++ mods/cubyz/rotations/texture_pile.zig | 229 +++++++++++++++++-- src/items.zig | 4 +- src/models.zig | 7 + src/zon.zig | 9 +- 11 files changed, 389 insertions(+), 31 deletions(-) create mode 100644 assets/cubyz/models/layer/1.obj create mode 100644 assets/cubyz/models/layer/2.obj create mode 100644 assets/cubyz/models/layer/3.obj diff --git a/assets/cubyz/blocks/castilleja.zig.zon b/assets/cubyz/blocks/castilleja.zig.zon index aaa4a85f52..e7ae0cb4ae 100644 --- a/assets/cubyz/blocks/castilleja.zig.zon +++ b/assets/cubyz/blocks/castilleja.zig.zon @@ -10,8 +10,26 @@ .viewThrough = true, .absorbedLight = 0x000000, .collide = false, - .model = "cubyz:flower/castelleja", - .rotation = "cubyz:planar", + .rotation = "cubyz:texture_pile", + .model = .{ + .mode = .modelReposition, + .model = "cubyz:flower/castelleja", + .offsets = .{ + .{ + .{0.0, 0.0, 0.0}, + }, + .{ + .{-0.25, -0.25, 0.0}, + .{0.25, 0.25, 0.0}, + }, + .{ + .{0.25, 0.25, 0.0}, + .{-0.25, 0.25, 0.0}, + .{0.0, -0.25, 0.0}, + }, + }, + .states = 3, + }, .texture = "cubyz:castilleja", .texture_top = "cubyz:castilleja_top", .texture_bottom = "cubyz:castilleja_top", diff --git a/assets/cubyz/blocks/dead_leaf_pile.zig.zon b/assets/cubyz/blocks/dead_leaf_pile.zig.zon index eed30e900e..c2c561ba83 100644 --- a/assets/cubyz/blocks/dead_leaf_pile.zig.zon +++ b/assets/cubyz/blocks/dead_leaf_pile.zig.zon @@ -11,7 +11,13 @@ .alwaysViewThrough = true, .absorbedLight = 0x000000, .model = .{ - .model = "cubyz:plane", + .mode = .textureAndModel, + .models = .{ + "cubyz:plane", + "cubyz:layer/1", + "cubyz:layer/2", + "cubyz:layer/3", + }, .states = 4, }, .rotation = "cubyz:texture_pile", diff --git a/assets/cubyz/blocks/red_leaf_pile.zig.zon b/assets/cubyz/blocks/red_leaf_pile.zig.zon index 8fec15c113..40f72d4dfd 100644 --- a/assets/cubyz/blocks/red_leaf_pile.zig.zon +++ b/assets/cubyz/blocks/red_leaf_pile.zig.zon @@ -11,7 +11,13 @@ .alwaysViewThrough = true, .absorbedLight = 0x000000, .model = .{ - .model = "cubyz:plane", + .mode = .textureAndModel, + .models = .{ + "cubyz:plane", + "cubyz:layer/1", + "cubyz:layer/2", + "cubyz:layer/3", + }, .states = 4, }, .rotation = "cubyz:texture_pile", diff --git a/assets/cubyz/blocks/yellow_leaf_pile.zig.zon b/assets/cubyz/blocks/yellow_leaf_pile.zig.zon index e5194faf5f..8d060f005d 100644 --- a/assets/cubyz/blocks/yellow_leaf_pile.zig.zon +++ b/assets/cubyz/blocks/yellow_leaf_pile.zig.zon @@ -11,7 +11,13 @@ .alwaysViewThrough = true, .absorbedLight = 0x000000, .model = .{ - .model = "cubyz:plane", + .mode = .textureAndModel, + .models = .{ + "cubyz:plane", + "cubyz:layer/1", + "cubyz:layer/2", + "cubyz:layer/3", + }, .states = 4, }, .rotation = "cubyz:texture_pile", diff --git a/assets/cubyz/models/layer/1.obj b/assets/cubyz/models/layer/1.obj new file mode 100644 index 0000000000..3f8c69343f --- /dev/null +++ b/assets/cubyz/models/layer/1.obj @@ -0,0 +1,45 @@ +o layer0 +v 1 0 0.0625 +v 1 1 0.0625 +v 0 1 0.0625 +v 0 0 0.0625 +v 0 0 0 +v 0 1 0 +v 1 1 0 +v 1 0 0 +vt 0 0.25 +vt 0 0 +vt 0.25 0 +vt 0.25 0.25 +vt 0.25 0.25 +vt 0.25 0 +vt 0.5 0 +vt 0.5 0.25 +vt 0.5 0.25 +vt 0.515625 0.25 +vt 0.515625 0 +vt 0.5 0 +vt 1 0 +vt 0.984375 0 +vt 0.984375 0.25 +vt 1 0.25 +vt 0 0.265625 +vt 0 0.25 +vt 0.25 0.25 +vt 0.25 0.265625 +vt 0.25 0.5 +vt 0.25 0.484375 +vt 0.5 0.484375 +vt 0.5 0.5 +vn 0 0 1 +vn 0 0 -1 +vn 1 0 0 +vn -1 0 0 +vn 0 1 0 +vn 0 -1 0 +f 1/1/1 2/2/1 3/3/1 4/4/1 +f 5/5/2 6/6/2 7/7/2 8/8/2 +f 1/9/3 8/10/3 7/11/3 2/12/3 +f 3/13/4 6/14/4 5/15/4 4/16/4 +f 2/17/5 7/18/5 6/19/5 3/20/5 +f 4/21/6 5/22/6 8/23/6 1/24/6 \ No newline at end of file diff --git a/assets/cubyz/models/layer/2.obj b/assets/cubyz/models/layer/2.obj new file mode 100644 index 0000000000..55d4acf7d1 --- /dev/null +++ b/assets/cubyz/models/layer/2.obj @@ -0,0 +1,40 @@ +o layer1 +v 1.000000 0.000000 0.125000 +v 1.000000 1.000000 0.125000 +v 0.000000 1.000000 0.125000 +v 0.000000 0.000000 0.125000 +v 0.000000 0.000000 0.000000 +v 0.000000 1.000000 -0.000000 +v 1.000000 1.000000 -0.000000 +v 1.000000 0.000000 0.000000 +vn -0.0000 -0.0000 1.0000 +vn -0.0000 -0.0000 -1.0000 +vn 1.0000 -0.0000 -0.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.0000 1.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vt 0.000000 0.250000 +vt 0.000000 0.000000 +vt 0.250000 0.000000 +vt 0.250000 0.250000 +vt 0.500000 0.000000 +vt 0.500000 0.250000 +vt 0.531250 0.250000 +vt 0.531250 0.000000 +vt 1.000000 0.000000 +vt 0.968750 0.000000 +vt 0.968750 0.250000 +vt 1.000000 0.250000 +vt 0.000000 0.281250 +vt 0.250000 0.281250 +vt 0.250000 0.500000 +vt 0.250000 0.468750 +vt 0.500000 0.468750 +vt 0.500000 0.500000 +s 0 +f 1/1/1 2/2/1 3/3/1 4/4/1 +f 5/4/2 6/3/2 7/5/2 8/6/2 +f 1/6/3 8/7/3 7/8/3 2/5/3 +f 3/9/4 6/10/4 5/11/4 4/12/4 +f 2/13/5 7/1/5 6/4/5 3/14/5 +f 4/15/6 5/16/6 8/17/6 1/18/6 diff --git a/assets/cubyz/models/layer/3.obj b/assets/cubyz/models/layer/3.obj new file mode 100644 index 0000000000..a0ef106cfc --- /dev/null +++ b/assets/cubyz/models/layer/3.obj @@ -0,0 +1,40 @@ +o layer3 +v 1.000000 0.000000 0.187500 +v 1.000000 1.000000 0.187500 +v 0.000000 1.000000 0.187500 +v 0.000000 0.000000 0.187500 +v 0.000000 0.000000 0.000000 +v 0.000000 1.000000 -0.000000 +v 1.000000 1.000000 -0.000000 +v 1.000000 0.000000 0.000000 +vn -0.0000 -0.0000 1.0000 +vn -0.0000 -0.0000 -1.0000 +vn 1.0000 -0.0000 -0.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.0000 1.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vt 0.000000 0.250000 +vt 0.000000 0.000000 +vt 0.250000 0.000000 +vt 0.250000 0.250000 +vt 0.500000 0.000000 +vt 0.500000 0.250000 +vt 0.546875 0.250000 +vt 0.546875 0.000000 +vt 1.000000 0.000000 +vt 0.953125 0.000000 +vt 0.953125 0.250000 +vt 1.000000 0.250000 +vt 0.000000 0.296875 +vt 0.250000 0.296875 +vt 0.250000 0.500000 +vt 0.250000 0.453125 +vt 0.500000 0.453125 +vt 0.500000 0.500000 +s 0 +f 1/1/1 2/2/1 3/3/1 4/4/1 +f 5/4/2 6/3/2 7/5/2 8/6/2 +f 1/6/3 8/7/3 7/8/3 2/5/3 +f 3/9/4 6/10/4 5/11/4 4/12/4 +f 2/13/5 7/1/5 6/4/5 3/14/5 +f 4/15/6 5/16/6 8/17/6 1/18/6 diff --git a/mods/cubyz/rotations/texture_pile.zig b/mods/cubyz/rotations/texture_pile.zig index cd3b797ba8..2c687d664d 100644 --- a/mods/cubyz/rotations/texture_pile.zig +++ b/mods/cubyz/rotations/texture_pile.zig @@ -12,47 +12,236 @@ const Vec3f = vec.Vec3f; const Vec3i = vec.Vec3i; const ZonElement = main.ZonElement; -var rotatedModels: std.StringHashMap(ModelIndex) = undefined; +const minStateCount: u8 = 2; +const maxStateCount: u8 = 16; + +var textureOnlyCache: std.StringHashMapUnmanaged(ModelIndex) = .{}; +var modelRepositionCache: std.StringHashMapUnmanaged(ModelIndex) = .{}; + +const MultiModelKey = struct { + modelId: [maxStateCount][]const u8, + + fn initFromZon(zon: ZonElement) MultiModelKey { + var self: MultiModelKey = .{ .modelId = undefined }; + for (0..maxStateCount) |index| { + self.modelId[index] = zon.getAtIndex([]const u8, index) orelse "cubyz:cube"; + } + return self; + } + + const Context = struct { + pub fn hash(_: Context, val: MultiModelKey) u64 { + var hasher = std.hash.Wyhash.init(0); + for (val.modelId) |modelId| { + hasher.update(modelId); + } + return hasher.final(); + } + pub fn eql(_: Context, val1: MultiModelKey, val2: MultiModelKey) bool { + for (0..maxStateCount) |index| { + if (!std.mem.eql(u8, val1.modelId[index], val2.modelId[index])) return false; + } + return true; + } + }; +}; +const MultiModelHashMap = std.HashMapUnmanaged(MultiModelKey, ModelIndex, MultiModelKey.Context, std.hash_map.default_max_load_percentage); + +var modelOnlyCache: MultiModelHashMap = .{}; +var textureAndModelCache: MultiModelHashMap = .{}; pub fn init() void { - rotatedModels = .init(main.globalAllocator.allocator); + textureOnlyCache = .empty; + modelOnlyCache = .empty; + modelRepositionCache = .empty; + textureAndModelCache = .empty; } pub fn deinit() void { - rotatedModels.deinit(); + textureOnlyCache.deinit(main.globalAllocator.allocator); + modelOnlyCache.deinit(main.globalAllocator.allocator); + modelRepositionCache.deinit(main.globalAllocator.allocator); + textureAndModelCache.deinit(main.globalAllocator.allocator); } pub fn reset() void { - rotatedModels.clearRetainingCapacity(); + textureOnlyCache.clearRetainingCapacity(); + modelOnlyCache.clearRetainingCapacity(); + modelRepositionCache.clearRetainingCapacity(); + textureAndModelCache.clearRetainingCapacity(); } -fn transform(quad: *main.models.QuadInfo, data: u16) void { - quad.textureSlot = data%16; -} +const PileMode = enum(u8) { + /// Use single model, change texture with every state. + textureOnly = 0, + /// Use one texture, change model with every state. + modelOnly = 1, + /// Use one texture and one model, but translate copies of the model. Copies use same texture. + modelReposition = 2, + /// Use different texture and model pair for every state. + textureAndModel = 3, +}; pub fn createBlockModel(block: Block, modeData: *u16, zon: ZonElement) ModelIndex { - const modelId = zon.get([]const u8, "model") orelse "cubyz:cube"; - const stateCount = zon.get(u16, "states") orelse 2; - const blockId = block.id(); - if (stateCount <= 1) { - std.log.err("Block '{s}' uses texture pile with {} states. 'texturePile' should have at least 2 states, use 'no_rotation' instead", .{blockId, stateCount}); - } else if (stateCount > 16) { - std.log.err("Block '{s}' uses texture pile with {} states. 'texturePile' can have at most 16 states.", .{blockId, stateCount}); + const stateCount = zon.get(u8, "states") orelse 2; + if (stateCount < minStateCount) { + std.log.err("Block '{s}' uses texture pile with {} states. 'cubyz:texture_pile' should have at least {} states, use 'no_rotation' instead", .{block.id(), stateCount, minStateCount}); + } else if (stateCount > maxStateCount) { + std.log.err("Block '{s}' uses texture pile with {} states. 'cubyz:texture_pile' can have at most {} states.", .{block.id(), stateCount, maxStateCount}); } - modeData.* = stateCount; + modeData.* = @min(stateCount, maxStateCount); + + const mode = zon.get(PileMode, "mode") orelse .textureOnly; + const modelIndex = switch (mode) { + .textureOnly => return createModelModeTextureOnly(block, zon), + .modelOnly => return createModelModeOnly(block, zon), + .modelReposition => return createModelModeModelReposition(block, zon), + .textureAndModel => return createModelModeTextureAndModel(block, zon), + }; + return modelIndex; +} + +fn createModelModeTextureOnly(_: Block, zon: ZonElement) ModelIndex { + const modelId = zon.get([]const u8, "model") orelse "cubyz:cube"; - if (rotatedModels.get(modelId)) |modelIndex| return modelIndex; + if (textureOnlyCache.get(modelId)) |modelIndex| return modelIndex; const baseModel = main.models.getModelIndex(modelId).model(); - const modelIndex = baseModel.transformModel(transform, .{@as(u16, @intCast(0))}); - for (1..16) |data| { - _ = baseModel.transformModel(transform, .{@as(u16, @intCast(data))}); + const transform = struct { + fn transform(quad: *main.models.QuadInfo, data: u16) void { + quad.textureSlot = data%maxStateCount; + } + }; + + const modelIndex = baseModel.transformModel(transform.transform, .{@as(u16, @intCast(0))}); + for (1..maxStateCount) |data| { + _ = baseModel.transformModel(transform.transform, .{@as(u16, @intCast(data))}); } - rotatedModels.put(modelId, modelIndex) catch unreachable; + textureOnlyCache.put(main.globalAllocator.allocator, modelId, modelIndex) catch unreachable; return modelIndex; } + +fn getRequiredModelsField(mode: PileMode, block: Block, zon: ZonElement) ZonElement { + const modelIdField = zon.getChild("models"); + if (modelIdField == .null) { + std.log.err("Block {s} uses 'cubyz:texture_pile' with mode '{s}' but has no 'models' field.", .{block.id(), @tagName(mode)}); + return .null; + } + if (modelIdField != .array) { + std.log.err("Block {s} uses 'cubyz:texture_pile' with mode '{s}' but 'models' field is not an array.", .{block.id(), @tagName(mode)}); + return .null; + } + return modelIdField; +} + +fn createModelModeOnly(block: Block, zon: ZonElement) ModelIndex { + const modelIdsArray = getRequiredModelsField(.modelOnly, block, zon); + + const cacheKey: MultiModelKey = .initFromZon(modelIdsArray); + if (modelOnlyCache.get(cacheKey)) |modelIndex| return modelIndex; + + var firstModelIndex: ?ModelIndex = null; + for (0..maxStateCount) |index| { + const modelId = cacheKey.modelId[index] ; + + const baseModel = main.models.getModelIndex(modelId).model(); + const modelIndex = baseModel.dupeModel(); + if (firstModelIndex == null) firstModelIndex = modelIndex; + } + modelOnlyCache.put(main.globalAllocator.allocator, cacheKey, firstModelIndex.?) catch unreachable; + return firstModelIndex.?; +} + +fn createModelModeModelReposition(block: Block, zon: ZonElement) ModelIndex { + const stateCount = zon.get(u8, "states") orelse 2; + const modelId = zon.get([]const u8, "model") orelse "cubyz:cube"; + const offsets = zon.getChild("offsets"); + if (offsets == .null) { + std.log.err("Block {s} uses 'cubyz:texture_pile' with mode 'modelReposition' but has no 'offsets' field.", .{block.id()}); + } + if (offsets != .array) { + std.log.err("Block {s} uses 'cubyz:texture_pile' with mode 'modelReposition' but 'offsets' field is not an array.", .{block.id()}); + } + + // if (modelRepositionCache.get(modelId)) |modelIndex| return modelIndex; + + const baseModel = main.models.getModelIndex(modelId).model(); + + var firstModelIndex: ?ModelIndex = null; + for (0..maxStateCount) |data| { + var modelIndex: ModelIndex = undefined; + + if (data >= stateCount) { + modelIndex = main.models.getModelIndex("cubyz:cube").model().dupeModel(); + } else { + const stateOffsets = offsets.getAtIndex(ZonElement, data) orelse .null; + if (stateOffsets != .array) { + std.log.err("Block {s} invalid offset at index {} expected array got {s}.", .{block.id(), data, @tagName(stateOffsets)}); + } + + var transformedModels: main.List(ModelIndex) = .initCapacity(main.stackAllocator, @as(usize, data + 1)); + defer transformedModels.deinit(main.stackAllocator); + + const transform = struct { + fn transform(quad: *main.models.QuadInfo, transformMatrix: vec.Mat4f) void { + for (&quad.corners) |*corner| { + corner.* = vec.xyz(vec.Mat4f.mulVec(transformMatrix, vec.combine(corner.*, 1))); + } + } + }; + + for (0..data + 1) |i| { + std.log.debug("Block {s} modelReposition state {} i {}", .{block.id(), data, i}); + const offset = stateOffsets.getAtIndex(ZonElement, i) orelse .null; + if (offset != .array) { + std.log.err("Block {s} invalid offset at index {} for state {}.", .{block.id(), i, data}); + } + const x = offset.getAtIndex(f32, 0) orelse 0.0; + const y = offset.getAtIndex(f32, 1) orelse 0.0; + const z = offset.getAtIndex(f32, 2) orelse 0.0; + + const transformedModel = baseModel.transformModel(transform.transform, .{vec.Mat4f.translation(.{x, y, z})}); + transformedModels.append(main.stackAllocator, transformedModel); + } + modelIndex = main.models.Model.mergeModels(transformedModels.items); + } + + if (firstModelIndex == null) firstModelIndex = modelIndex; + } + // modelRepositionCache.put(main.globalAllocator.allocator, modelId, firstModelIndex.?) catch unreachable; + return firstModelIndex.?; +} + +fn createModelModeTextureAndModel(block: Block, zon: ZonElement) ModelIndex { + const modelIdsField = getRequiredModelsField(.textureAndModel, block, zon); + + var cacheKey: MultiModelKey = .{ .modelId = undefined }; + for (0..maxStateCount) |index| { + cacheKey.modelId[index] = modelIdsField.getAtIndex([]const u8, index) orelse "cubyz:cube"; + } + if (textureAndModelCache.get(cacheKey)) |modelIndex| return modelIndex; + + var firstModelIndex: ?ModelIndex = null; + for (0..maxStateCount) |index| { + const modelId = cacheKey.modelId[index]; + + const baseModel = main.models.getModelIndex(modelId).model(); + + const transform = struct { + fn transform(quad: *main.models.QuadInfo, data: u16) void { + quad.textureSlot = data%maxStateCount; + } + }; + + const modelIndex = baseModel.transformModel(transform.transform, .{@as(u16, @intCast(index))}); + if (firstModelIndex == null) firstModelIndex = modelIndex; + } + textureAndModelCache.put(main.globalAllocator.allocator, cacheKey, firstModelIndex.?) catch unreachable; + return firstModelIndex.?; +} + pub fn model(block: Block) ModelIndex { return blocks.meshes.modelIndexStart(block).add(@min(block.data, block.modeData() - 1)); } diff --git a/src/items.zig b/src/items.zig index 1be05698df..951884bb9f 100644 --- a/src/items.zig +++ b/src/items.zig @@ -930,7 +930,7 @@ pub const ProceduralItem = struct { // MARK: ProceduralItem fn extractItemsFromZon(zonArray: ZonElement) [craftingGridSize]?BaseItemIndex { var items: [craftingGridSize]?BaseItemIndex = undefined; for (&items, 0..) |*item, i| { - item.* = .fromId(zonArray.getAtIndex([]const u8, i, "null")); + item.* = .fromId(zonArray.getAtIndex([]const u8, i) orelse "null"); if (item.* != null and item.*.?.material() == null) item.* = null; } return items; @@ -1502,7 +1502,7 @@ pub fn registerProceduralItem(assetFolder: []const u8, id: []const u8, zon: ZonE const matrixZon = paramZon.getChild("matrix"); var total_weight: f32 = 0.0; for (0..25) |i| { - val.weights[i] = matrixZon.getAtIndex(f32, i, 0.0); + val.weights[i] = matrixZon.getAtIndex(f32, i) orelse 0.0; } for (0..25) |i| { total_weight += val.weights[i]; diff --git a/src/models.zig b/src/models.zig index c60460625e..7db4cf9098 100644 --- a/src/models.zig +++ b/src/models.zig @@ -638,6 +638,13 @@ pub const Model = struct { return Model.init(quadList.items); } + pub fn dupeModel(model: Model) ModelIndex { + var quadList = main.ListManaged(QuadInfo).init(main.stackAllocator); + defer quadList.deinit(); + model.getRawFaces(&quadList); + return Model.init(quadList.items); + } + fn appendQuadsToList(quadList: []const QuadIndex, list: *main.ListManaged(FaceData), block: main.blocks.Block, pos: main.chunk.BlockPos, comptime backFace: bool) void { for (quadList) |quadIndex| { const texture = main.blocks.meshes.textureIndex(block, quadIndex.quadInfo().textureSlot); diff --git a/src/zon.zig b/src/zon.zig index 2e569614bf..3a7aa14c97 100644 --- a/src/zon.zig +++ b/src/zon.zig @@ -27,14 +27,14 @@ pub const ZonElement = union(enum) { // MARK: Zon return .{.array = list}; } - pub fn getAtIndex(self: *const ZonElement, comptime T: type, index: usize, replacement: T) T { + pub fn getAtIndex(self: *const ZonElement, comptime T: type, index: usize) ?T { if (self.* != .array) { - return replacement; + return null; } else { if (index < self.array.items.len) { - return self.array.items[index].as(T) orelse replacement; + return self.array.items[index].as(T); } else { - return replacement; + return null; } } } @@ -192,6 +192,7 @@ pub const ZonElement = union(enum) { // MARK: Zon else => return null, } }, + ZonElement => return self.*, else => { @compileError("Unsupported type '" ++ @typeName(T) ++ "'."); },