diff --git a/benchmarks/rust-native-chunk-models-2026-10-06/startup-smoke.json b/benchmarks/rust-native-chunk-models-2026-10-06/startup-smoke.json new file mode 100644 index 000000000..38986e175 --- /dev/null +++ b/benchmarks/rust-native-chunk-models-2026-10-06/startup-smoke.json @@ -0,0 +1,676 @@ +{ + "adapter": { + "backend": "Vulkan", + "device": 0, + "device_type": "Cpu", + "name": "llvmpipe (LLVM 21.1.8, 256 bits)", + "vendor": 65541 + }, + "animated_sprites": 1, + "animation_uploads": 1, + "arch": "x86_64", + "camera_pitch": -0.3839724354387525, + "camera_position": [ + 25.0, + 18.0, + 25.0 + ], + "camera_yaw": -2.356194490192345, + "culled_model_faces": 59636, + "elapsed_seconds": 0.674486099, + "error": null, + "merged_quads": 3169, + "model_blocks": 10919, + "model_gallery": [ + { + "block": "minecraft:oak_stairs", + "models": [ + { + "model": "minecraft:block/oak_stairs_inner", + "rotations": [ + 2, + 3, + 0 + ], + "uv_lock": true + } + ], + "position": [ + -7, + 4, + 14 + ], + "properties": { + "facing": "north", + "half": "top", + "shape": "inner_left", + "waterlogged": "false" + }, + "state_id": 5466 + }, + { + "block": "minecraft:stone_slab", + "models": [ + { + "model": "minecraft:block/stone_slab_top", + "rotations": [ + 0, + 0, + 0 + ], + "uv_lock": false + } + ], + "position": [ + -3, + 4, + 14 + ], + "properties": { + "type": "top", + "waterlogged": "false" + }, + "state_id": 15250 + }, + { + "block": "minecraft:anvil", + "models": [ + { + "model": "minecraft:block/anvil", + "rotations": [ + 0, + 3, + 0 + ], + "uv_lock": false + } + ], + "position": [ + 1, + 4, + 14 + ], + "properties": { + "facing": "east" + }, + "state_id": 12965 + }, + { + "block": "minecraft:torch", + "models": [ + { + "model": "minecraft:block/torch", + "rotations": [ + 0, + 0, + 0 + ], + "uv_lock": false + } + ], + "position": [ + 5, + 4, + 14 + ], + "properties": {}, + "state_id": 4926 + }, + { + "block": "minecraft:lantern", + "models": [ + { + "model": "minecraft:block/lantern", + "rotations": [ + 0, + 0, + 0 + ], + "uv_lock": false + } + ], + "position": [ + -7, + 4, + 18 + ], + "properties": { + "hanging": "false", + "waterlogged": "false" + }, + "state_id": 24197 + }, + { + "block": "minecraft:oak_fence", + "models": [ + { + "model": "minecraft:block/oak_fence_post", + "rotations": [ + 0, + 0, + 0 + ], + "uv_lock": false + }, + { + "model": "minecraft:block/oak_fence_side", + "rotations": [ + 0, + 0, + 0 + ], + "uv_lock": true + }, + { + "model": "minecraft:block/oak_fence_side", + "rotations": [ + 0, + 1, + 0 + ], + "uv_lock": true + } + ], + "position": [ + -3, + 4, + 18 + ], + "properties": { + "east": "true", + "north": "true", + "south": "false", + "waterlogged": "false", + "west": "false" + }, + "state_id": 8642 + }, + { + "block": "minecraft:oak_fence_gate", + "models": [ + { + "model": "minecraft:block/oak_fence_gate_open", + "rotations": [ + 0, + 1, + 0 + ], + "uv_lock": true + } + ], + "position": [ + 1, + 4, + 18 + ], + "properties": { + "facing": "west", + "in_wall": "false", + "open": "true", + "powered": "false" + }, + "state_id": 10401 + }, + { + "block": "minecraft:iron_bars", + "models": [ + { + "model": "minecraft:block/iron_bars_post_ends", + "rotations": [ + 0, + 0, + 0 + ], + "uv_lock": false + }, + { + "model": "minecraft:block/iron_bars_side", + "rotations": [ + 0, + 0, + 0 + ], + "uv_lock": false + }, + { + "model": "minecraft:block/iron_bars_side_alt", + "rotations": [ + 0, + 0, + 0 + ], + "uv_lock": false + } + ], + "position": [ + 5, + 4, + 18 + ], + "properties": { + "east": "false", + "north": "true", + "south": "true", + "waterlogged": "false", + "west": "false" + }, + "state_id": 9711 + }, + { + "block": "minecraft:cobweb", + "models": [ + { + "model": "minecraft:block/cobweb", + "rotations": [ + 0, + 0, + 0 + ], + "uv_lock": false + } + ], + "position": [ + -7, + 4, + 22 + ], + "properties": {}, + "state_id": 2362 + }, + { + "block": "minecraft:grindstone", + "models": [ + { + "model": "minecraft:block/grindstone", + "rotations": [ + 1, + 3, + 0 + ], + "uv_lock": false + } + ], + "position": [ + -3, + 4, + 22 + ], + "properties": { + "face": "wall", + "facing": "west" + }, + "state_id": 24135 + }, + { + "block": "minecraft:ladder", + "models": [ + { + "model": "minecraft:block/ladder", + "rotations": [ + 0, + 3, + 0 + ], + "uv_lock": false + } + ], + "position": [ + 1, + 4, + 22 + ], + "properties": { + "facing": "west", + "waterlogged": "false" + }, + "state_id": 7312 + }, + { + "block": "minecraft:amethyst_cluster", + "models": [ + { + "model": "minecraft:block/amethyst_cluster", + "rotations": [ + 1, + 1, + 0 + ], + "uv_lock": false + } + ], + "position": [ + 5, + 4, + 22 + ], + "properties": { + "facing": "east", + "waterlogged": "false" + }, + "state_id": 26764 + }, + { + "block": "minecraft:stone", + "models": [ + { + "model": "minecraft:block/stone", + "rotations": [ + 0, + 2, + 0 + ], + "uv_lock": false + } + ], + "position": [ + -7, + 4, + 26 + ], + "properties": {}, + "state_id": 1 + }, + { + "block": "minecraft:stone", + "models": [ + { + "model": "minecraft:block/stone_mirrored", + "rotations": [ + 0, + 2, + 0 + ], + "uv_lock": false + } + ], + "position": [ + -3, + 4, + 26 + ], + "properties": {}, + "state_id": 1 + }, + { + "block": "minecraft:stone", + "models": [ + { + "model": "minecraft:block/stone", + "rotations": [ + 0, + 0, + 0 + ], + "uv_lock": false + } + ], + "position": [ + 1, + 4, + 26 + ], + "properties": {}, + "state_id": 1 + }, + { + "block": "minecraft:stone", + "models": [ + { + "model": "minecraft:block/stone", + "rotations": [ + 0, + 2, + 0 + ], + "uv_lock": false + } + ], + "position": [ + 5, + 4, + 26 + ], + "properties": {}, + "state_id": 1 + } + ], + "model_quads": 179, + "os": "linux", + "presented_frames": 30, + "project": "Horizon", + "resident_chunks": 9, + "schema": 1, + "scope": "Native cube and cuboid-model renderer with binary cutouts, stepped sprite animation and free camera; no gameplay simulation", + "sprite_layers": 18, + "visible_model_faces": 5991, + "window_size": [ + 1280, + 720 + ], + "native_scene_culling": { + "blocks": 10919, + "culled_faces": 59636, + "visible_faces": 5991 + }, + "native_selection": { + "matched_placements": 16, + "selected_parts": 20, + "client_jar_sha256": "4508d006323f24fa02876310c192d739af56516eb259000ac50f0909a68c9a2d", + "output_sha256": "a2ff1c890d6f3a5292c3a9402f02ca5404a993dc117853b25cbe64ac92b76d30", + "scope": "Actual native registry states, blockstate dispatch, tagged root baking and legacy collection using state.getSeed(position). All 16 featured placements match model names/transforms. An independently constructed native scene also matches total occupied blocks and visible/culled model faces using native parent resolution, rotated culling directions and Block.shouldRenderFace. No shader/pixel parity claim." + }, + "resource_files": { + "files": 5582, + "sha256": "9b03385cb17f8c75a9031e05c0524c2b50606e149857a44a398fe353a830f756" + }, + "executable_sha256": "6c3bed79f13b82275f8164ce2c98feb11fdf8718be59c90b7bbd139f52f7703b", + "resource_manifest_sha256": "f25499f7b4897feee0f097c065b65541e932611e4de6817210db527532d36c14", + "sources": { + "Cargo.toml": "3f93319c1eb70dbd7ba25cd74ef343fbf35c52c7c4752f955605c58699cbff9d", + "Cargo.lock": "cb1e87d449114a63edd904c0559d631e8fa9feeb27efd6539aacdcfeae178876", + "tools/test_native_client.py": "95b12bdb90702a5e2253119543f5430fa7a344e0dc5ad312354325feb97c2e79", + "crates/horizon-client/Cargo.toml": "d56524457d761c2613c0699b20e21ad6977d93e009d59ef150bbd9fc20d7a025", + "crates/horizon-assets/Cargo.toml": "2f2cffacec0b4e22dd7cb032d594289bc51c94ae6cc4e985fee3e310f441d751", + "crates/horizon-client/src/assets.rs": "e595afa610e0425d0b3ad694218794bf9e680e46ae80f3925c278ef1a823c914", + "crates/horizon-client/src/camera.rs": "f9821c8a51a004dd4ad9c0952e746eb07926f6040cf8660bc26da5a0728ae572", + "crates/horizon-client/src/main.rs": "e74045bc8f882b55d1861ff87700e517766645596d02dd72f2e16e8165d5b6e5", + "crates/horizon-client/src/mesh.rs": "4ac8d85b30f599b37a98b92adcc016b16434816b93c401bd39b8215f591f9330", + "crates/horizon-client/src/models/catalog.rs": "620103719167fc9ab76e730d68eeacb50014e95b7ad209ae7adf2cbbe69f70f7", + "crates/horizon-client/src/models/cube.rs": "1067229f9dd9b276950bf7ec0c5f67061ce3d40a21932dca8034b9748e7439a4", + "crates/horizon-client/src/models.rs": "98dd3b969947c2b297862a934e6235067ddc1fc8cda5adcc6276d19014d4b048", + "crates/horizon-client/src/render.rs": "db17511223d0b72cf13bbb1bd7cbfca119d249f1d8914e7c1c06a710494c5123", + "crates/horizon-client/src/sprites.rs": "292f844a31f6e8884008df9630e2b8862db1acfbc4640cc07d526e25f21f0bc5", + "crates/horizon-client/src/world.rs": "17bd7127283c5ea27166e0e8079e8c284c6cff426a2fa8994879e48151e1ded4", + "crates/horizon-client/src/world.wgsl": "972a58ccb34e9965515adbb30c47a93fceca5b742390db6d9f69cba1ec31f559", + "crates/horizon-assets/src/animation.rs": "ebbf7952146a0e66b41396174492f5702627170b03a57e6cd5a45c97a08c975e", + "crates/horizon-assets/src/bake/transform.rs": "a95e658fef85b21bcb2f9f7512f98f98d57022d62c1199885b7133beae35a09d", + "crates/horizon-assets/src/bake.rs": "f55dda6b6cee5ac6e65a8ee4837fdd2c6674437d59e59b4c45718f6324d4a4d5", + "crates/horizon-assets/src/blockstate/bind.rs": "6278962e55c9786bba8184bdf2939af9c6e3b9002b08c3022129964b49a60009", + "crates/horizon-assets/src/blockstate/choice.rs": "5a66c8b9833e2f19dd8b0773f3593d97ff5941d3c5b773896b98049efa1395ba", + "crates/horizon-assets/src/blockstate.rs": "a4299dfd7adaeb4173982ffaa8e9c9eda2925739ae579a714193c99da2e7ce56", + "crates/horizon-assets/src/json.rs": "99fe8af4fbf5d4eed408a92e829ac1a39597a0d92471a6bff73771fad9c830a8", + "crates/horizon-assets/src/lib.rs": "e39f21c59a2c3e1c53d977b29bd5e0638e5fc41d86ff0fcc1882a6d90fd961e4", + "crates/horizon-assets/src/model.rs": "57ddb3d65e88e7a8aa22c62bc298a2dae7f4e994326a873a31f06c0f36ed051b", + "crates/horizon-assets/src/resolve.rs": "3927ac926cdcbd7d334901cc32d0ecc1e498b1bec98f76a46bca6d4964c13541", + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaGallerySelectionOracle.java": "dc5433f1ce1de629b30cb70f031a43a691fc1df1fb218ce23de89769c164d562", + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaCuboidModelOracle.java": "7526f20513c549483122ab0d5ad24b9e87f38dc2f6aae55addd701a91ac85e43", + "mods/horizon/src/test/java/dev/horizonlab/parity/VanillaHeightmapOracle.java": "145c943637ed177cc4ef8a85f2484da8afd164868e84a8463882feec1653a860", + "tools/test_model_baking_parity.py": "392ffec44918fb91740ca4a1316d684480d39f14bd34d46175f7f44946d15bd7", + "tools/test_rust_parity.py": "29d3db08005873528894b45ca10dcd6278753650473b3eb574e53e06ede40ec6", + "tools/lab.py": "d00bf107fa01b6c579734a082c3ca4ade3018f0d6851d46844acf17574f66e64", + "tools/prepare_native_client.py": "712c1de220bea609621b376cdff04e65fa2102dbb214604d556cff7511fb9f7c", + "crates/horizon-core/Cargo.toml": "e21b502c949da93cf67826592459b687a8cbf737ef92fee514e3ed2cc7cd27ad", + "crates/horizon-core/src/bin/density_parity.rs": "e5b1dd33b7d01306c73cf072629b4fb186e9ab87b672a00108999d1a72263fd0", + "crates/horizon-core/src/bin/float_log_parity.rs": "4a655fcf8822ee7668c4bd126bc84d0b118864a6fc7f5a6aeee667d8bf2590b1", + "crates/horizon-core/src/bin/geometry_parity.rs": "27f6c09fe5018ac3de8e70e0f0bbf78f9abb14f5e4783bcaf46e8ca87b9c6f8a", + "crates/horizon-core/src/bin/noise_parity.rs": "c135bfc708d853e7061e81487d2cfe10de64b2f65677d3b4837cbfe0b8341010", + "crates/horizon-core/src/bin/parity.rs": "f35d928c66f958de39fac5e7e836a657eed95f83599e84899f7ea7a5802ba481", + "crates/horizon-core/src/bin/tick_parity.rs": "a54c164fe4814034d369947a887f5273966f5a878ba849204b80d1c729526a8f", + "crates/horizon-core/src/biome/seed_hash.rs": "5afbb4921c750826b395a08bf06f382fc7f01b5759a93de02a2749c523e2a317", + "crates/horizon-core/src/biome.rs": "908efa8cd4aa0a7adf1cb5e1a222e3dfb640eed5e9172e0fe7f0d1287afae7e8", + "crates/horizon-core/src/carving/ellipsoid.rs": "09232a7588610e8a9176fd9c62d40afb1fb89f2ecab6d555536a1d847b142d86", + "crates/horizon-core/src/carving.rs": "0aa3093caa0437868a8c0ed52a6c6e1993bd51c03583c670a2837e749eae5426", + "crates/horizon-core/src/density/arithmetic.rs": "372faec96c923e2cf4aa6c66c1ac398e676853274557823850dbc8933dd15ab1", + "crates/horizon-core/src/density/beardifier.rs": "bb1052e27dae4a01954ca78d7d5babafef85f3b2c704ccaf1920cb85a098cb50", + "crates/horizon-core/src/density/cache.rs": "38d0b6c9dd04ffef979fdad3f29cfebf98d331c8c9d430be5f39aa34f6c30975", + "crates/horizon-core/src/density/end_islands.rs": "168b9384237bd2e41d0ae5a74e944da4dc74e493bdec31fb73184c9a49e3de55", + "crates/horizon-core/src/density/graph/compiler.rs": "ac67deeb16f16d0859aa20e5a165811a29aca0b910037ee6bf5098211f3a6ede", + "crates/horizon-core/src/density/graph/mod.rs": "b776e7da934cb03051a3053d56d461c71e9978e084922fe4194147aa580c07bf", + "crates/horizon-core/src/density/graph/seeded.rs": "6bcdcaa3dffcfa9df673bbbea35120057a16ba3d2b506c52709c36cf8976b74a", + "crates/horizon-core/src/density/graph/tests.rs": "6bfb8cbff462587dd147b0775ea01d370abbd97544b9bd596c792df1b5f7e634", + "crates/horizon-core/src/density/interpolation.rs": "8c890f52b6467f38a94c654986a6a484b8e0b296ab98c398e60a6ce465b5b010", + "crates/horizon-core/src/density/interval.rs": "b42629163baa6b599a233afe8596d1c370cd81d015cc6fc36a9663aa47b98424", + "crates/horizon-core/src/density/mod.rs": "ca0d77d3282e3746ddcbc92bee97dbf640b9a56214e6ac1537f51f9a5b63d6e7", + "crates/horizon-core/src/density/power.rs": "d5b7270a128d4264268cd772109ad16d71a8ec3ff2d3b63b84cb985296d54a91", + "crates/horizon-core/src/density/rounding.rs": "71840c5206ff8d218ac29bf6f66915d5212764ba270c675711a866ba5b55a7a6", + "crates/horizon-core/src/density/selection.rs": "0dd26af2e85004ec5e053039ad11c384c17c56ace9ab45eb3f5cb3503c0b0330", + "crates/horizon-core/src/density/slice.rs": "5d77605573c0deda47778f1f78f7fa3fbf1944d8f5efbcc65cf589181cadf80b", + "crates/horizon-core/src/density/spline.rs": "6bb2e40f940fc6544d6fb09cb0b8cac722be03bc663dbdfb0f61faae207400bc", + "crates/horizon-core/src/density/surface.rs": "159e0e69cda74f870e80ea64fe523c4397e64af4ccb861bb9fd07b2c3cb91b00", + "crates/horizon-core/src/density/terrain_noise.rs": "8d0059d25eb5d248cb97a4c5a2e670d61528928ea151495c004f25adb723fe3e", + "crates/horizon-core/src/density/tests.rs": "7817796ee40d0ae241bfbebec6bca190e0a8e7d5a1caa8ddf858c3eaa58b8858", + "crates/horizon-core/src/density/world_fields.rs": "c1ce305cb8046b384d90c564cafb24af7365d580389637910f3d8b0cc18eb583", + "crates/horizon-core/src/geometry/mod.rs": "dc92f73885dd0a7760743bf74f1ab8afa839d61cef2dfef890f8a23b67b4742e", + "crates/horizon-core/src/geometry/movement.rs": "930e34abef6f6eeec1ee7a6c920b5ba01bfe37b90678884490f4be53241f6b96", + "crates/horizon-core/src/geometry/shape/create.rs": "189812b91d5e0e3ad963afcd1a6e2831b31b8188df91a904ef27061e644806ad", + "crates/horizon-core/src/geometry/shape/face.rs": "f243bce81384040a95ce7984b02b5699206c596c537b2713097e86005b4d3a5c", + "crates/horizon-core/src/geometry/shape/join.rs": "f1b51887b7bf9482557e3ffa99e136f2e5db07ceeb05fdb116adf45e87129075", + "crates/horizon-core/src/geometry/shape.rs": "e8d88a763de1525f77a30b99591641e8a8b80946263acbde035a9bcc356df960", + "crates/horizon-core/src/geometry/sweep.rs": "abd6009c61555161ef2f18b431b2aa384b126c0871cae8b96a62358498a56a1d", + "crates/horizon-core/src/geometry/traversal.rs": "77a3da570a0b85582b669cf4d841cba4b06327006dda26d4a26df4e8ce05bae4", + "crates/horizon-core/src/geometry/trig.rs": "05cef9341a298a474221a0556a11ddf47650393ea7a97b4b2022a4da6628fe37", + "crates/horizon-core/src/lib.rs": "4255e16837fde42a52ba118f60581d197c571bb530b60d635e506184ebf4db0b", + "crates/horizon-core/src/noise/mod.rs": "1dab5a713abf702402e69ace1bf9e476bba544b0ede5a61c3c3afc60af0a03f3", + "crates/horizon-core/src/noise/normal.rs": "e9e7a46472a54a02c98c46da819842090567153b32fe0c84249009ef19af7aad", + "crates/horizon-core/src/noise/perlin.rs": "e73815447432eab7542f67366cb3f1b368d164e33f6c9cdbd045ac8e4db0932a", + "crates/horizon-core/src/noise/simplex.rs": "6c0dbc15b2b7510195dd28fceca01dcaac1847ccc4a84d299c351589c642fe7a", + "crates/horizon-core/src/noise/stack.rs": "7f6ef0e7d920139ef1a6ad3ec5640f92765715f86dcc5ec29f1a149984f45fa3", + "crates/horizon-core/src/noise/volume.rs": "3f05d2b1780acfdfee60037677d309937a5033b45ef428fc2d540942d4b56b43", + "crates/horizon-core/src/position/iter.rs": "32d2d8e74821cc4e49e3354202d35d3120e4415bfdda9375a73f2137ef651129", + "crates/horizon-core/src/position.rs": "e10101088e6902386b9b6d27773d44312f1ff72bebb19e57967a54945dde9dcd", + "crates/horizon-core/src/random/hash.rs": "0f88a127a1ba2705d3d6c0ebd009324d6ba86ffa2d27edf2d2a84765e690e4a3", + "crates/horizon-core/src/random/positional.rs": "35494745e5d4cf89e282c3ae0ee245e9f2640ddbbc03bc0055df742006a9c862", + "crates/horizon-core/src/random/worldgen.rs": "9f07ff33bcc038d34e27aa5465753bb21df18f2975e43cae201f709bc186945a", + "crates/horizon-core/src/random.rs": "ddb95d3a83e28ad605ff1f49f5df270cc543ca5ef1130e37d989dc74c8d861e6", + "crates/horizon-core/src/storage.rs": "f6c32201c64aefde06e57a2c767be46918df613548a322801546b133799cc33d", + "crates/horizon-core/src/text.rs": "e6553ddb5db5039e8dc4614b6a6dc9f35a26dc3d1673b68a04e72e7983b67abb", + "crates/horizon-core/src/ticks/generation.rs": "be1237e790693f633a61714ddae68c6506a777588a8a3ab5efbe53a35774dbd2", + "crates/horizon-core/src/ticks/heap.rs": "bf28afab8af77c3d243aa96cd8a1fb8d934140a4dbd07dbfdbf4e7908bb4b845", + "crates/horizon-core/src/ticks/index.rs": "1f1a6a568c2a30eafecb020ca4d5f5fc414860de85b8b381fc419a2e3e65c24a", + "crates/horizon-core/src/ticks/mod.rs": "01634630668172516b8b79d0907bad47f6e2c543a6d499c2fa4b201823b375cb", + "crates/horizon-world/Cargo.toml": "977338bf3e654193685b015d383493b9f59ff6f5d6210bec212cef8ac4a50bed", + "crates/horizon-world/src/bin/worldgen_parity.rs": "3143ceb9b56eecfd7667e5feb5f9f0cd13cfb8754b38d4ea338dbba97f2819da", + "crates/horizon-world/src/block_collision.rs": "cec2af81d212d5eed7a2196e4cf0be2daf4cecf53248f78cc68f2190e4d83f9a", + "crates/horizon-world/src/block_entities.rs": "a9deae92e6e5a1fcf4f0476c33ea4d170ddad6fd4b3303e1bb66d933973d5e06", + "crates/horizon-world/src/block_placement.rs": "524f4892d50bab77b7d89d19f7b048ffe746996f2ed7f376db2c6ab00ed75c24", + "crates/horizon-world/src/block_properties.rs": "b0bd8a983336c48e488a20caa77dd5c39b1a74bbd14ebc03b3b64556a1c864fc", + "crates/horizon-world/src/block_render.rs": "8df52ee11aef406a1571198a64048062a0ef6864e0058d9293a57f64b73c916a", + "crates/horizon-world/src/block_support.rs": "ade4fbd47284b450dabdd682dc86138b2ac05a0fd379525d7110648908245476", + "crates/horizon-world/src/block_survival.rs": "28ce64375e09819a4f13da9cfbe37c08b5ad0daf01b289c1c360b4567e50e811", + "crates/horizon-world/src/chunk.rs": "8843e6f48dbbc4245ce4e95564dfba3a4d7a051ba0b1577fb038c07adb0ff0f9", + "crates/horizon-world/src/chunk_status.rs": "dbb8583cfa775203c544d24d69e03720385428f15a267a561356c21f5025e9c7", + "crates/horizon-world/src/collision.rs": "05313e0c1f3089524fe029f9aac3e439cd47f1c9584c8c7e154c9adcfa88c335", + "crates/horizon-world/src/compression.rs": "4db73e6ca9a0d552d415ce60e7e88b89c9cf2b7d060a669ff8328f518316e101", + "crates/horizon-world/src/dynamic_support.rs": "e799b31e5120cfdb04e0b342f2097d610ed98ef8c4be031620ee00368d82df59", + "crates/horizon-world/src/entity_obstruction.rs": "a4b28a19bf83b23611c59d94535c488db37e4aee24b22c973c545910894a809f", + "crates/horizon-world/src/fluid.rs": "a0a1f34ba94d8b59dc975becee8952960764d67ae08cf2d6a8bcd40a614b15c4", + "crates/horizon-world/src/heightmap.rs": "a5edef082b5af40d814421ea17e129b3b06d70abee6434ed263e4bba58571698", + "crates/horizon-world/src/lib.rs": "9f254668a237fc2d28a599b0ee6f1eac57cdafa2fd1af67e6a2303ee98fdd5ed", + "crates/horizon-world/src/main.rs": "fce57418b8c22a6a0a9044baa89ed3d9c243bc2e8413fea3ff0f32985daba021", + "crates/horizon-world/src/nbt.rs": "60262c3de8c8f68bc2da0370ccb17393d32b85f5f5022c6358599a7d32119ea6", + "crates/horizon-world/src/piston_shape.rs": "399c2337103b8e7e0eb5667227c5aa14f9549d6a467b7be7da3d0717f0263500", + "crates/horizon-world/src/region.rs": "72b642e87fed37ed17f54d4c30f96c3b64d26c14bc0589c31ef40ff1616188fc", + "crates/horizon-world/src/registry/json.rs": "7a683afcd1742c5307756927d7f0aa779c7d7992af87d8d371dd98108728e235", + "crates/horizon-world/src/registry.rs": "f2e58b2b4d944ac8fc6226bd3352cefc3d2052d8473e02ccf7dc402b176df435", + "crates/horizon-world/src/retrogen.rs": "6efd36a6293756688c1990a7965bd65609d7c5e2bf2a5f4f01d54932d2b9386b", + "crates/horizon-world/src/tick_data.rs": "f8d04a69ea263f4168bce33f92e5b4ee2fe07d7caf50a1621c6b3ef6eb2479f3", + "crates/horizon-world/src/worldgen/anchor.rs": "d156b8a0b147a274cdd143409b441f4f43edecde6e8ee14ef9cf0309f743143a", + "crates/horizon-world/src/worldgen/aquifer/config.rs": "fcd2cc3a520c1527c88f095a3b5d38487e7ebb1ccecc5f757f93d04a069add4f", + "crates/horizon-world/src/worldgen/aquifer/noise_based.rs": "74daaa389b5c3dfdc99214dfff256a9e172f83548d2326cbc941d9637cc53fb7", + "crates/horizon-world/src/worldgen/aquifer.rs": "cb4b66d87de3f9f79ae6ffe4badd6f0f44886cd42ad9de2771b8b56c4f1e28c7", + "crates/horizon-world/src/worldgen/bamboo.rs": "d9b47113c99be34256e58308bc6f40072ff25c579d6bf995182aa862c0006102", + "crates/horizon-world/src/worldgen/biome.rs": "3fe316b875aa32683759c337dc71f624910faf0812a2d6d83b8098d6d0bac7a0", + "crates/horizon-world/src/worldgen/biome_chunk.rs": "8fdfe6ebe3ffeca97611fd5991a5bc6b2783ea1c62f4d8ef3b2dd53c04a5dc1d", + "crates/horizon-world/src/worldgen/biome_predicate.rs": "bfc581bc40077cc74ffda43e990a55fe997adc04c7130f98e039c3305ca79aac", + "crates/horizon-world/src/worldgen/biome_source/codec.rs": "2b9e4f5d679eb444cb73174f511e3b9f32e9eb138248ab12cffc6cfd76adf3a8", + "crates/horizon-world/src/worldgen/biome_source/runtime.rs": "f0b02c1521178e4d0377c6ca62694604b6a773f1f234f070ff0cde4685911d45", + "crates/horizon-world/src/worldgen/biome_source.rs": "e181e1736f3e0b75f7091a24c88de3fd116e01b98424d02382aea7a503d1eb54", + "crates/horizon-world/src/worldgen/biome_temperature.rs": "cde4bbab5e260c4065b113376c423b8511ade93c5aa5fd976e245ee8a58b5ad1", + "crates/horizon-world/src/worldgen/biome_weather.rs": "96b1f9494559ed31d06257b83d604d00cc069ccc8c7b68094117e034b4a71bd7", + "crates/horizon-world/src/worldgen/block_blob.rs": "c608c57b3d77279646ecb89e50677dd8e452b0422ffd357ba755536c5589eb89", + "crates/horizon-world/src/worldgen/block_column.rs": "f72c21a9066ee0aa68c2fb79fba436d05afb14c6ef5a2cb012f1ed35fc17cd22", + "crates/horizon-world/src/worldgen/block_feature.rs": "36b4e46ee7f7f73bcb94d8bce8d99013e4b16234c9a03542eeb3186e88dd1c21", + "crates/horizon-world/src/worldgen/block_pile.rs": "623e369b77e0526ed2756485a6c0bb09683cd28ad8876dea907360ca6665b39d", + "crates/horizon-world/src/worldgen/block_predicate.rs": "074c692367e3f2191a1bb28d87b23402430a843c246bd30a4ea838ee3500280e", + "crates/horizon-world/src/worldgen/blue_ice.rs": "eedd77bdaff808330b864166d303378096daa5c7d235e925770b171c05109247", + "crates/horizon-world/src/worldgen/bonus_chest.rs": "05d0f63fbc7412f13ba1fa37a7238114b8f70bfff318beb2dece590482d49cc0", + "crates/horizon-world/src/worldgen/bulk_section.rs": "eecf61532d7793f853884c5a18a7f436a59835c7b13d3ec9075804511553c9ba", + "crates/horizon-world/src/worldgen/carver/canyon.rs": "509c4d5a9d689849e98e894ab14a9fbf82d10707d38e9cc688ded3c4778df3e2", + "crates/horizon-world/src/worldgen/carver/generation.rs": "a4498d0d1457536de3922a08e9c40ccce8d2ef9cc263801c15a412327513c098", + "crates/horizon-world/src/worldgen/carver.rs": "46b251e28225fe1250de38f49dec5dacda4e2c4ba1e556e21e6d1e14e2651fc3", + "crates/horizon-world/src/worldgen/carving.rs": "4e77d4e5c4a0ca55ca3563b9bdf6f379342e0bdf9d019d60f7bc57ce4afb5fbd", + "crates/horizon-world/src/worldgen/chorus_plant.rs": "29224c164ede919e280a65a68f155c873e2c8fd0aa7d7e244d8fa9e468760250", + "crates/horizon-world/src/worldgen/climate/codec.rs": "d567adeb5d837816fca69102db51d790f71aec1b51c09298230d24612e400a14", + "crates/horizon-world/src/worldgen/climate/index.rs": "c5ce61679a5f0e0f2f0af2a373424effa124d0cce8250609eff38b7034650756", + "crates/horizon-world/src/worldgen/climate.rs": "1b9ad44407f40aee4c1cd5357a5c9c443c1b105728f01cf5c644072f99f8357e", + "crates/horizon-world/src/worldgen/decoration.rs": "75a79b20b21e133816e729f16e758be6165d0db18a64bfbd5fd3618325a2adf3", + "crates/horizon-world/src/worldgen/delta.rs": "c2bf49ddf3231d28c109e68871429bc13d76a7bebe14eb33e84aa856bdd66145", + "crates/horizon-world/src/worldgen/density.rs": "65a98c156a290a6b374ef670d28cd5dfbe0aa93171824d1bbd96422536273f84", + "crates/horizon-world/src/worldgen/disk.rs": "6f8f9e059755af44c9bfa1b91eba54a7d13fad811dce31717c5502d7f6df33d7", + "crates/horizon-world/src/worldgen/end_gateway.rs": "f53fde77d6a36a525cd15d890003b2c129d0689a4cbfa2cf443a0c2e3acae040", + "crates/horizon-world/src/worldgen/end_podium.rs": "8f6d0d6d60ff0fde63de5781e867239db321f82cf12639f0537d410a3731ee1d", + "crates/horizon-world/src/worldgen/feature.rs": "40500af71ee6628dfb27518262d99f08d0383ae6c7b9397452feef0c213eadd9", + "crates/horizon-world/src/worldgen/feature_catalog.rs": "efc7b17b227e71622ef34108be9abd010f5675b68ea0409fcab82581d9aab88a", + "crates/horizon-world/src/worldgen/feature_graph.rs": "da132fca9fdafb0d89a917d5f4b68c60b70fb33cadf948a107da0e35d83dcd91", + "crates/horizon-world/src/worldgen/feature_region.rs": "df0ff142a461cafa9a3b42b949ad3376887f2be8d4ea5cc794a3ed2e977bb499", + "crates/horizon-world/src/worldgen/feature_sort.rs": "3488a093dfe1321aaa29d8a30cfb115d8e666a81bf195514e87b170de626835c", + "crates/horizon-world/src/worldgen/fluid_predicate.rs": "64197c9e63bebf1ce04bc280bfe980bb6c1b9aa887ca5cded904aeb0d9ae6c46", + "crates/horizon-world/src/worldgen/height_provider.rs": "368d4dc119854742a045c4a717679c921456da681fc5f71aa9b37102d6f33b47", + "crates/horizon-world/src/worldgen/material_condition/codec.rs": "09b909bc384472f76d163c8069ec6b507c7d5c6a028a36d5e0e3df20deff7b3b", + "crates/horizon-world/src/worldgen/material_condition/density.rs": "df9d0761e978e5010663ae8b44f2d6c1ecb4907331e333bdb6a2b487c1e107de", + "crates/horizon-world/src/worldgen/material_condition/library.rs": "a55534c5da97575a819af5a7dc0dbec9069993fa8b2619c6c9d5576946e683fa", + "crates/horizon-world/src/worldgen/material_condition/noise.rs": "ffb59cc6be2275d7a5621bf7d768527e74a8326ed7ad78618746711540921675", + "crates/horizon-world/src/worldgen/material_condition/rule.rs": "fed643352efbcb1f68f29b6241896617db889fc7c577a6247564e01922fec1fe", + "crates/horizon-world/src/worldgen/material_condition.rs": "d638dcb8cbcb5da20b3cb29602b0ae6a86c4fdf9d8c0fb725de8ec5fc108717e", + "crates/horizon-world/src/worldgen/material_extensions.rs": "ab806eb679d5116e1aebb64fca70e1499ae71f83c9507a8cb4c3201c2b1bf037", + "crates/horizon-world/src/worldgen/material_stage.rs": "698cbc786eff13c246e2bc2ea062401918cd4265899fbc3d53abc87e45a8e7a1", + "crates/horizon-world/src/worldgen/material_surface.rs": "5d37b90099df3d6da0845dab164e71469fb287019c2ee74bc6714f3b9d6d6d5b", + "crates/horizon-world/src/worldgen/ore_feature.rs": "83b89fd3be691c5412c0215b6abefdcaa44119a05aa41217e061970eb7e12ebd", + "crates/horizon-world/src/worldgen/placement/chain.rs": "9218399b4d3757f7bbc550d7729a7201f32a1c83a7030865e0c12aab8a80d116", + "crates/horizon-world/src/worldgen/placement/context.rs": "a278304795136677ad4b4933ea50c972b9729a1c671c03fa309c193e224b1fd6", + "crates/horizon-world/src/worldgen/placement/layer.rs": "a0f5ca776f33f9d4f72b8fdcd163426152e5e43cb4a7572fc1d4fc57bf965de1", + "crates/horizon-world/src/worldgen/placement.rs": "b0397e5818fae84358546193a95f84bcb0b5bffafce909e6fcb3386179ee8023", + "crates/horizon-world/src/worldgen/replace_blobs.rs": "bf3377d86d2570cd15288ba69dde64a2cf67c5bcbb9a31d0c23421964af74afa", + "crates/horizon-world/src/worldgen/router.rs": "2cee7cf15a0d2ada12be6b39d83e78177fd3033922e4f4cce87a64073cec2847", + "crates/horizon-world/src/worldgen/rule_test.rs": "d1662ec492489ebaa4b87e20f9a8c809dcf3a223a151211bd65ec241a63692fc", + "crates/horizon-world/src/worldgen/settings.rs": "a8fe87752e1982ef11cb67f8d80d8e3428b2c27db178044cb425ba803e689e59", + "crates/horizon-world/src/worldgen/simple_block.rs": "9ba1a88d5db4925f8c37366557887f776c805f9a6444781615b5dbaff52e92a4", + "crates/horizon-world/src/worldgen/snow_freeze.rs": "9040be3d5c7f8f0fd812d2df45e154bf8a9eeef038530b83fa4159d246eac35f", + "crates/horizon-world/src/worldgen/speleothem.rs": "c71651c1c97818a8a85863e14414d6139ea3bdeb897fa422043bcc1499d99aae", + "crates/horizon-world/src/worldgen/spike.rs": "eacf0d9d3d2bc3d4bcf62bc87555f90031138236a5bfef0eb5270204f7c2a4fc", + "crates/horizon-world/src/worldgen/spring.rs": "830849e9b89fcdf09e9b06fbefdcdebe3c8c3f82804f60b8bd5de04ba303cb62", + "crates/horizon-world/src/worldgen/state_provider/catalog.rs": "c8687e15a4887a9200c8d365857d801efaa8f90e669eee3e600a1db4905119eb", + "crates/horizon-world/src/worldgen/state_provider/noise.rs": "7ee369c2a032e5620cb9bdf9ee7adad1f70870fb0a27efb7b7c514d92f83491f", + "crates/horizon-world/src/worldgen/state_provider/properties.rs": "9909139aaca1dc4f743957257fadaa9d423d21fa401fa9abc050505f79365139", + "crates/horizon-world/src/worldgen/state_provider.rs": "be29ada818826a90afe01ab60916360fdfc4cbde5af15c9359cd56b1f1f7b156", + "crates/horizon-world/src/worldgen/tags.rs": "d35ded93fe6e95ea045cadcf4925e656dc34ef832b742179b9ad0cc49f902f31", + "crates/horizon-world/src/worldgen/terrain.rs": "fd1f2e0afc15daf66f484ea40a1f2b7013650bda0941b23c64c81ee9f79cb673", + "crates/horizon-world/src/worldgen/underwater_magma.rs": "7317e40af5ee5a9780f90ce358fa5d3e7f5b3d513b731df6ff97754d9c2a0154", + "crates/horizon-world/src/worldgen/value_provider.rs": "3358b563db8cb0bc8a9cfbbe2b8b3e70941cd2d37a0b519fbb6545f8d0dcbb69", + "crates/horizon-world/src/worldgen/vines.rs": "ee9b79b65012bd86a838241d2ef708680f7fe8eafc8aa656dae9cf2abf3c6418", + "crates/horizon-world/src/worldgen/weighted.rs": "839840aae25302933bbd97a81a2336330e2d03a0ad1684e1e16ac769ed408e6b", + "crates/horizon-world/src/worldgen/write_zone.rs": "8e17a179764d3f68c05882aef35f6810c28a0000f9a83e58bd6030660b76e64e", + "crates/horizon-world/src/worldgen.rs": "e8e9f099ee1fcec8eedce31805d676d3f89c18e84f494a147ffcbfee23d3d54f" + }, + "visual_inspection": { + "platform": "Linux x86_64, headless Wayland, Vulkan llvmpipe LLVM 21.1.8 CPU renderer", + "resident_chunks": 81, + "model_blocks": 84647, + "merged_quads": 28071, + "model_quads": 179, + "presented_frames": 3232, + "error": null, + "observed": "Resident terrain and featured models inspected on the virtual screen. Physical movement, arrow rotation, diagnostics toggle and Ctrl+Q exercised; clean exit. No native pixel or hardware FPS comparison.", + "screenshots": "Private under .local/horizon-client/." + } +} diff --git a/crates/horizon-client/src/assets.rs b/crates/horizon-client/src/assets.rs index 7c8844045..40ae1a335 100644 --- a/crates/horizon-client/src/assets.rs +++ b/crates/horizon-client/src/assets.rs @@ -53,11 +53,11 @@ pub struct Assets { pub registry: BlockRegistry, pub properties: Properties, pub material_states: [BlockStateId; 6], - pub layers: Vec, pub sprite_size: u32, pub pixels: Vec, pub texture_layers: u32, - pub model_quads: Vec, + pub model_catalog: crate::models::StateModels, + pub model_placements: Vec<(horizon_core::position::BlockPos, BlockStateId)>, pub model_gallery: Vec, pub animations: Vec, } @@ -167,11 +167,6 @@ impl Assets { }) .collect::>()?, )); - let mut layers: Vec<_> = properties - .0 - .iter() - .map(|state| if state.air { -1 } else { -2 }) - .collect(); let mut material_states = [BlockStateId(0); 6]; let mut sprites = crate::sprites::Sprites::default(); for (layer, name) in MATERIALS.iter().enumerate() { @@ -179,20 +174,19 @@ impl Assets { .default_state(&format!("minecraft:{name}")) .ok_or_else(|| invalid("missing test-scene material"))?; material_states[layer] = state; - let sprite = sprites.insert(root, &format!("minecraft:block/{name}"))?; - layers[state.0 as usize] = sprite as i32; + sprites.insert(root, &format!("minecraft:block/{name}"))?; } - let gallery = crate::models::gallery(root, ®istry, &mut sprites)?; + let models = crate::models::prepare(root, ®istry, &mut sprites)?; Ok(Self { registry, properties, material_states, - layers, sprite_size: sprites.size, texture_layers: u32::try_from(sprites.layers.len())?, pixels: sprites.pixels, - model_quads: gallery.quads, - model_gallery: gallery.states, + model_catalog: models.catalog, + model_placements: models.placements, + model_gallery: models.states, animations: sprites.animations, }) } diff --git a/crates/horizon-client/src/main.rs b/crates/horizon-client/src/main.rs index 4bf6274b8..fc18d484c 100644 --- a/crates/horizon-client/src/main.rs +++ b/crates/horizon-client/src/main.rs @@ -10,7 +10,7 @@ mod sprites; mod world; use assets::Assets; use camera::Camera; -use mesh::Quad; +use mesh::Mesh; use render::Renderer; use std::{ collections::HashSet, @@ -101,7 +101,7 @@ impl Options { struct App<'a> { assets: &'a Assets, world: TestWorld<'a>, - mesh: Vec, + mesh: Mesh, options: Options, window: Option>, renderer: Option, @@ -167,8 +167,11 @@ impl App<'_> { "presented_frames": self.frames, "elapsed_seconds": self.started.elapsed().as_secs_f64(), "resident_chunks": self.world.chunks.len(), - "merged_quads": self.mesh.len(), - "model_quads": self.assets.model_quads.len(), + "merged_quads": self.mesh.cubes.len(), + "model_quads": self.mesh.models.len(), + "visible_model_faces": self.mesh.visible_faces, + "culled_model_faces": self.mesh.culled_faces, + "model_blocks": self.mesh.model_blocks, "model_gallery": self.assets.model_gallery, "sprite_layers": self.assets.texture_layers, "animated_sprites": self.assets.animations.len(), @@ -204,7 +207,8 @@ impl ApplicationHandler for App<'_> { match pollster::block_on(Renderer::new( window.clone(), self.assets, - &self.mesh, + &self.mesh.cubes, + &self.mesh.models, ((self.options.radius + 1) * 16) as f32, )) { Ok(renderer) => self.renderer = Some(renderer), @@ -305,7 +309,7 @@ impl ApplicationHandler for App<'_> { self.window.as_ref().unwrap().set_title(&format!( "Horizon — Development | {fps:.0} FPS | {} chunks | {} quads", self.world.chunks.len(), - self.mesh.len() + self.mesh.cubes.len() )); self.title_at = Instant::now(); self.title_frames = self.frames; @@ -353,12 +357,13 @@ fn run() -> Result<()> { format!("{error}\nPrepare pinned resources with: python3 tools/prepare_native_client.py") })?; let world = TestWorld::new(&assets, options.radius)?; - let mesh = mesh::build(&world)?; + let mut visibility = models::load_visibility(&options.resources, &assets.registry)?; + let mesh = mesh::build(&world, &assets, &mut visibility)?; eprintln!( "Horizon development scene: {} resident chunks, {} merged cube faces, {} baked model faces.\nClick to capture mouse; WASD/Space/Shift move; arrows rotate; Esc releases; Ctrl+Q quits.", world.chunks.len(), - mesh.len(), - assets.model_quads.len() + mesh.cubes.len(), + mesh.models.len() ); let now = Instant::now(); let mut app = App { diff --git a/crates/horizon-client/src/mesh.rs b/crates/horizon-client/src/mesh.rs index 4640d5f98..37213bf29 100644 --- a/crates/horizon-client/src/mesh.rs +++ b/crates/horizon-client/src/mesh.rs @@ -1,6 +1,14 @@ -//! Greedy opaque-cube meshing for the controlled renderer scene. Model geometry, -//! weighted variants, tinting, transparency and lighting are separate work. -use crate::{Result, world::TestWorld}; +//! Resident blockstate model meshing. Greedy rectangles preserve the texture +//! and signed UV basis of eligible unit-cube faces; other geometry stays baked. +use crate::{ + Result, + assets::Assets, + models::{self, CubeStyle, ModelInstance}, + world::TestWorld, +}; +use horizon_core::{geometry::Direction, position::BlockPos}; +use horizon_world::block_render::FaceVisibility; + #[repr(C)] #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] pub struct Quad { @@ -8,6 +16,17 @@ pub struct Quad { pub extent: [f32; 2], pub face: u32, pub layer: u32, + pub uv_origin: [f32; 2], + pub uv_u: [f32; 2], + pub uv_v: [f32; 2], + pub shade: f32, +} +pub struct Mesh { + pub cubes: Vec, + pub models: Vec, + pub visible_faces: usize, + pub culled_faces: usize, + pub model_blocks: usize, } fn position(face: usize, plane: usize, u: usize, v: usize) -> [usize; 3] { match face { @@ -16,36 +35,137 @@ fn position(face: usize, plane: usize, u: usize, v: usize) -> [usize; 3] { _ => [plane, v, u], } } -pub fn build(world: &TestWorld<'_>) -> Result> { - let mut quads = Vec::new(); +pub fn build( + world: &TestWorld<'_>, + assets: &Assets, + visibility: &mut FaceVisibility, +) -> Result { + let mut mesh = Mesh { + cubes: Vec::new(), + models: Vec::new(), + visible_faces: 0, + culled_faces: 0, + model_blocks: 0, + }; for &(cx, cz) in world.chunks.keys() { - let mut cells = [-1_i32; 18 * 18 * 18]; - for y in 0..18 { - for z in 0..18 { - for x in 0..18 { - cells[index(x, y, z)] = world.layer( - cx * 16 + x as i32 - 1, - y as i32 - 1, - cz * 16 + z as i32 - 1, - )?; + let mut masks = vec![[None; 256]; 6 * 16]; + for y in 0..16 { + for z in 0..16 { + for x in 0..16 { + let pos = BlockPos::new(cx * 16 + x, y, cz * 16 + z); + let state = world.block(pos.x, pos.y, pos.z); + if world.is_air(state) { + continue; + } + mesh.model_blocks += 1; + let selected = assets.model_catalog.collect(state, pos)?; + let mut visible = [false; 6]; + for direction in Direction::ALL { + let [dx, dy, dz] = direction.normal(); + let neighbor = world.block(pos.x + dx, pos.y + dy, pos.z + dz); + visible[direction as usize] = + visibility.should_render_face(state, neighbor, direction, true)?; + } + if selected.len() == 1 + && let Some(styles) = selected[0].1.cube + { + for (face, style) in styles.into_iter().enumerate() { + if !visible[face] { + mesh.culled_faces += 1; + continue; + } + mesh.visible_faces += 1; + let (plane, u, v) = match face { + 0 | 1 => (y, x, z), + 2 | 3 => (z, x, y), + _ => (x, z, y), + }; + masks[face * 16 + plane as usize][(u + 16 * v) as usize] = Some(style); + } + } else { + for (_, model) in selected { + for face in &model.faces { + if face.quad.cull.is_some_and(|d| !visible[d as usize]) { + mesh.culled_faces += 1; + continue; + } + mesh.visible_faces += 1; + mesh.models.push(models::instance(face, pos)); + } + } + } } } } - mesh_chunk(&cells, cx, cz, &mut quads); + for (index, mask) in masks.iter_mut().enumerate() { + merge(mask, index / 16, index % 16, cx, cz, &mut mesh.cubes); + } } - Ok(quads) + Ok(mesh) } +fn merge( + mask: &mut [Option; 256], + face: usize, + plane: usize, + cx: i32, + cz: i32, + quads: &mut Vec, +) { + for v in 0..16 { + let mut u = 0; + while u < 16 { + let Some(style) = mask[u + v * 16] else { + u += 1; + continue; + }; + let mut width = 1; + while u + width < 16 && mask[u + width + v * 16] == Some(style) { + width += 1; + } + let mut height = 1; + while v + height < 16 + && (u..u + width).all(|x| mask[x + (v + height) * 16] == Some(style)) + { + height += 1; + } + for y in v..v + height { + for x in u..u + width { + mask[x + y * 16] = None; + } + } + let p = position(face, plane + usize::from(face % 2 == 1), u, v); + let [a, b, c, d, e, f] = style.uv.map(f32::from); + quads.push(Quad { + origin: [ + (cx * 16 + p[0] as i32) as f32, + p[1] as f32, + (cz * 16 + p[2] as i32) as f32, + ], + extent: [width as f32, height as f32], + face: face as u32, + layer: style.layer, + uv_origin: [a, b], + uv_u: [c, d], + uv_v: [e, f], + shade: f32::from_bits(style.shade_bits), + }); + u += width; + } + } +} + +#[cfg(test)] fn index(x: usize, y: usize, z: usize) -> usize { x + 18 * (z + 18 * y) } +#[cfg(test)] fn mesh_chunk(cells: &[i32; 18 * 18 * 18], cx: i32, cz: i32, quads: &mut Vec) { for face in 0..6 { for plane in 0..16 { - let mut mask = [-1_i32; 256]; + let mut mask = [None; 256]; for v in 0..16 { for u in 0..16 { let p = position(face, plane + 1, u + 1, v + 1); - let layer = cells[index(p[0], p[1], p[2])]; let mut adjacent = p; let axis = match face { 0 | 1 => 1, @@ -57,52 +177,20 @@ fn mesh_chunk(cells: &[i32; 18 * 18 * 18], cx: i32, cz: i32, quads: &mut Vec= 0 && cells[index(adjacent[0], adjacent[1], adjacent[2])] < 0 { - mask[u + v * 16] = layer; + mask[u + 16 * v] = Some(CubeStyle { + layer: layer as u32, + uv: [0, 1, 1, 0, 0, -1], + shade_bits: 1f32.to_bits(), + }); } } } - for v in 0..16 { - let mut u = 0; - while u < 16 { - let layer = mask[u + v * 16]; - if layer < 0 { - u += 1; - continue; - } - let mut width = 1; - while u + width < 16 && mask[u + width + v * 16] == layer { - width += 1; - } - let mut height = 1; - while v + height < 16 - && (u..u + width).all(|x| mask[x + (v + height) * 16] == layer) - { - height += 1; - } - for y in v..v + height { - for x in u..u + width { - mask[x + y * 16] = -1; - } - } - let p = position(face, plane + usize::from(face % 2 == 1), u, v); - quads.push(Quad { - origin: [ - (cx * 16 + p[0] as i32) as f32, - p[1] as f32, - (cz * 16 + p[2] as i32) as f32, - ], - extent: [width as f32, height as f32], - face: face as u32, - layer: layer as u32, - }); - u += width; - } - } + merge(&mut mask, face, plane, cx, cz, quads); } } } - #[cfg(test)] mod tests { use super::*; @@ -178,6 +266,63 @@ mod tests { quads } + #[test] + fn merging_preserves_rotation_mirroring_shade_and_texture() { + let bases = [ + [0, 0, 1, 0, 0, 1], + [1, 0, 0, 1, -1, 0], + [1, 1, -1, 0, 0, -1], + [0, 1, 0, -1, 1, 0], + [1, 0, -1, 0, 0, 1], + [0, 1, 1, 0, 0, -1], + [0, 0, 0, 1, 1, 0], + [1, 1, 0, -1, -1, 0], + ]; + let original: [Option; 256] = std::array::from_fn(|i| { + (i % 17 != 0).then_some(CubeStyle { + layer: (i / 64) as u32, + uv: bases[(i / 4) % 8], + shade_bits: if i % 5 == 0 { 0.5f32 } else { 1f32 }.to_bits(), + }) + }); + for face in 0..6 { + let mut mask = original; + let mut quads = Vec::new(); + merge(&mut mask, face, 4, -2, 3, &mut quads); + let mut covered = [false; 256]; + for q in quads { + let p = [q.origin[0] + 32.0, q.origin[1], q.origin[2] - 48.0]; + let (u0, v0) = match face { + 0 | 1 => (p[0] as usize, p[2] as usize), + 2 | 3 => (p[0] as usize, p[1] as usize), + _ => (p[2] as usize, p[1] as usize), + }; + for dv in 0..q.extent[1] as usize { + for du in 0..q.extent[0] as usize { + let index = u0 + du + 16 * (v0 + dv); + assert!(!covered[index]); + covered[index] = true; + let style = original[index].unwrap(); + assert_eq!(q.layer, style.layer); + assert_eq!(q.shade.to_bits(), style.shade_bits); + let [a, b, c, d, e, f] = style.uv.map(f32::from); + let local = [a + c * 0.25 + e * 0.75, b + d * 0.25 + f * 0.75]; + let merged: [f32; 2] = std::array::from_fn(|axis| { + q.uv_origin[axis] + + q.uv_u[axis] * (du as f32 + 0.25) + + q.uv_v[axis] * (dv as f32 + 0.75) + }); + assert_eq!( + local.map(|v| v.rem_euclid(1.0)), + merged.map(|v| v.rem_euclid(1.0)) + ); + } + } + } + assert_eq!(covered, original.map(|v| v.is_some())); + } + } + #[test] fn empty_solid_and_neighbor_seams() { let mut cells = [-1; 18 * 18 * 18]; diff --git a/crates/horizon-client/src/models.rs b/crates/horizon-client/src/models.rs index ec12d568a..5cd6d319e 100644 --- a/crates/horizon-client/src/models.rs +++ b/crates/horizon-client/src/models.rs @@ -1,10 +1,12 @@ //! A blockstate-driven inspection gallery, separate from gameplay simulation. mod catalog; +mod cube; use crate::{Result, sprites::Sprites}; -use catalog::{StateModels, descriptor}; -use horizon_assets::model::Direction; +pub use catalog::StateModels; +use catalog::descriptor; +pub use cube::CubeStyle; use horizon_core::position::BlockPos; -use horizon_world::registry::BlockRegistry; +use horizon_world::registry::{BlockRegistry, BlockStateId}; use serde_json::{Value, json}; use std::path::Path; @@ -43,13 +45,20 @@ pub struct ModelInstance { pub layer: u32, pub shade: f32, } -pub struct Gallery { - pub quads: Vec, +pub struct PreparedModels { + pub catalog: StateModels, + pub placements: Vec<(BlockPos, BlockStateId)>, pub states: Vec, } -pub fn gallery(root: &Path, registry: &BlockRegistry, sprites: &mut Sprites) -> Result { - let models = StateModels::load(root, registry, &GALLERY.map(|(name, _)| name), sprites)?; - let mut instances = Vec::new(); +pub fn prepare( + root: &Path, + registry: &BlockRegistry, + sprites: &mut Sprites, +) -> Result { + let mut blocks = GALLERY.map(|(name, _)| name).to_vec(); + blocks.extend(crate::assets::MATERIALS); + let catalog = StateModels::load(root, registry, &blocks, sprites)?; + let mut placements = Vec::new(); let mut states = Vec::new(); for (index, (name, properties)) in GALLERY.iter().enumerate() { let state = registry.resolve(&descriptor(name, properties))?; @@ -57,33 +66,68 @@ pub fn gallery(root: &Path, registry: &BlockRegistry, sprites: &mut Sprites) -> .state(state) .ok_or("missing gallery registry state")?; let position = BlockPos::new((index % 4) as i32 * 4 - 7, 4, (index / 4) as i32 * 4 + 14); - let origin = [position.x as f32, position.y as f32, position.z as f32]; - let selected = models.collect(state, position)?; + let selected = catalog.collect(state, position)?; states.push(json!({"block":actual.name,"properties":actual.properties,"state_id":state.0, "position":[position.x,position.y,position.z], "models":selected.iter().map(|(v,_)| json!({"model":v.model,"rotations":v.rotations,"uv_lock":v.uv_lock})).collect::>()})); - for (_, faces) in selected { - for face in faces { - let quad = &face.quad; - let shade = match quad.shade_direction_override.unwrap_or(quad.facing) { - Direction::Down => 0.5, - Direction::Up => 1.0, - Direction::North | Direction::South => 0.8, - Direction::West | Direction::East => 0.6, - }; - instances.push(ModelInstance { - positions: quad - .positions - .map(|v| std::array::from_fn(|i| v[i] + origin[i])), - uv: quad.uv, - layer: face.layer, - shade, - }); - } - } + placements.push((position, state)); } - Ok(Gallery { - quads: instances, + Ok(PreparedModels { + catalog, + placements, states, }) } +pub fn instance(face: &catalog::BakedFace, position: BlockPos) -> ModelInstance { + let origin = [position.x as f32, position.y as f32, position.z as f32]; + ModelInstance { + positions: face + .quad + .positions + .map(|v| std::array::from_fn(|i| v[i] + origin[i])), + uv: face.quad.uv, + layer: face.layer, + shade: cube::shade(face), + } +} + +pub fn load_visibility( + root: &Path, + registry: &BlockRegistry, +) -> Result { + use horizon_world::{ + block_render::{FaceVisibility, RenderDefinition, SkipRendering}, + collision::CachedCollisionRegistry, + nbt::{self, Limits}, + }; + use std::{collections::BTreeMap, fs}; + let data = nbt::read_named(&fs::read(root.join("occlusion.nbt"))?, Limits::default())? + .0 + .tag; + let occlusion = CachedCollisionRegistry::from_nbt(&data, registry)?; + let raw: BTreeMap = + serde_json::from_reader(fs::File::open(root.join("render-definitions.json"))?)?; + let definitions = raw + .into_iter() + .map(|(name, v)| { + let skip = match v["rule"].as_str().ok_or("missing render rule")? { + "never" => SkipRendering::Never, + "same_block" => SkipRendering::SameBlock, + "bars" => SkipRendering::Bars, + "leaves" => SkipRendering::Leaves, + "liquid" => SkipRendering::Liquid, + "mangrove_roots" => SkipRendering::MangroveRoots, + _ => return Err("unrecognized render rule"), + }; + Ok(( + name, + RenderDefinition { + skip, + can_occlude: v["can_occlude"].as_bool().ok_or("missing occlusion flag")?, + bars_tag: v["bars_tag"].as_bool().ok_or("missing bars tag")?, + }, + )) + }) + .collect::>()?; + Ok(FaceVisibility::new(&occlusion, &definitions)?) +} diff --git a/crates/horizon-client/src/models/catalog.rs b/crates/horizon-client/src/models/catalog.rs index 046ae172d..087926849 100644 --- a/crates/horizon-client/src/models/catalog.rs +++ b/crates/horizon-client/src/models/catalog.rs @@ -30,12 +30,17 @@ use std::{ pub struct BakedFace { pub quad: ModelQuad, pub layer: u32, + pub opaque: bool, +} +pub struct BakedModel { + pub faces: Vec, + pub cube: Option<[super::cube::CubeStyle; 6]>, } type VariantKey = (String, [u8; 3], bool); pub struct StateModels { choices: Vec, states: BTreeMap, - baked: BTreeMap>, + baked: BTreeMap, } fn key(variant: &Variant) -> VariantKey { (variant.model.clone(), variant.rotations, variant.uv_lock) @@ -159,14 +164,26 @@ impl StateModels { let mut baked = BTreeMap::new(); for (name, turns, lock) in variants { let model = library.resolve(&name)?; - let faces = bake_model_transformed(&model, BlockModelTransform::new(turns, lock))?.into_iter().map(|quad| { - if quad.material.force_translucent || quad.tint_index != -1 { - return Err("inspection gallery requires untinted opaque or binary-cutout materials".into()); - } - let layer = sprites.insert(root, &quad.material.sprite)?; - Ok(BakedFace { quad, layer }) - }).collect::>()?; - baked.insert((name, turns, lock), faces); + let faces: Vec = + bake_model_transformed(&model, BlockModelTransform::new(turns, lock))? + .into_iter() + .map(|quad| { + if quad.material.force_translucent || quad.tint_index != -1 { + return Err( + "inspection gallery requires untinted opaque or binary-cutout materials" + .into(), + ); + } + let layer = sprites.insert(root, &quad.material.sprite)?; + Ok(BakedFace { + quad, + layer, + opaque: sprites.opaque[layer as usize], + }) + }) + .collect::>()?; + let cube = super::cube::cube_styles(&faces); + baked.insert((name, turns, lock), BakedModel { faces, cube }); } Ok(Self { choices, @@ -178,7 +195,7 @@ impl StateModels { &self, state: BlockStateId, position: BlockPos, - ) -> Result> { + ) -> Result> { let (choice, selected) = self .states .get(&state) @@ -190,15 +207,7 @@ impl StateModels { self.choices[*choice].collect(selected, &mut random, &mut variants)?; variants .into_iter() - .map(|v| { - Ok(( - v, - self.baked - .get(&key(v)) - .ok_or("unbaked block model")? - .as_slice(), - )) - }) + .map(|v| Ok((v, self.baked.get(&key(v)).ok_or("unbaked block model")?))) .collect() } } diff --git a/crates/horizon-client/src/models/cube.rs b/crates/horizon-client/src/models/cube.rs new file mode 100644 index 000000000..9192f36d8 --- /dev/null +++ b/crates/horizon-client/src/models/cube.rs @@ -0,0 +1,158 @@ +//! Exact unit-cube faces eligible for repeated-texture greedy rectangles. +use super::catalog::BakedFace; +use horizon_assets::model::Direction; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CubeStyle { + pub layer: u32, + /// UV at spatial (u,v)=(0,0), then the two signed unit-axis derivatives. + pub uv: [i8; 6], + pub shade_bits: u32, +} +pub fn shade(face: &BakedFace) -> f32 { + match face + .quad + .shade_direction_override + .unwrap_or(face.quad.facing) + { + Direction::Down => 0.5, + Direction::Up => 1.0, + Direction::North | Direction::South => 0.8, + Direction::West | Direction::East => 0.6, + } +} +pub fn cube_styles(faces: &[BakedFace]) -> Option<[CubeStyle; 6]> { + if faces.len() != 6 { + return None; + } + let mut styles = [None; 6]; + for face in faces { + if !face.opaque || face.quad.light_emission != 0 { + return None; + } + let q = &face.quad; + let direction = q.cull?; + if q.facing != direction || styles[direction as usize].is_some() { + return None; + } + let (axis, u, v) = match direction { + Direction::Down | Direction::Up => (1, 0, 2), + Direction::North | Direction::South => (2, 0, 1), + Direction::West | Direction::East => (0, 2, 1), + }; + let plane = if (direction as usize).is_multiple_of(2) { + 0.0 + } else { + 1.0 + }; + let mut uv = [None; 4]; + for (p, texture) in q.positions.iter().zip(q.uv) { + if p[axis] != plane + || ![p[u], p[v], texture[0], texture[1]] + .iter() + .all(|&x| x == 0.0 || x == 1.0) + { + return None; + } + let index = p[u] as usize + 2 * p[v] as usize; + if uv[index].replace(texture.map(|x| x as i8)).is_some() { + return None; + } + } + let [a, b, c, d] = uv.map(|v| v.unwrap()); + let du = [b[0] - a[0], b[1] - a[1]]; + let dv = [c[0] - a[0], c[1] - a[1]]; + if d != [a[0] + du[0] + dv[0], a[1] + du[1] + dv[1]] + || (du[0] * dv[1] - du[1] * dv[0]).abs() != 1 + { + return None; + } + styles[direction as usize] = Some(CubeStyle { + layer: face.layer, + uv: [a[0], a[1], du[0], du[1], dv[0], dv[1]], + shade_bits: shade(face).to_bits(), + }); + } + Some(styles.map(|v| v.unwrap())) +} + +#[cfg(test)] +mod tests { + use super::*; + use horizon_assets::{ + bake::{BlockModelTransform, bake_model_transformed}, + model::CuboidModel, + resolve::ModelLibrary, + }; + use serde_json::json; + #[test] + fn projected_cube_uvs_match_baked_quarter_turns() { + let faces: serde_json::Map = Direction::ALL + .into_iter() + .map(|d| { + ( + d.name().to_owned(), + json!({"texture":"#all","cullface":d.name()}), + ) + }) + .collect(); + let model = CuboidModel::from_json(&json!({"textures":{"all":"horizon:probe"},"elements":[{"from":[0,0,0],"to":[16,16,16],"faces":faces}]}).to_string()).unwrap(); + let library = ModelLibrary::new([("horizon:cube".to_owned(), model)].into()).unwrap(); + let model = library.resolve("horizon:cube").unwrap(); + let mut eligible = 0; + for x in 0..4 { + for y in 0..4 { + for z in 0..4 { + for lock in [false, true] { + let faces: Vec<_> = bake_model_transformed( + &model, + BlockModelTransform::new([x, y, z], lock), + ) + .unwrap() + .into_iter() + .map(|quad| BakedFace { + quad, + layer: 7, + opaque: true, + }) + .collect(); + let Some(styles) = cube_styles(&faces) else { + assert!(lock); + continue; + }; + eligible += 1; + for face in &faces { + let direction = face.quad.cull.unwrap(); + let (u, v) = match direction { + Direction::Down | Direction::Up => (0, 2), + Direction::North | Direction::South => (0, 1), + _ => (2, 1), + }; + let style = styles[direction as usize]; + let [a, b, c, d, e, f] = style.uv.map(f32::from); + for (p, uv) in face.quad.positions.iter().zip(face.quad.uv) { + assert_eq!([a + c * p[u] + e * p[v], b + d * p[u] + f * p[v]], uv); + } + } + } + } + } + } + assert!(eligible >= 64); + let mut faces: Vec<_> = + bake_model_transformed(&model, BlockModelTransform::new([0; 3], false)) + .unwrap() + .into_iter() + .map(|quad| BakedFace { + quad, + layer: 0, + opaque: true, + }) + .collect(); + faces[0].opaque = false; + assert!(cube_styles(&faces).is_none()); + faces[0].opaque = true; + faces[0].quad.positions[0][0] = 0.25; + assert!(cube_styles(&faces).is_none()); + } +} diff --git a/crates/horizon-client/src/render.rs b/crates/horizon-client/src/render.rs index 631295c61..f9639bc85 100644 --- a/crates/horizon-client/src/render.rs +++ b/crates/horizon-client/src/render.rs @@ -39,6 +39,7 @@ impl Renderer { window: Arc, assets: &Assets, quads: &[Quad], + model_faces: &[ModelInstance], fog_distance: f32, ) -> Result { let instance = @@ -153,7 +154,7 @@ impl Renderer { cache: None, }) }; - let attributes = wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Uint32,3=>Uint32]; + let attributes = wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Uint32,3=>Uint32,4=>Float32x2,5=>Float32x2,6=>Float32x2,7=>Float32]; let pipeline = create_pipeline( "Horizon opaque cubes", "vs_main", @@ -236,10 +237,10 @@ impl Renderer { contents: bytemuck::cast_slice(quads), usage: wgpu::BufferUsages::VERTEX, }); - let model_quad_count = u32::try_from(assets.model_quads.len())?; + let model_quad_count = u32::try_from(model_faces.len())?; let model_quads = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Baked vanilla model faces"), - contents: bytemuck::cast_slice(&assets.model_quads), + contents: bytemuck::cast_slice(model_faces), usage: wgpu::BufferUsages::VERTEX, }); let depth = Self::depth(&device, &config); diff --git a/crates/horizon-client/src/sprites.rs b/crates/horizon-client/src/sprites.rs index b8e9c4eb0..d8804f777 100644 --- a/crates/horizon-client/src/sprites.rs +++ b/crates/horizon-client/src/sprites.rs @@ -30,6 +30,7 @@ pub struct Sprites { pub pixels: Vec, pub layers: BTreeMap, pub animations: Vec, + pub opaque: Vec, } impl Sprites { pub fn insert(&mut self, root: &Path, name: &str) -> Result { @@ -130,6 +131,8 @@ impl Sprites { clip: Arc::new(clip), }); } + self.opaque + .push(pixels.as_chunks::<4>().0.iter().all(|p| p[3] == 255)); self.layers.insert(name.to_owned(), layer); self.size = width; self.pixels.extend_from_slice(&frames[initial]); diff --git a/crates/horizon-client/src/world.rs b/crates/horizon-client/src/world.rs index 036251865..f50afc116 100644 --- a/crates/horizon-client/src/world.rs +++ b/crates/horizon-client/src/world.rs @@ -1,4 +1,4 @@ -//! A controlled opaque-cube scene for exercising the native renderer. This is +//! A controlled blockstate scene for exercising the native renderer. This is //! not the vanilla world generator or an authoritative gameplay simulation. use crate::{ Result, @@ -10,7 +10,6 @@ use std::collections::BTreeMap; pub struct TestWorld<'a> { pub chunks: BTreeMap<(i32, i32), TerrainChunk<'a, Properties>>, - assets: &'a Assets, air: BlockStateId, } impl<'a> TestWorld<'a> { @@ -21,7 +20,6 @@ impl<'a> TestWorld<'a> { .ok_or("missing air state")?; let mut world = Self { chunks: BTreeMap::new(), - assets, air, }; for cz in -radius..=radius { @@ -73,6 +71,9 @@ impl<'a> TestWorld<'a> { } } } + for (position, state) in &assets.model_placements { + world.set(position.x, position.y, position.z, *state)?; + } Ok(world) } fn set(&mut self, x: i32, y: i32, z: i32, state: BlockStateId) -> Result<()> { @@ -82,15 +83,12 @@ impl<'a> TestWorld<'a> { .set_block_worldgen(x, y, z, state)?; Ok(()) } - pub fn layer(&self, x: i32, y: i32, z: i32) -> Result { - let state = self - .chunks + pub fn block(&self, x: i32, y: i32, z: i32) -> BlockStateId { + self.chunks .get(&(x >> 4, z >> 4)) - .map_or(self.air, |chunk| chunk.block(x, y, z)); - let layer = self.assets.layers[state.0 as usize]; - if layer == -2 { - return Err("test scene contains an unsupported non-cube material".into()); - } - Ok(layer) + .map_or(self.air, |chunk| chunk.block(x, y, z)) + } + pub fn is_air(&self, state: BlockStateId) -> bool { + state == self.air } } diff --git a/crates/horizon-client/src/world.wgsl b/crates/horizon-client/src/world.wgsl index 35126504a..16c2b24a9 100644 --- a/crates/horizon-client/src/world.wgsl +++ b/crates/horizon-client/src/world.wgsl @@ -15,7 +15,9 @@ struct VertexOut { } @vertex fn vs_main(@builtin(vertex_index) vertex: u32, @location(0) origin: vec3, @location(1) extent: vec2, - @location(2) face: u32, @location(3) layer: u32) -> VertexOut { + @location(2) face: u32, @location(3) layer: u32, + @location(4) uv_origin: vec2, @location(5) uv_u: vec2, + @location(6) uv_v: vec2, @location(7) shade: f32) -> VertexOut { let corners = array,4>(vec2(0.0,0.0),vec2(1.0,0.0),vec2(1.0,1.0),vec2(0.0,1.0)); let normal_order = array(0u,1u,2u,0u,2u,3u); let reversed_order = array(0u,2u,1u,0u,3u,2u); @@ -28,9 +30,9 @@ struct VertexOut { else { position+=vec3(0.0,offset.y,offset.x); } var output: VertexOut; output.clip=camera.view_projection*vec4(position,1.0); - output.uv=vec2(offset.x,extent.y-offset.y); + output.uv=uv_origin+uv_u*offset.x+uv_v*offset.y; output.layer=layer; - output.shade=select(select(0.8,0.6,face>=4u),select(0.5,1.0,face==1u),face<2u); + output.shade=shade; output.world_position=position; return output; } diff --git a/docs/face-visibility.md b/docs/face-visibility.md index f4112eb70..093695af6 100644 --- a/docs/face-visibility.md +++ b/docs/face-visibility.md @@ -3,8 +3,8 @@ `horizon-world::block_render::FaceVisibility` decides whether to emit a model quad with a culling direction beside a neighboring block. It prepares immutable face geometry from the pinned raw occlusion shapes, shares faces across states, -and caches shape-pair subtraction with bounded memory. It is ready for chunk -meshing; it does not yet replace the development scene's cube-only culling. +and caches shape-pair subtraction with bounded memory. The development client +now uses it for resident chunk model meshing, including cross-chunk neighbors. The decision preserves the native order: a canonical full neighbor face hides the quad, block-specific suppression can hide it, canonical empty faces bypass diff --git a/docs/native-client.md b/docs/native-client.md index de20fac54..dff5146fb 100644 --- a/docs/native-client.md +++ b/docs/native-client.md @@ -1,28 +1,33 @@ # Horizon native development client `horizon-client` opens a native Rust window and renders a controlled block scene -stored in Horizon's resident chunks, alongside sixteen blockstate-driven model placements. It -loads the block registry, models and sprites from pinned Java Edition 26.3 -resources. A padded chunk mesh -culls hidden faces across chunk boundaries and merges adjacent matching faces; -the GPU draws the rectangles as instances. A separate instanced draw renders -baked cuboid faces without expanding them into cube approximations. The camera -is a free inspection tool. +stored in Horizon's resident chunks. Every occupied cell selects models from its +registry state, including the terrain and sixteen featured placements. It loads +block definitions, cuboid models, sprites and occlusion grids from pinned Java +Edition 26.3 resources. Model selection uses the default block position seed; +neighbor visibility uses the validated Rust face rules across chunk boundaries. +The camera remains a free inspection tool. + +Eligible opaque unit-cube faces use greedy rectangles only when texture layer, +UV rotation/mirroring and shading match. Each rectangle retains its signed UV +basis, so weighted texture variation survives merging. Other baked geometry and +binary-alpha cutouts use the separate model-face instance pipeline. Geometry +with nonintegral UV-lock rounding also stays on that path. This checkpoint has no player simulation, vanilla world generation, block interaction, inventory, sound, networking or world save/load. It does not meet the [playable-build acceptance criteria](rust-parity.md#completion-criteria-and-playable-builds). -The model gallery now uses blockstate dispatch, weighted variants, multipart -conditions and transformed baking. Chunk-wide model rendering, fractional -transparency, tinting, lighting, resource packs, chunk streaming and full visual -parity remain unfinished. +The controlled chunk scene now uses blockstate dispatch, weighted variants, +multipart conditions and transformed baking. Full block-catalog material support, +fractional transparency, tinting, lighting, resource packs, chunk streaming and +full visual parity remain unfinished. The resource gallery sits beside the pyramid and includes oak stairs, a slab, an anvil, a torch, a lantern, connected fences and iron bars, a cobweb, a wall grindstone, a ladder, a sideways amethyst cluster and four weighted stone placements. Registry state properties select the models: the stairs are an upside-down inner corner, the slab is a top slab and the fence gate is open. -These remain static inspection placements without simulation callbacks. Binary-alpha cutouts are rendered; +These are resident chunk states; simulation callbacks remain unfinished. Binary-alpha cutouts are rendered; the lantern's three frames advance every eight inspection ticks at 20 ticks/s. The animation metadata codec and frame selection now use the shared `horizon-assets` implementation checked against the pinned client. GPU @@ -170,8 +175,8 @@ selector and predicate cases: 3,802 definitions and 687,466 state results match, with 220 expected binding errors. It checks the native replacement-before-abort behavior for overlapping variants, eager multipart validation even when variants cover every state, integer-property aliases, nested conditions and empty part -selections. The development gallery now uses this binder; general chunk model -rendering remains unfinished. +selections. The controlled chunk renderer now uses this binder for its model +catalog. The Java integer-property parser is shared by `horizon-core`, client binding and world-state decoding. Its [codec regression](../benchmarks/rust-blockstate-model-binding-2026-10-06/shared-integer-codec.json) @@ -231,8 +236,8 @@ python3 tools/test_native_client.py --vscreen The mesh tests compare expanded rectangles against independently enumerated exposed voxel faces, including solid neighbors, cavities, material boundaries, negative chunk origins and random surroundings. The startup runner launches a -nine-chunk scene, requires 30 presented frames and a clean exit, verifies 209 -baked gallery faces across 18 sprite layers, and writes GPU, +nine-chunk scene, requires 30 presented frames and a clean exit, verifies 3,169 +greedy cube rectangles and 179 other model faces across 18 sprite layers, and writes GPU, platform, binary and source information to ignored `.local/horizon-client/startup-smoke.json`. It has an external timeout as well as the application's frame deadline. These checks do not establish gameplay or @@ -265,4 +270,16 @@ IDs, properties, positions and models are recorded in the startup report. [Neighbor-face visibility](face-visibility.md) now has native client/server coverage across all registered states and suppression-rule families. Integrating -that result with resident chunk model meshing is the next rendering step. +that result with resident chunk model meshing is now covered by the scene check +below. + +The [resident-model report](../benchmarks/rust-native-chunk-models-2026-10-06/startup-smoke.json) +checks all 10,919 occupied cells in the bounded startup scene against a separately +constructed native scene. Actual Java state-model selection, parent geometry, +rotated culling directions and `Block.shouldRenderFace` produce the same 5,991 +visible and 59,636 culled model faces. The sixteen featured model selections also +retain their direct native comparison. Expanded greedy-rectangle tests cover +rotated and mirrored UV bases, texture and shade boundaries, negative origins, +neighbor seams and cavities; cube projection tests cover all XYZ quarter turns. +These checks cover CPU mesh behavior and GPU startup, not native shader pixels +or a hardware performance comparison. diff --git a/docs/rust-parity.md b/docs/rust-parity.md index ef31caf53..9a85a7658 100644 --- a/docs/rust-parity.md +++ b/docs/rust-parity.md @@ -3,8 +3,8 @@ The behavior target is the repository's pinned **Minecraft Java Edition 26.3**. One-to-one vanilla gameplay parity is the acceptance criterion. It has **not** been reached. Horizon now has a [native development client](native-client.md) -that renders a controlled block scene and sixteen blockstate-driven model placements with a -free camera. It does not run a +that renders a controlled resident-chunk scene with blockstate-selected models +and a free camera. It does not run a complete game server or gameplay simulation. The Rust workspace implements core arithmetic/random streams, world data and resident sections, scheduled tick ordering, geometry, contained fluid states, @@ -24,11 +24,14 @@ binding now also matches native selection across registered and special item-frame states. Weighted selection and multipart random draw consumption also match native traces. Transformed baking now checks every bundled model/rotation/UV-lock tuple and all XYZ quarter-turn combinations against native -geometry by exact float bits. The native renderer now draws an -inspection gallery from registry-bound, selected and transformed models, including binary +geometry by exact float bits. The native renderer now draws its controlled +resident chunks from registry-bound, selected and transformed models, including binary cutouts, multipart connections, weighted stone variants and stepped lantern animation. All 16 scene selections match native model collection. These are not -yet gameplay blocks; general chunk model rendering remains unfinished. +yet simulated gameplay blocks. Resident chunk meshing now preserves weighted +variants and UV orientation when greedily merging matching cube faces, and uses +native-checked neighbor visibility. Full block-catalog material support, lighting +and chunk streaming remain unfinished. [Neighbor face visibility](face-visibility.md) now matches both reference JARs across 1,441,816 state pairs, including all pinned suppression-rule families. Animation metadata, frame sequences and tick-state selection now have native diff --git a/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaGallerySelectionOracle.java b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaGallerySelectionOracle.java index 334f3c089..290425f1e 100644 --- a/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaGallerySelectionOracle.java +++ b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaGallerySelectionOracle.java @@ -8,12 +8,13 @@ import java.util.*; import net.minecraft.client.renderer.block.dispatch.*; import net.minecraft.client.resources.model.*; import net.minecraft.client.resources.model.geometry.*; +import net.minecraft.client.resources.model.cuboid.*; import net.minecraft.client.resources.model.sprite.*; import net.minecraft.core.*; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.Identifier; import net.minecraft.util.StrictJsonParser; -import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.*; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.Property; import net.minecraft.world.level.levelgen.LegacyRandomSource; @@ -33,8 +34,9 @@ public final class VanillaGallerySelectionOracle { var faces=new ArrayList();for(var d:Direction.values())faces.add(matrix(state.inverseFaceTransformation(d))); return JSON.toJsonTree(map("model",name,"matrix",matrix(state.transformation().getMatrix()),"inverse_uv",faces)); } + private record PartTag(Identifier model,ModelState transform,JsonElement signature) {} private static final class Baker implements ModelBaker { - final IdentityHashMap tags=new IdentityHashMap<>(); + final IdentityHashMap tags=new IdentityHashMap<>(); public ResolvedModel getModel(Identifier id) { return new ResolvedModel() { public String debugName(){return id.toString();} @@ -47,7 +49,7 @@ public final class VanillaGallerySelectionOracle { try { var constructor=QuadCollection.class.getDeclaredConstructors()[0];constructor.setAccessible(true); var empty=List.of();var result=(QuadCollection)constructor.newInstance(empty,empty,empty,empty,empty,empty,empty,empty); - tags.put(result,tag(id.toString(),state));return result; + tags.put(result,new PartTag(id,state,tag(id.toString(),state)));return result; } catch(ReflectiveOperationException error){throw new AssertionError(error);} } }; @@ -60,9 +62,67 @@ public final class VanillaGallerySelectionOracle { private static > BlockState set(BlockState state,Property property,String value) { return state.setValue(property,property.getValue(value).orElseThrow()); } + private static Object scene(JsonObject report)throws Exception { + int chunks=report.get("resident_chunks").getAsInt();int diameter=(int)Math.sqrt(chunks); + if(diameter*diameter!=chunks || diameter%2==0)throw new AssertionError("unsupported scene grid"); + int radius=diameter/2;var world=new HashMap(); + String[] layers={"bedrock","dirt","stone","sand","oak_planks","bricks"}; + var materials=new ArrayList();for(var name:layers)materials.add(BuiltInRegistries.BLOCK.getValue(Identifier.parse(name)).defaultBlockState()); + for(int cz=-radius;cz<=radius;cz++)for(int cx=-radius;cx<=radius;cx++) + for(int z=0;z<16;z++)for(int x=0;x<16;x++)for(int y=0;y<4;y++) { + int layer=y==0?0:y<3?1:((cx+cz)&1)==0?2:3; + world.put(new BlockPos(cx*16+x,y,cz*16+z),materials.get(layer)); + } + for(int y=4;y<11;y++)for(int z=-10;z<=10;z++)for(int x=-10;x<=10;x++) + if(Math.max(Math.abs(x),Math.abs(z))<=14-y)world.put(new BlockPos(x,y,z),materials.get((y&1)==0?4:5)); + for(var item:report.getAsJsonArray("model_gallery")) { + var row=item.getAsJsonObject();var p=row.getAsJsonArray("position"); + world.put(new BlockPos(p.get(0).getAsInt(),p.get(1).getAsInt(),p.get(2).getAsInt()),Block.stateById(row.get("state_id").getAsInt())); + } + var roots=new HashMap(); + for(var state:new HashSet<>(world.values())) { + var id=BuiltInRegistries.BLOCK.getKey(state.getBlock()); + try(var input=VanillaGallerySelectionOracle.class.getResourceAsStream("/assets/"+id.getNamespace()+"/blockstates/"+id.getPath()+".json")) { + var raw=new String(Objects.requireNonNull(input).readAllBytes(),java.nio.charset.StandardCharsets.UTF_8); + roots.putAll(BlockStateModelDispatcher.CODEC.parse(JsonOps.INSTANCE,StrictJsonParser.parse(raw)).getOrThrow().instantiate(state.getBlock().getStateDefinition(),id::toString)); + } + } + var baker=new Baker();var models=new HashMap(); + for(var state:new HashSet<>(world.values()))models.put(state,Objects.requireNonNull(roots.get(state)).bake(state,baker)); + var unbaked=new HashMap();var pending=new ArrayList(); + for(var part:baker.tags.values())pending.add(part.model()); + for(int i=0;i{for(var id:unbaked.keySet())resolver.markDependency(id);});var resolved=discovery.resolve(); + int visible=0,culled=0;LeavesBlock.setCutoutLeaves(true); + var parts=new ArrayList(); + for(var entry:world.entrySet()) { + var pos=entry.getKey();var state=entry.getValue();parts.clear(); + models.get(state).collectParts(new LegacyRandomSource(state.getSeed(pos)),parts); + for(var selected:parts) { + var part=baker.tags.get(((SimpleModelWrapper)selected).quads()); + var geometry=resolved.get(part.model()).getTopGeometry();if(geometry==UnbakedGeometry.EMPTY)continue; + for(var element:((UnbakedCuboidGeometry)geometry).elements())for(var face:element.faces().values()) { + var direction=face.cullForDirection(); + if(direction!=null)direction=Direction.rotate(part.transform().transformation().getMatrix(),direction); + if(direction==null || Block.shouldRenderFace(state,world.getOrDefault(pos.relative(direction),Blocks.AIR.defaultBlockState()),direction))visible++;else culled++; + } + } + } + if(visible!=report.get("visible_model_faces").getAsInt() || culled!=report.get("culled_model_faces").getAsInt() || world.size()!=report.get("model_blocks").getAsInt()) + throw new AssertionError("native chunk model totals disagree: "+world.size()+" blocks / "+visible+" visible / "+culled+" culled"); + return map("blocks",world.size(),"visible_faces",visible,"culled_faces",culled); + } public static void main(String[] args) throws Exception { net.minecraft.SharedConstants.tryDetectVersion();net.minecraft.server.Bootstrap.bootStrap(); var report=JsonParser.parseString(Files.readString(Path.of(args[0]))).getAsJsonObject(); + VanillaHeightmapOracle.bindTags(JsonParser.parseString(Files.readString(Path.of(args[2]).resolve("block-tags.json"))).getAsJsonObject()); var results=new ArrayList();int index=0; for(var item:report.getAsJsonArray("model_gallery")) { var row=item.getAsJsonObject();var id=Identifier.parse(row.get("block").getAsString()); @@ -80,7 +140,7 @@ public final class VanillaGallerySelectionOracle { var pos=row.getAsJsonArray("position");var position=new BlockPos(pos.get(0).getAsInt(),pos.get(1).getAsInt(),pos.get(2).getAsInt()); var parts=new ArrayList();model.collectParts(new LegacyRandomSource(state.getSeed(position)),parts); var actual=new ArrayList(); - for(var part:parts)actual.add(Objects.requireNonNull(baker.tags.get(((SimpleModelWrapper)part).quads()))); + for(var part:parts)actual.add(Objects.requireNonNull(baker.tags.get(((SimpleModelWrapper)part).quads())).signature()); var expected=new ArrayList(); for(var selected:row.getAsJsonArray("models")) { var v=selected.getAsJsonObject();var t=v.getAsJsonArray("rotations"); @@ -90,6 +150,6 @@ public final class VanillaGallerySelectionOracle { if(!actual.equals(expected))throw new AssertionError("gallery selection mismatch at "+index+" "+id+": native="+actual+" Rust="+expected); results.add(map("case",index++,"state_id",Block.getId(state),"models",actual)); } - Files.writeString(Path.of(args[1]),JSON.toJson(results)); + Files.writeString(Path.of(args[1]),JSON.toJson(map("gallery",results,"scene",scene(report)))); } } diff --git a/tools/prepare_native_client.py b/tools/prepare_native_client.py index 285df656a..23aad9a5c 100644 --- a/tools/prepare_native_client.py +++ b/tools/prepare_native_client.py @@ -65,6 +65,29 @@ def main(): if tags != worldgen.tag_resources(jars['client']): raise ValueError('Reference block tags disagree') lab.write_json(root / 'block-tags.json', tags) + visibility = oracle.with_name('VanillaFaceVisibilityOracle.java') + collision = oracle.with_name('VanillaCollisionOracle.java') + render_sources = base.source_hashes([visibility, collision, oracle]) + render_files = ['render-definitions.json', 'occlusion.nbt'] + tag_hash = lab.digest(root/'block-tags.json', 'sha256') + if not (previous.get('render_exporters_sha256') == render_sources + and previous.get('server_sha256') == origin['server_sha256'] + and previous.get('block_tags_sha256') == tag_hash + and all((root/name).exists() and previous.get('render_files_sha256', {}).get(name) + == lab.digest(root/name, 'sha256') for name in render_files)): + export = lab.LOCAL/'horizon-client-render'; export.mkdir(exist_ok=True) + lab.write_json(export/'tags.json', tags) + classes = export/'classes'; classes.mkdir(exist_ok=True) + dependencies = os.pathsep.join(map(str, [jars['server'], *libraries])) + with (export/'export.log').open('w') as log: + subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none', + '-cp', dependencies, '-d', str(classes), str(visibility), str(collision), str(oracle)], + check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600) + subprocess.run(['java', '-Xmx768M', '-cp', str(classes)+os.pathsep+dependencies, + 'dev.horizonlab.parity.VanillaFaceVisibilityOracle', 'export', str(export)], + check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600) + (root/'render-definitions.json').write_bytes((export/'definitions.json').read_bytes()) + (root/'occlusion.nbt').write_bytes((export/'occlusion.nbt').read_bytes()) count = 0 prefixes = ('assets/minecraft/blockstates/', 'assets/minecraft/models/block/', 'assets/minecraft/textures/block/') @@ -82,7 +105,9 @@ def main(): target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(data) count += 1 - lab.write_json(manifest, {'schema': 2, **origin, + lab.write_json(manifest, {'schema': 3, **origin, + 'render_exporters_sha256': render_sources, + 'render_files_sha256': {name: lab.digest(root/name, 'sha256') for name in render_files}, 'model_state_exporters_sha256': binding_sources, 'model_states_sha256': lab.digest(model_states, 'sha256'), 'data_version': version['world_version'], 'facts_sha256': lab.digest(facts, 'sha256'), diff --git a/tools/test_native_client.py b/tools/test_native_client.py index cad568f0f..282ec3e4a 100644 --- a/tools/test_native_client.py +++ b/tools/test_native_client.py @@ -21,7 +21,7 @@ except ImportError: def resource_fingerprint(root): - files = [root / 'facts.json', root / 'model-states.json', root / 'block-tags.json', root / 'horizon-resources.json', + files = [root / 'facts.json', root / 'model-states.json', root / 'render-definitions.json', root / 'occlusion.nbt', root / 'block-tags.json', root / 'horizon-resources.json', *sorted(path for path in (root / 'assets').rglob('*') if path.is_file())] digest = hashlib.sha256() for path in files: @@ -50,7 +50,8 @@ def main(): *sorted((lab.ROOT / 'crates/horizon-assets/src').rglob('*.rs'))] oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaGallerySelectionOracle.java' helper = oracle.with_name('VanillaCuboidModelOracle.java') - sources += [oracle, helper, lab.ROOT/'tools/test_model_baking_parity.py', lab.ROOT/'tools/test_rust_parity.py', lab.ROOT/'tools/lab.py', lab.ROOT/'tools/prepare_native_client.py'] + heightmap = oracle.with_name('VanillaHeightmapOracle.java') + sources += [oracle, helper, heightmap, lab.ROOT/'tools/test_model_baking_parity.py', lab.ROOT/'tools/test_rust_parity.py', lab.ROOT/'tools/lab.py', lab.ROOT/'tools/prepare_native_client.py'] for crate in ['horizon-core', 'horizon-world']: sources += [lab.ROOT/'crates'/crate/'Cargo.toml', *sorted((lab.ROOT/'crates'/crate/'src').rglob('*.rs'))] tested_sources = { @@ -74,8 +75,10 @@ def main(): stdout=log, stderr=subprocess.STDOUT, timeout=120) result = json.loads(report.read_text()) if (result.get('error') is not None or result.get('presented_frames') != 30 - or result.get('resident_chunks') != 9 or result.get('merged_quads', 0) <= 0 - or result.get('model_quads') != 209 or len(result.get('model_gallery', [])) != 16 + or result.get('resident_chunks') != 9 or result.get('merged_quads') != 3169 + or result.get('visible_model_faces') != 5991 or result.get('culled_model_faces') != 59636 + or result.get('model_blocks') != 10919 + or result.get('model_quads') != 179 or len(result.get('model_gallery', [])) != 16 or result.get('sprite_layers') != 18 or result.get('animated_sprites') != 1 or not result.get('adapter') or min(result.get('window_size', [0])) <= 0): raise RuntimeError('Incomplete native client presentation: ' + str(report)) @@ -85,19 +88,21 @@ def main(): native = root/'gallery-selection.json' with (root/'gallery-selection.log').open('w') as log: subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none', '-cp', dependencies, - '-d', str(classes), str(oracle), str(helper)], cwd=lab.ROOT, check=True, + '-d', str(classes), str(oracle), str(helper), str(heightmap)], cwd=lab.ROOT, check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600) subprocess.run(['java', '-Xmx512M', '-cp', str(classes)+os.pathsep+dependencies, - 'dev.horizonlab.parity.VanillaGallerySelectionOracle', str(report), str(native)], + 'dev.horizonlab.parity.VanillaGallerySelectionOracle', str(report), str(native), str(root/'26.3')], cwd=lab.ROOT, check=True, stdout=log, stderr=subprocess.STDOUT, timeout=3600) - selections = json.loads(native.read_text()) + native_result = json.loads(native.read_text()) + selections = native_result['gallery'] + result['native_scene_culling'] = native_result['scene'] if len(selections) != len(result['model_gallery']): raise RuntimeError('Incomplete native gallery selection check') result['native_selection'] = {'matched_placements': len(selections), 'selected_parts': sum(len(s['models']) for s in selections), 'client_jar_sha256': lab.digest(jars['client'], 'sha256'), 'output_sha256': lab.digest(native, 'sha256'), - 'scope': 'Actual native registry states, blockstate dispatch, root baking with tagged empty geometry, and legacy collection using state.getSeed(position). Model names and transform matrices match scene selections; no shader/pixel parity claim.'} + 'scope': 'Actual native registry states, blockstate dispatch, tagged root baking and legacy collection using state.getSeed(position). All 16 featured placements match model names/transforms. An independently constructed native scene also matches total occupied blocks and visible/culled model faces using native parent resolution, rotated culling directions and Block.shouldRenderFace. No shader/pixel parity claim.'} if resource_fingerprint(root / '26.3') != resources: raise RuntimeError('Resources changed during native client verification') result['resource_files'] = resources