Mesh resident block models with native culling and UV-aware merging

This commit is contained in:
Ludovic Marie
2026-10-06 22:30:16 +02:00
parent f5ba959cdc
commit a6966ab450
17 changed files with 1330 additions and 185 deletions
@@ -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/."
}
}
+7 -13
View File
@@ -53,11 +53,11 @@ pub struct Assets {
pub registry: BlockRegistry,
pub properties: Properties,
pub material_states: [BlockStateId; 6],
pub layers: Vec<i32>,
pub sprite_size: u32,
pub pixels: Vec<u8>,
pub texture_layers: u32,
pub model_quads: Vec<crate::models::ModelInstance>,
pub model_catalog: crate::models::StateModels,
pub model_placements: Vec<(horizon_core::position::BlockPos, BlockStateId)>,
pub model_gallery: Vec<Value>,
pub animations: Vec<crate::sprites::SpriteAnimation>,
}
@@ -167,11 +167,6 @@ impl Assets {
})
.collect::<Result<_>>()?,
));
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, &registry, &mut sprites)?;
let models = crate::models::prepare(root, &registry, &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,
})
}
+14 -9
View File
@@ -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<Quad>,
mesh: Mesh,
options: Options,
window: Option<Arc<Window>>,
renderer: Option<Renderer>,
@@ -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 {
+202 -57
View File
@@ -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<Quad>,
pub models: Vec<ModelInstance>,
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<Vec<Quad>> {
let mut quads = Vec::new();
pub fn build(
world: &TestWorld<'_>,
assets: &Assets,
visibility: &mut FaceVisibility,
) -> Result<Mesh> {
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<CubeStyle>; 256],
face: usize,
plane: usize,
cx: i32,
cz: i32,
quads: &mut Vec<Quad>,
) {
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<Quad>) {
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<Qua
} else {
adjacent[axis] += 1;
}
let layer = cells[index(p[0], p[1], p[2])];
if layer >= 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<CubeStyle>; 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];
+75 -31
View File
@@ -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<ModelInstance>,
pub struct PreparedModels {
pub catalog: StateModels,
pub placements: Vec<(BlockPos, BlockStateId)>,
pub states: Vec<Value>,
}
pub fn gallery(root: &Path, registry: &BlockRegistry, sprites: &mut Sprites) -> Result<Gallery> {
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<PreparedModels> {
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::<Vec<_>>()}));
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<horizon_world::block_render::FaceVisibility> {
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<String, Value> =
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::<std::result::Result<_, &str>>()?;
Ok(FaceVisibility::new(&occlusion, &definitions)?)
}
+28 -19
View File
@@ -30,12 +30,17 @@ use std::{
pub struct BakedFace {
pub quad: ModelQuad,
pub layer: u32,
pub opaque: bool,
}
pub struct BakedModel {
pub faces: Vec<BakedFace>,
pub cube: Option<[super::cube::CubeStyle; 6]>,
}
type VariantKey = (String, [u8; 3], bool);
pub struct StateModels {
choices: Vec<CompiledChoices>,
states: BTreeMap<BlockStateId, (usize, SelectedModels)>,
baked: BTreeMap<VariantKey, Vec<BakedFace>>,
baked: BTreeMap<VariantKey, BakedModel>,
}
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::<Result<_>>()?;
baked.insert((name, turns, lock), faces);
let faces: Vec<BakedFace> =
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::<Result<_>>()?;
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<Vec<(&Variant, &[BakedFace])>> {
) -> Result<Vec<(&Variant, &BakedModel)>> {
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()
}
}
+158
View File
@@ -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<String, serde_json::Value> = 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());
}
}
+4 -3
View File
@@ -39,6 +39,7 @@ impl Renderer {
window: Arc<Window>,
assets: &Assets,
quads: &[Quad],
model_faces: &[ModelInstance],
fog_distance: f32,
) -> Result<Self> {
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);
+3
View File
@@ -30,6 +30,7 @@ pub struct Sprites {
pub pixels: Vec<u8>,
pub layers: BTreeMap<String, u32>,
pub animations: Vec<SpriteAnimation>,
pub opaque: Vec<bool>,
}
impl Sprites {
pub fn insert(&mut self, root: &Path, name: &str) -> Result<u32> {
@@ -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]);
+10 -12
View File
@@ -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<i32> {
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
}
}
+5 -3
View File
@@ -15,7 +15,9 @@ struct VertexOut {
}
@vertex fn vs_main(@builtin(vertex_index) vertex: u32,
@location(0) origin: vec3<f32>, @location(1) extent: vec2<f32>,
@location(2) face: u32, @location(3) layer: u32) -> VertexOut {
@location(2) face: u32, @location(3) layer: u32,
@location(4) uv_origin: vec2<f32>, @location(5) uv_u: vec2<f32>,
@location(6) uv_v: vec2<f32>, @location(7) shade: f32) -> VertexOut {
let corners = array<vec2<f32>,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<u32,6>(0u,1u,2u,0u,2u,3u);
let reversed_order = array<u32,6>(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;
}
+2 -2
View File
@@ -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
+34 -17
View File
@@ -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.
+8 -5
View File
@@ -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
@@ -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<Object>();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<QuadCollection,JsonElement> tags=new IdentityHashMap<>();
final IdentityHashMap<QuadCollection,PartTag> 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 <T extends Comparable<T>> BlockState set(BlockState state,Property<T> 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<BlockPos,BlockState>();
String[] layers={"bedrock","dirt","stone","sand","oak_planks","bricks"};
var materials=new ArrayList<BlockState>();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<BlockState,BlockStateModel.UnbakedRoot>();
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<BlockState,BlockStateModel>();
for(var state:new HashSet<>(world.values()))models.put(state,Objects.requireNonNull(roots.get(state)).bake(state,baker));
var unbaked=new HashMap<Identifier,UnbakedModel>();var pending=new ArrayList<Identifier>();
for(var part:baker.tags.values())pending.add(part.model());
for(int i=0;i<pending.size();i++) {
var id=pending.get(i);if(unbaked.containsKey(id))continue;
try(var input=VanillaGallerySelectionOracle.class.getResourceAsStream("/assets/"+id.getNamespace()+"/models/"+id.getPath()+".json")) {
var model=CuboidModel.fromStream(new java.io.InputStreamReader(Objects.requireNonNull(input),java.nio.charset.StandardCharsets.UTF_8));
unbaked.put(id,model);if(model.parent()!=null)pending.add(model.parent());
}
}
var discovery=new ModelDiscovery(unbaked,MissingCuboidModel.missingModel());
discovery.addRoot(resolver->{for(var id:unbaked.keySet())resolver.markDependency(id);});var resolved=discovery.resolve();
int visible=0,culled=0;LeavesBlock.setCutoutLeaves(true);
var parts=new ArrayList<BlockStateModelPart>();
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<Object>();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<BlockStateModelPart>();model.collectParts(new LegacyRandomSource(state.getSeed(position)),parts);
var actual=new ArrayList<JsonElement>();
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<JsonElement>();
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))));
}
}
+26 -1
View File
@@ -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'),
+13 -8
View File
@@ -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