diff --git a/Cargo.toml b/Cargo.toml index 340ab14a58..245e996a63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2939,6 +2939,17 @@ description = "Demonstrates FPS overlay" category = "Dev tools" wasm = true +[[example]] +name = "visibility_range" +path = "examples/3d/visibility_range.rs" +doc-scrape-examples = true + +[package.metadata.example.visibility_range] +name = "Visibility range" +description = "Demonstrates visibility ranges" +category = "3D Rendering" +wasm = true + [[example]] name = "color_grading" path = "examples/3d/color_grading.rs" diff --git a/assets/models/FlightHelmetLowPoly/FlightHelmetLowPoly.bin b/assets/models/FlightHelmetLowPoly/FlightHelmetLowPoly.bin new file mode 100644 index 0000000000..590b75a23f Binary files /dev/null and b/assets/models/FlightHelmetLowPoly/FlightHelmetLowPoly.bin differ diff --git a/assets/models/FlightHelmetLowPoly/FlightHelmetLowPoly.gltf b/assets/models/FlightHelmetLowPoly/FlightHelmetLowPoly.gltf new file mode 100644 index 0000000000..0240933459 --- /dev/null +++ b/assets/models/FlightHelmetLowPoly/FlightHelmetLowPoly.gltf @@ -0,0 +1,1739 @@ +{ + "asset": { + "generator": "Khronos glTF Blender I/O v4.0.44", + "version": "2.0" + }, + "scene": 0, + "scenes": [ + { + "name": "Scene", + "nodes": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16 + ] + } + ], + "nodes": [ + { + "mesh": 0, + "name": "GlassPlastic_low.002", + "translation": [ + 0.0005031332839280367, + -0.000009032823982124683, + -0.000502362905535847 + ] + }, + { + "mesh": 1, + "name": "LeatherParts_low.002", + "translation": [ + 0.0005031332839280367, + -0.000009032823982124683, + -0.000502362905535847 + ] + }, + { + "mesh": 2, + "name": "Lenses_low.002", + "translation": [ + 0.0005031332839280367, + -0.000009032823982124683, + -0.000502362905535847 + ] + }, + { + "mesh": 3, + "name": "RubberWood_low.002", + "translation": [ + 0.0005031332839280367, + -0.000009032823982124683, + -0.000502362905535847 + ] + }, + { + "mesh": 4, + "name": "RubberWood_low.003", + "translation": [ + 0.0005031332839280367, + -0.000009032823982124683, + -0.000502362905535847 + ] + }, + { + "mesh": 5, + "name": "MetalParts_low.002" + }, + { + "mesh": 6, + "name": "Hose_low.003", + "translation": [ + 0.0005031332839280367, + -0.000009032823982124683, + -0.000502362905535847 + ] + }, + { + "mesh": 7, + "name": "RubberWood_low.004" + }, + { + "mesh": 8, + "name": "LowString3" + }, + { + "mesh": 9, + "name": "LowString1" + }, + { + "mesh": 10, + "name": "LowString0" + }, + { + "mesh": 11, + "name": "LowString2" + }, + { + "mesh": 12, + "name": "MetalParts_low.003" + }, + { + "mesh": 13, + "name": "LeatherParts_low.003" + }, + { + "mesh": 14, + "name": "LeatherParts_low.004" + }, + { + "mesh": 15, + "name": "MetalParts_low.004" + }, + { + "mesh": 16, + "name": "MetalParts_low.005", + "translation": [ + 0, + 0, + 0.0030103428289294243 + ] + } + ], + "materials": [ + { + "name": "GlassPlasticMat.014", + "normalTexture": { + "index": 0 + }, + "occlusionTexture": { + "index": 1 + }, + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 2 + }, + "metallicRoughnessTexture": { + "index": 1 + } + } + }, + { + "name": "LeatherPartsMat.014", + "normalTexture": { + "index": 3 + }, + "occlusionTexture": { + "index": 4 + }, + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 5 + }, + "metallicRoughnessTexture": { + "index": 4 + } + } + }, + { + "alphaMode": "BLEND", + "name": "LensesMat.011", + "normalTexture": { + "index": 6 + }, + "occlusionTexture": { + "index": 7 + }, + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 8 + }, + "metallicRoughnessTexture": { + "index": 7 + } + } + }, + { + "name": "RubberWoodMat.015", + "normalTexture": { + "index": 9 + }, + "occlusionTexture": { + "index": 10 + }, + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 11 + }, + "metallicRoughnessTexture": { + "index": 10 + } + } + }, + { + "name": "RubberWoodMat.015", + "normalTexture": { + "index": 12 + }, + "occlusionTexture": { + "index": 13 + }, + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 14 + }, + "metallicRoughnessTexture": { + "index": 13 + } + } + }, + { + "name": "MetalPartsMat.014", + "normalTexture": { + "index": 15 + }, + "occlusionTexture": { + "index": 16 + }, + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 17 + }, + "metallicRoughnessTexture": { + "index": 16 + } + } + }, + { + "doubleSided": true, + "name": "HoseMat.013", + "normalTexture": { + "index": 18 + }, + "occlusionTexture": { + "index": 19 + }, + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 20 + }, + "metallicRoughnessTexture": { + "index": 19 + } + } + } + ], + "meshes": [ + { + "name": "GlassPlastic_low.001", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "NORMAL": 1, + "TEXCOORD_0": 2 + }, + "indices": 3, + "material": 0 + } + ] + }, + { + "name": "LeatherParts_low.001", + "primitives": [ + { + "attributes": { + "POSITION": 4, + "NORMAL": 5, + "TEXCOORD_0": 6 + }, + "indices": 7, + "material": 1 + } + ] + }, + { + "name": "Lenses_low.001", + "primitives": [ + { + "attributes": { + "POSITION": 8, + "NORMAL": 9, + "TEXCOORD_0": 10 + }, + "indices": 11, + "material": 2 + } + ] + }, + { + "name": "RubberWood_low.001", + "primitives": [ + { + "attributes": { + "POSITION": 12, + "NORMAL": 13, + "TEXCOORD_0": 14 + }, + "indices": 15, + "material": 3 + } + ] + }, + { + "name": "RubberWood_low.002", + "primitives": [ + { + "attributes": { + "POSITION": 16, + "NORMAL": 17, + "TEXCOORD_0": 18 + }, + "indices": 19, + "material": 4 + } + ] + }, + { + "name": "MetalParts_low.002", + "primitives": [ + { + "attributes": { + "POSITION": 20, + "NORMAL": 21, + "TEXCOORD_0": 22 + }, + "indices": 23, + "material": 5 + } + ] + }, + { + "name": "Hose_low.005", + "primitives": [ + { + "attributes": { + "POSITION": 24, + "NORMAL": 25, + "TEXCOORD_0": 26 + }, + "indices": 27, + "material": 6 + } + ] + }, + { + "name": "RubberWood_low.003", + "primitives": [ + { + "attributes": { + "POSITION": 28, + "NORMAL": 29, + "TEXCOORD_0": 30 + }, + "indices": 31, + "material": 3 + } + ] + }, + { + "name": "RubberWood_low.004", + "primitives": [ + { + "attributes": { + "POSITION": 32, + "NORMAL": 33, + "TEXCOORD_0": 34 + }, + "indices": 35, + "material": 4 + } + ] + }, + { + "name": "RubberWood_low.005", + "primitives": [ + { + "attributes": { + "POSITION": 36, + "NORMAL": 37, + "TEXCOORD_0": 38 + }, + "indices": 39, + "material": 4 + } + ] + }, + { + "name": "RubberWood_low.006", + "primitives": [ + { + "attributes": { + "POSITION": 40, + "NORMAL": 41, + "TEXCOORD_0": 42 + }, + "indices": 43, + "material": 4 + } + ] + }, + { + "name": "RubberWood_low.007", + "primitives": [ + { + "attributes": { + "POSITION": 44, + "NORMAL": 45, + "TEXCOORD_0": 46 + }, + "indices": 47, + "material": 4 + } + ] + }, + { + "name": "MetalParts_low.001", + "primitives": [ + { + "attributes": { + "POSITION": 48, + "NORMAL": 49, + "TEXCOORD_0": 50 + }, + "indices": 51, + "material": 5 + } + ] + }, + { + "name": "LeatherParts_low.002", + "primitives": [ + { + "attributes": { + "POSITION": 52, + "NORMAL": 53, + "TEXCOORD_0": 54 + }, + "indices": 55, + "material": 1 + } + ] + }, + { + "name": "LeatherParts_low.004", + "primitives": [ + { + "attributes": { + "POSITION": 56, + "NORMAL": 57, + "TEXCOORD_0": 58 + }, + "indices": 59, + "material": 1 + } + ] + }, + { + "name": "MetalParts_low.003", + "primitives": [ + { + "attributes": { + "POSITION": 60, + "NORMAL": 61, + "TEXCOORD_0": 62 + }, + "indices": 63, + "material": 5 + } + ] + }, + { + "name": "MetalParts_low.004", + "primitives": [ + { + "attributes": { + "POSITION": 64, + "NORMAL": 65, + "TEXCOORD_0": 66 + }, + "indices": 67, + "material": 5 + } + ] + } + ], + "textures": [ + { + "sampler": 0, + "source": 0 + }, + { + "sampler": 0, + "source": 1 + }, + { + "sampler": 0, + "source": 2 + }, + { + "sampler": 0, + "source": 3 + }, + { + "sampler": 0, + "source": 4 + }, + { + "sampler": 0, + "source": 5 + }, + { + "sampler": 0, + "source": 6 + }, + { + "sampler": 0, + "source": 7 + }, + { + "sampler": 0, + "source": 8 + }, + { + "sampler": 0, + "source": 9 + }, + { + "sampler": 0, + "source": 10 + }, + { + "sampler": 0, + "source": 11 + }, + { + "sampler": 0, + "source": 9 + }, + { + "sampler": 0, + "source": 10 + }, + { + "sampler": 0, + "source": 11 + }, + { + "sampler": 0, + "source": 12 + }, + { + "sampler": 0, + "source": 13 + }, + { + "sampler": 0, + "source": 14 + }, + { + "sampler": 0, + "source": 9 + }, + { + "sampler": 0, + "source": 10 + }, + { + "sampler": 0, + "source": 11 + } + ], + "images": [ + { + "bufferView": 4, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_GlassPlasticMat_Normal.png" + }, + { + "bufferView": 5, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_GlassPlasticMat_OcclusionRoughMetal.png" + }, + { + "bufferView": 6, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_GlassPlasticMat_BaseColor.png" + }, + { + "bufferView": 11, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_LeatherPartsMat_Normal.png" + }, + { + "bufferView": 12, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_LeatherPartsMat_OcclusionRoughMetal.png" + }, + { + "bufferView": 13, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_LeatherPartsMat_BaseColor.png" + }, + { + "bufferView": 18, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_LensesMat_Normal.png" + }, + { + "bufferView": 19, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_LensesMat_OcclusionRoughMetal.png" + }, + { + "bufferView": 20, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_LensesMat_BaseColor.png" + }, + { + "bufferView": 25, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_RubberWoodMat_Normal.png" + }, + { + "bufferView": 26, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_RubberWoodMat_OcclusionRoughMetal.png" + }, + { + "bufferView": 27, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_RubberWoodMat_BaseColor.png" + }, + { + "bufferView": 36, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_MetalPartsMat_Normal.png" + }, + { + "bufferView": 37, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_MetalPartsMat_OcclusionRoughMetal.png" + }, + { + "bufferView": 38, + "mimeType": "image/png", + "name": "../FlightHelmet/FlightHelmet_Materials_MetalPartsMat_BaseColor.png" + } + ], + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 1156, + "max": [ + 0.14054431021213531, + 0.6182851791381836, + 0.14643971621990204 + ], + "min": [ + -0.14054431021213531, + 0.4409570097923279, + -0.1078183650970459 + ], + "type": "VEC3" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 1156, + "type": "VEC3" + }, + { + "bufferView": 2, + "componentType": 5126, + "count": 1156, + "type": "VEC2" + }, + { + "bufferView": 3, + "componentType": 5123, + "count": 4998, + "type": "SCALAR" + }, + { + "bufferView": 7, + "componentType": 5126, + "count": 203, + "max": [ + 0.1251438856124878, + 0.7154311537742615, + 0.13240304589271545 + ], + "min": [ + -0.12485110014677048, + 0.40861979126930237, + -0.14235740900039673 + ], + "type": "VEC3" + }, + { + "bufferView": 8, + "componentType": 5126, + "count": 203, + "type": "VEC3" + }, + { + "bufferView": 9, + "componentType": 5126, + "count": 203, + "type": "VEC2" + }, + { + "bufferView": 10, + "componentType": 5123, + "count": 756, + "type": "SCALAR" + }, + { + "bufferView": 14, + "componentType": 5126, + "count": 439, + "max": [ + 0.10192074626684189, + 0.5936986207962036, + 0.1529267281293869 + ], + "min": [ + -0.101920947432518, + 0.5300428867340088, + 0.09017482399940491 + ], + "type": "VEC3" + }, + { + "bufferView": 15, + "componentType": 5126, + "count": 439, + "type": "VEC3" + }, + { + "bufferView": 16, + "componentType": 5126, + "count": 439, + "type": "VEC2" + }, + { + "bufferView": 17, + "componentType": 5123, + "count": 2208, + "type": "SCALAR" + }, + { + "bufferView": 21, + "componentType": 5126, + "count": 913, + "max": [ + 0.12686167657375336, + 0.7014831900596619, + 0.1686343550682068 + ], + "min": [ + -0.12686167657375336, + 0.3303739130496979, + -0.1446644514799118 + ], + "type": "VEC3" + }, + { + "bufferView": 22, + "componentType": 5126, + "count": 913, + "type": "VEC3" + }, + { + "bufferView": 23, + "componentType": 5126, + "count": 913, + "type": "VEC2" + }, + { + "bufferView": 24, + "componentType": 5123, + "count": 3951, + "type": "SCALAR" + }, + { + "bufferView": 28, + "componentType": 5126, + "count": 354, + "max": [ + 0.1588573157787323, + 0.5594512820243835, + 0.20407593250274658 + ], + "min": [ + -0.1588585376739502, + 0.008279342204332352, + -0.17154550552368164 + ], + "type": "VEC3" + }, + { + "bufferView": 29, + "componentType": 5126, + "count": 354, + "type": "VEC3" + }, + { + "bufferView": 30, + "componentType": 5126, + "count": 354, + "type": "VEC2" + }, + { + "bufferView": 31, + "componentType": 5123, + "count": 1704, + "type": "SCALAR" + }, + { + "bufferView": 32, + "componentType": 5126, + "count": 1906, + "max": [ + 0.1284475326538086, + 0.6024364233016968, + 0.15449710190296173 + ], + "min": [ + -0.119997538626194, + 0.5269837379455566, + -0.0017509098397567868 + ], + "type": "VEC3" + }, + { + "bufferView": 33, + "componentType": 5126, + "count": 1906, + "type": "VEC3" + }, + { + "bufferView": 34, + "componentType": 5126, + "count": 1906, + "type": "VEC2" + }, + { + "bufferView": 35, + "componentType": 5123, + "count": 8412, + "type": "SCALAR" + }, + { + "bufferView": 39, + "componentType": 5126, + "count": 180, + "max": [ + 0.10536719858646393, + 0.3556426167488098, + 0.1873881220817566 + ], + "min": [ + -0.07081730663776398, + 0.10695144534111023, + -0.08597134798765182 + ], + "type": "VEC3" + }, + { + "bufferView": 40, + "componentType": 5126, + "count": 180, + "type": "VEC3" + }, + { + "bufferView": 41, + "componentType": 5126, + "count": 180, + "type": "VEC2" + }, + { + "bufferView": 42, + "componentType": 5123, + "count": 912, + "type": "SCALAR" + }, + { + "bufferView": 43, + "componentType": 5126, + "count": 2799, + "max": [ + 0.1629406362771988, + 0.7025225758552551, + 0.1800684630870819 + ], + "min": [ + -0.15134775638580322, + 0.3202095031738281, + -0.14698635041713715 + ], + "type": "VEC3" + }, + { + "bufferView": 44, + "componentType": 5126, + "count": 2799, + "type": "VEC3" + }, + { + "bufferView": 45, + "componentType": 5126, + "count": 2799, + "type": "VEC2" + }, + { + "bufferView": 46, + "componentType": 5123, + "count": 12879, + "type": "SCALAR" + }, + { + "bufferView": 47, + "componentType": 5126, + "count": 168, + "max": [ + 0.13280345499515533, + 0.5543191432952881, + -0.01956009864807129 + ], + "min": [ + 0.05380884185433388, + 0.5075913071632385, + -0.14741668105125427 + ], + "type": "VEC3" + }, + { + "bufferView": 48, + "componentType": 5126, + "count": 168, + "type": "VEC3" + }, + { + "bufferView": 49, + "componentType": 5126, + "count": 168, + "type": "VEC2" + }, + { + "bufferView": 50, + "componentType": 5123, + "count": 900, + "type": "SCALAR" + }, + { + "bufferView": 51, + "componentType": 5126, + "count": 552, + "max": [ + 0.06188388541340828, + 0.6163731217384338, + -0.054040633141994476 + ], + "min": [ + -0.13275185227394104, + 0.29835832118988037, + -0.1523909866809845 + ], + "type": "VEC3" + }, + { + "bufferView": 52, + "componentType": 5126, + "count": 552, + "type": "VEC3" + }, + { + "bufferView": 53, + "componentType": 5126, + "count": 552, + "type": "VEC2" + }, + { + "bufferView": 54, + "componentType": 5123, + "count": 3204, + "type": "SCALAR" + }, + { + "bufferView": 55, + "componentType": 5126, + "count": 544, + "max": [ + 0.06400927901268005, + 0.6145105957984924, + -0.05163004994392395 + ], + "min": [ + -0.13188888132572174, + 0.29923850297927856, + -0.15189503133296967 + ], + "type": "VEC3" + }, + { + "bufferView": 56, + "componentType": 5126, + "count": 544, + "type": "VEC3" + }, + { + "bufferView": 57, + "componentType": 5126, + "count": 544, + "type": "VEC2" + }, + { + "bufferView": 58, + "componentType": 5123, + "count": 3156, + "type": "SCALAR" + }, + { + "bufferView": 59, + "componentType": 5126, + "count": 168, + "max": [ + 0.1347089558839798, + 0.5577988624572754, + -0.01678743213415146 + ], + "min": [ + 0.05212289094924927, + 0.5110973119735718, + -0.14619335532188416 + ], + "type": "VEC3" + }, + { + "bufferView": 60, + "componentType": 5126, + "count": 168, + "type": "VEC3" + }, + { + "bufferView": 61, + "componentType": 5126, + "count": 168, + "type": "VEC2" + }, + { + "bufferView": 62, + "componentType": 5123, + "count": 900, + "type": "SCALAR" + }, + { + "bufferView": 63, + "componentType": 5126, + "count": 157, + "max": [ + 0.060162343084812164, + 0.31056779623031616, + -0.10742472112178802 + ], + "min": [ + 0.03281792253255844, + 0.23964202404022217, + -0.12603096663951874 + ], + "type": "VEC3" + }, + { + "bufferView": 64, + "componentType": 5126, + "count": 157, + "type": "VEC3" + }, + { + "bufferView": 65, + "componentType": 5126, + "count": 157, + "type": "VEC2" + }, + { + "bufferView": 66, + "componentType": 5123, + "count": 780, + "type": "SCALAR" + }, + { + "bufferView": 67, + "componentType": 5126, + "count": 280, + "max": [ + 0.1127360537648201, + 0.5311859250068665, + 0.06694276630878448 + ], + "min": [ + -0.1127360537648201, + 0.4395008087158203, + 0.007419719360768795 + ], + "type": "VEC3" + }, + { + "bufferView": 68, + "componentType": 5126, + "count": 280, + "type": "VEC3" + }, + { + "bufferView": 69, + "componentType": 5126, + "count": 280, + "type": "VEC2" + }, + { + "bufferView": 70, + "componentType": 5123, + "count": 1362, + "type": "SCALAR" + }, + { + "bufferView": 71, + "componentType": 5126, + "count": 68, + "max": [ + 0.047261931002140045, + 0.40747731924057007, + 0.05520160123705864 + ], + "min": [ + -0.006848366465419531, + 0.3005165457725525, + 0.018949463963508606 + ], + "type": "VEC3" + }, + { + "bufferView": 72, + "componentType": 5126, + "count": 68, + "type": "VEC3" + }, + { + "bufferView": 73, + "componentType": 5126, + "count": 68, + "type": "VEC2" + }, + { + "bufferView": 74, + "componentType": 5123, + "count": 228, + "type": "SCALAR" + }, + { + "bufferView": 75, + "componentType": 5126, + "count": 777, + "max": [ + -0.06951691210269928, + 0.15422965586185455, + -0.004792370833456516 + ], + "min": [ + -0.20364271104335785, + 0.10436197370290756, + -0.09295757114887238 + ], + "type": "VEC3" + }, + { + "bufferView": 76, + "componentType": 5126, + "count": 777, + "type": "VEC3" + }, + { + "bufferView": 77, + "componentType": 5126, + "count": 777, + "type": "VEC2" + }, + { + "bufferView": 78, + "componentType": 5123, + "count": 2679, + "type": "SCALAR" + }, + { + "bufferView": 79, + "componentType": 5126, + "count": 32, + "max": [ + 0.06071668490767479, + 0.05985165387392044, + 0.19947707653045654 + ], + "min": [ + -0.06379426270723343, + 0.021160749718546867, + 0.19847150146961212 + ], + "type": "VEC3" + }, + { + "bufferView": 80, + "componentType": 5126, + "count": 32, + "type": "VEC3" + }, + { + "bufferView": 81, + "componentType": 5126, + "count": 32, + "type": "VEC2" + }, + { + "bufferView": 82, + "componentType": 5123, + "count": 126, + "type": "SCALAR" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteLength": 13872, + "byteOffset": 0, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 13872, + "byteOffset": 13872, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 9248, + "byteOffset": 27744, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 9996, + "byteOffset": 36992, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 248241, + "byteOffset": 46988 + }, + { + "buffer": 0, + "byteLength": 289819, + "byteOffset": 295232 + }, + { + "buffer": 0, + "byteLength": 217227, + "byteOffset": 585052 + }, + { + "buffer": 0, + "byteLength": 2436, + "byteOffset": 802280, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 2436, + "byteOffset": 804716, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1624, + "byteOffset": 807152, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1512, + "byteOffset": 808776, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 365647, + "byteOffset": 810288 + }, + { + "buffer": 0, + "byteLength": 335246, + "byteOffset": 1175936 + }, + { + "buffer": 0, + "byteLength": 313659, + "byteOffset": 1511184 + }, + { + "buffer": 0, + "byteLength": 5268, + "byteOffset": 1824844, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 5268, + "byteOffset": 1830112, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 3512, + "byteOffset": 1835380, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 4416, + "byteOffset": 1838892, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 1322, + "byteOffset": 1843308 + }, + { + "buffer": 0, + "byteLength": 183549, + "byteOffset": 1844632 + }, + { + "buffer": 0, + "byteLength": 244032, + "byteOffset": 2028184 + }, + { + "buffer": 0, + "byteLength": 10956, + "byteOffset": 2272216, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 10956, + "byteOffset": 2283172, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 7304, + "byteOffset": 2294128, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 7902, + "byteOffset": 2301432, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 259025, + "byteOffset": 2309336 + }, + { + "buffer": 0, + "byteLength": 301613, + "byteOffset": 2568364 + }, + { + "buffer": 0, + "byteLength": 255194, + "byteOffset": 2869980 + }, + { + "buffer": 0, + "byteLength": 4248, + "byteOffset": 3125176, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 4248, + "byteOffset": 3129424, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 2832, + "byteOffset": 3133672, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 3408, + "byteOffset": 3136504, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 22872, + "byteOffset": 3139912, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 22872, + "byteOffset": 3162784, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 15248, + "byteOffset": 3185656, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 16824, + "byteOffset": 3200904, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 297070, + "byteOffset": 3217728 + }, + { + "buffer": 0, + "byteLength": 317273, + "byteOffset": 3514800 + }, + { + "buffer": 0, + "byteLength": 294185, + "byteOffset": 3832076 + }, + { + "buffer": 0, + "byteLength": 2160, + "byteOffset": 4126264, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 2160, + "byteOffset": 4128424, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1440, + "byteOffset": 4130584, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1824, + "byteOffset": 4132024, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 33588, + "byteOffset": 4133848, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 33588, + "byteOffset": 4167436, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 22392, + "byteOffset": 4201024, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 25758, + "byteOffset": 4223416, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 2016, + "byteOffset": 4249176, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 2016, + "byteOffset": 4251192, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1344, + "byteOffset": 4253208, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1800, + "byteOffset": 4254552, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 6624, + "byteOffset": 4256352, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 6624, + "byteOffset": 4262976, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 4416, + "byteOffset": 4269600, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 6408, + "byteOffset": 4274016, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 6528, + "byteOffset": 4280424, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 6528, + "byteOffset": 4286952, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 4352, + "byteOffset": 4293480, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 6312, + "byteOffset": 4297832, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 2016, + "byteOffset": 4304144, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 2016, + "byteOffset": 4306160, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1344, + "byteOffset": 4308176, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1800, + "byteOffset": 4309520, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 1884, + "byteOffset": 4311320, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1884, + "byteOffset": 4313204, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1256, + "byteOffset": 4315088, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1560, + "byteOffset": 4316344, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 3360, + "byteOffset": 4317904, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 3360, + "byteOffset": 4321264, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 2240, + "byteOffset": 4324624, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 2724, + "byteOffset": 4326864, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 816, + "byteOffset": 4329588, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 816, + "byteOffset": 4330404, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 544, + "byteOffset": 4331220, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 456, + "byteOffset": 4331764, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 9324, + "byteOffset": 4332220, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 9324, + "byteOffset": 4341544, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 6216, + "byteOffset": 4350868, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 5358, + "byteOffset": 4357084, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 384, + "byteOffset": 4362444, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 384, + "byteOffset": 4362828, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 256, + "byteOffset": 4363212, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 252, + "byteOffset": 4363468, + "target": 34963 + } + ], + "samplers": [ + { + "magFilter": 9729, + "minFilter": 9987 + } + ], + "buffers": [ + { + "byteLength": 4363720, + "uri": "FlightHelmetLowPoly.bin" + } + ] +} \ No newline at end of file diff --git a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl index ef59418ea9..42f0d960b6 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl +++ b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl @@ -8,8 +8,8 @@ @group(0) @binding(3) var dt_lut_texture: texture_3d; @group(0) @binding(4) var dt_lut_sampler: sampler; #else - @group(0) @binding(18) var dt_lut_texture: texture_3d; - @group(0) @binding(19) var dt_lut_sampler: sampler; + @group(0) @binding(19) var dt_lut_texture: texture_3d; + @group(0) @binding(20) var dt_lut_sampler: sampler; #endif // Half the size of the crossfade region between shadows and midtones and diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index f4a3258c9f..0cbe8afab8 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -14,7 +14,10 @@ use bevy_render::{ primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, HalfSpace, Sphere}, render_resource::BufferBindingType, renderer::RenderDevice, - view::{InheritedVisibility, RenderLayers, ViewVisibility, VisibleEntities, WithMesh}, + view::{ + InheritedVisibility, RenderLayers, ViewVisibility, VisibilityRange, VisibleEntities, + VisibleEntityRanges, WithMesh, + }, }; use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::tracing::warn; @@ -1863,6 +1866,7 @@ pub fn check_light_mesh_visibility( Option<&RenderLayers>, Option<&Aabb>, Option<&GlobalTransform>, + Has, ), ( Without, @@ -1870,6 +1874,7 @@ pub fn check_light_mesh_visibility( With>, ), >, + visible_entity_ranges: Option>, ) { fn shrink_entities(visible_entities: &mut VisibleEntities) { // Check that visible entities capacity() is no more than two times greater than len() @@ -1887,6 +1892,8 @@ pub fn check_light_mesh_visibility( visible_entities.entities.shrink_to(reserved); } + let visible_entity_ranges = visible_entity_ranges.as_deref(); + // Directional lights for (directional_light, frusta, mut visible_entities, maybe_view_mask, light_view_visibility) in &mut directional_lights @@ -1928,6 +1935,7 @@ pub fn check_light_mesh_visibility( maybe_entity_mask, maybe_aabb, maybe_transform, + has_visibility_range, ) in &mut visible_entity_query { if !inherited_visibility.get() { @@ -1947,6 +1955,15 @@ pub fn check_light_mesh_visibility( .get_mut(view) .expect("Per-view visible entities should have been inserted already"); + // Check visibility ranges. + if has_visibility_range + && visible_entity_ranges.is_some_and(|visible_entity_ranges| { + !visible_entity_ranges.entity_is_in_range_of_view(entity, *view) + }) + { + continue; + } + for (frustum, frustum_visible_entities) in view_frusta.iter().zip(view_visible_entities) { @@ -2012,6 +2029,7 @@ pub fn check_light_mesh_visibility( maybe_entity_mask, maybe_aabb, maybe_transform, + has_visibility_range, ) in &mut visible_entity_query { if !inherited_visibility.get() { @@ -2023,6 +2041,15 @@ pub fn check_light_mesh_visibility( continue; } + // Check visibility ranges. + if has_visibility_range + && visible_entity_ranges.is_some_and(|visible_entity_ranges| { + !visible_entity_ranges.entity_is_in_range_of_any_view(entity) + }) + { + continue; + } + // If we have an aabb and transform, do frustum culling if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) { let model_to_world = transform.affine(); @@ -2077,6 +2104,7 @@ pub fn check_light_mesh_visibility( maybe_entity_mask, maybe_aabb, maybe_transform, + has_visibility_range, ) in &mut visible_entity_query { if !inherited_visibility.get() { @@ -2088,6 +2116,15 @@ pub fn check_light_mesh_visibility( continue; } + // Check visibility ranges. + if has_visibility_range + && visible_entity_ranges.is_some_and(|visible_entity_ranges| { + !visible_entity_ranges.entity_is_in_range_of_any_view(entity) + }) + { + continue; + } + // If we have an aabb and transform, do frustum culling if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) { let model_to_world = transform.affine(); diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 1309934a1e..ede7b1672a 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -31,7 +31,7 @@ use bevy_render::{ render_resource::*, renderer::RenderDevice, texture::FallbackImage, - view::{ExtractedView, Msaa, VisibleEntities, WithMesh}, + view::{ExtractedView, Msaa, RenderVisibilityRanges, VisibleEntities, WithMesh}, }; use bevy_utils::tracing::error; use std::marker::PhantomData; @@ -529,6 +529,7 @@ pub fn queue_material_meshes( render_mesh_instances: Res, render_material_instances: Res>, render_lightmaps: Res, + render_visibility_ranges: Res, mut views: Query<( &ExtractedView, &VisibleEntities, @@ -676,6 +677,10 @@ pub fn queue_material_meshes( mesh_key |= MeshPipelineKey::LIGHTMAPPED; } + if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) { + mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER; + } + let pipeline_id = pipelines.specialize( &pipeline_cache, &material_pipeline, diff --git a/crates/bevy_pbr/src/render/forward_io.wgsl b/crates/bevy_pbr/src/render/forward_io.wgsl index 2c861784c1..68378945d1 100644 --- a/crates/bevy_pbr/src/render/forward_io.wgsl +++ b/crates/bevy_pbr/src/render/forward_io.wgsl @@ -50,6 +50,9 @@ struct VertexOutput { #ifdef VERTEX_OUTPUT_INSTANCE_INDEX @location(6) @interpolate(flat) instance_index: u32, #endif +#ifdef VISIBILITY_RANGE_DITHER + @location(7) @interpolate(flat) visibility_range_dither: i32, +#endif } struct FragmentOutput { diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index fa3cbdee01..9433ac46c1 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -31,7 +31,10 @@ use bevy_render::{ render_resource::*, renderer::{RenderDevice, RenderQueue}, texture::{BevyDefault, DefaultImageSampler, ImageSampler, TextureFormatPixelInfo}, - view::{prepare_view_targets, GpuCulling, ViewTarget, ViewUniformOffset, ViewVisibility}, + view::{ + prepare_view_targets, GpuCulling, RenderVisibilityRanges, ViewTarget, ViewUniformOffset, + ViewVisibility, VisibilityRange, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT, + }, Extract, }; use bevy_transform::components::GlobalTransform; @@ -40,7 +43,7 @@ use bevy_utils::{tracing::error, tracing::warn, Entry, HashMap, Parallel}; #[cfg(debug_assertions)] use bevy_utils::warn_once; use bytemuck::{Pod, Zeroable}; -use nonmax::NonMaxU32; +use nonmax::{NonMaxU16, NonMaxU32}; use static_assertions::const_assert_eq; use crate::render::{ @@ -347,21 +350,30 @@ impl MeshUniform { // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_types.wgsl! bitflags::bitflags! { + /// Various flags and tightly-packed values on a mesh. + /// + /// Flags grow from the top bit down; other values grow from the bottom bit + /// up. #[repr(transparent)] pub struct MeshFlags: u32 { - const SHADOW_RECEIVER = 1 << 0; - const TRANSMITTED_SHADOW_RECEIVER = 1 << 1; + /// Bitmask for the 16-bit index into the LOD array. + /// + /// This will be `u16::MAX` if this mesh has no LOD. + const LOD_INDEX_MASK = (1 << 16) - 1; + const SHADOW_RECEIVER = 1 << 29; + const TRANSMITTED_SHADOW_RECEIVER = 1 << 30; // Indicates the sign of the determinant of the 3x3 model matrix. If the sign is positive, // then the flag should be set, else it should not be set. const SIGN_DETERMINANT_MODEL_3X3 = 1 << 31; const NONE = 0; - const UNINITIALIZED = 0xFFFF; + const UNINITIALIZED = 0xFFFFFFFF; } } impl MeshFlags { fn from_components( transform: &GlobalTransform, + lod_index: Option, not_shadow_receiver: bool, transmitted_receiver: bool, ) -> MeshFlags { @@ -377,8 +389,18 @@ impl MeshFlags { mesh_flags |= MeshFlags::SIGN_DETERMINANT_MODEL_3X3; } + let lod_index_bits = match lod_index { + None => u16::MAX, + Some(lod_index) => u16::from(lod_index), + }; + mesh_flags |= + MeshFlags::from_bits_retain((lod_index_bits as u32) << MeshFlags::LOD_INDEX_SHIFT); + mesh_flags } + + /// The first bit of the LOD index. + pub const LOD_INDEX_SHIFT: u32 = 0; } bitflags::bitflags! { @@ -747,6 +769,7 @@ pub struct ExtractMeshesSet; /// [`MeshUniform`] building. pub fn extract_meshes_for_cpu_building( mut render_mesh_instances: ResMut, + render_visibility_ranges: Res, mut render_mesh_instance_queues: Local>>, meshes_query: Extract< Query<( @@ -759,6 +782,7 @@ pub fn extract_meshes_for_cpu_building( Has, Has, Has, + Has, )>, >, ) { @@ -775,13 +799,23 @@ pub fn extract_meshes_for_cpu_building( transmitted_receiver, not_shadow_caster, no_automatic_batching, + visibility_range, )| { if !view_visibility.get() { return; } - let mesh_flags = - MeshFlags::from_components(transform, not_shadow_receiver, transmitted_receiver); + let mut lod_index = None; + if visibility_range { + lod_index = render_visibility_ranges.lod_index_for_entity(entity); + } + + let mesh_flags = MeshFlags::from_components( + transform, + lod_index, + not_shadow_receiver, + transmitted_receiver, + ); let shared = RenderMeshInstanceShared::from_components( previous_transform, @@ -830,6 +864,7 @@ pub fn extract_meshes_for_cpu_building( /// [`MeshUniform`] building. pub fn extract_meshes_for_gpu_building( mut render_mesh_instances: ResMut, + render_visibility_ranges: Res, mut batched_instance_buffers: ResMut< gpu_preprocessing::BatchedInstanceBuffers, >, @@ -848,6 +883,7 @@ pub fn extract_meshes_for_gpu_building( Has, Has, Has, + Has, )>, >, cameras_query: Extract, With)>>, @@ -881,13 +917,23 @@ pub fn extract_meshes_for_gpu_building( transmitted_receiver, not_shadow_caster, no_automatic_batching, + visibility_range, )| { if !view_visibility.get() { return; } - let mesh_flags = - MeshFlags::from_components(transform, not_shadow_receiver, transmitted_receiver); + let mut lod_index = None; + if visibility_range { + lod_index = render_visibility_ranges.lod_index_for_entity(entity); + } + + let mesh_flags = MeshFlags::from_components( + transform, + lod_index, + not_shadow_receiver, + transmitted_receiver, + ); let shared = RenderMeshInstanceShared::from_components( previous_transform, @@ -1023,9 +1069,14 @@ impl FromWorld for MeshPipeline { let (render_device, default_sampler, render_queue) = system_state.get_mut(world); let clustered_forward_buffer_binding_type = render_device .get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); + let visibility_ranges_buffer_binding_type = render_device + .get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); - let view_layouts = - generate_view_layouts(&render_device, clustered_forward_buffer_binding_type); + let view_layouts = generate_view_layouts( + &render_device, + clustered_forward_buffer_binding_type, + visibility_ranges_buffer_binding_type, + ); // A 1x1x1 'all 1.0' texture to use as a dummy texture to use in place of optional StandardMaterial textures let dummy_white_gpu_image = { @@ -1303,7 +1354,8 @@ bitflags::bitflags! { const READS_VIEW_TRANSMISSION_TEXTURE = 1 << 12; const LIGHTMAPPED = 1 << 13; const IRRADIANCE_VOLUME = 1 << 14; - const LAST_FLAG = Self::IRRADIANCE_VOLUME.bits(); + const VISIBILITY_RANGE_DITHER = 1 << 15; + const LAST_FLAG = Self::VISIBILITY_RANGE_DITHER.bits(); // Bitfields const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; @@ -1693,6 +1745,10 @@ impl SpecializedMeshPipeline for MeshPipeline { }, )); + if key.contains(MeshPipelineKey::VISIBILITY_RANGE_DITHER) { + shader_defs.push("VISIBILITY_RANGE_DITHER".into()); + } + if self.binding_arrays_are_usable { shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into()); } diff --git a/crates/bevy_pbr/src/render/mesh.wgsl b/crates/bevy_pbr/src/render/mesh.wgsl index 73e68dfe49..03dad7813e 100644 --- a/crates/bevy_pbr/src/render/mesh.wgsl +++ b/crates/bevy_pbr/src/render/mesh.wgsl @@ -91,6 +91,11 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput { out.instance_index = vertex_no_morph.instance_index; #endif +#ifdef VISIBILITY_RANGE_DITHER + out.visibility_range_dither = mesh_functions::get_visibility_range_dither_level( + vertex_no_morph.instance_index, model[3]); +#endif + return out; } diff --git a/crates/bevy_pbr/src/render/mesh_functions.wgsl b/crates/bevy_pbr/src/render/mesh_functions.wgsl index e94b4f7633..c100f16f6f 100644 --- a/crates/bevy_pbr/src/render/mesh_functions.wgsl +++ b/crates/bevy_pbr/src/render/mesh_functions.wgsl @@ -1,7 +1,7 @@ #define_import_path bevy_pbr::mesh_functions #import bevy_pbr::{ - mesh_view_bindings::view, + mesh_view_bindings::{view, visibility_ranges}, mesh_bindings::mesh, mesh_types::MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT, view_transformations::position_world_to_clip, @@ -83,3 +83,29 @@ fn mesh_tangent_local_to_world(model: mat4x4, vertex_tangent: vec4, in return vertex_tangent; } } + +// Returns an appropriate dither level for the current mesh instance. +// +// This looks up the LOD range in the `visibility_ranges` table and compares the +// camera distance to determine the dithering level. +#ifdef VISIBILITY_RANGE_DITHER +fn get_visibility_range_dither_level(instance_index: u32, world_position: vec4) -> i32 { + let visibility_buffer_index = mesh[instance_index].flags & 0xffffu; + if (visibility_buffer_index > arrayLength(&visibility_ranges)) { + return -16; + } + + let lod_range = visibility_ranges[visibility_buffer_index]; + let camera_distance = length(view.world_position.xyz - world_position.xyz); + + // This encodes the following mapping: + // + // `lod_range.` x y z w camera distance + // ←───────┼────────┼────────┼────────┼────────→ + // LOD level -16 -16 0 0 16 16 LOD level + let offset = select(-16, 0, camera_distance >= lod_range.z); + let bounds = select(lod_range.xy, lod_range.zw, camera_distance >= lod_range.z); + let level = i32(round((camera_distance - bounds.x) / (bounds.y - bounds.x) * 16.0)); + return offset + clamp(level, 0, 16); +} +#endif diff --git a/crates/bevy_pbr/src/render/mesh_types.wgsl b/crates/bevy_pbr/src/render/mesh_types.wgsl index 258b6ceef9..2e1e6aee41 100644 --- a/crates/bevy_pbr/src/render/mesh_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_types.wgsl @@ -29,7 +29,11 @@ struct MorphWeights { }; #endif -const MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u; -const MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT: u32 = 2u; +// [2^0, 2^16) +const MESH_FLAGS_VISIBILITY_RANGE_INDEX_BITS: u32 = 65535u; +// 2^29 +const MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 536870912u; +// 2^30 +const MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT: u32 = 1073741824u; // 2^31 - if the flag is set, the sign is positive, else it is negative const MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT: u32 = 2147483648u; diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 90f1c80284..db2aa1831a 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -12,13 +12,14 @@ use bevy_ecs::{ entity::Entity, system::{Commands, Query, Res}, }; +use bevy_math::Vec4; use bevy_render::{ globals::{GlobalsBuffer, GlobalsUniform}, render_asset::RenderAssets, render_resource::{binding_types::*, *}, renderer::RenderDevice, texture::{BevyDefault, FallbackImage, FallbackImageMsaa, FallbackImageZero, GpuImage}, - view::{Msaa, ViewUniform, ViewUniforms}, + view::{Msaa, RenderVisibilityRanges, ViewUniform, ViewUniforms}, }; #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] @@ -169,6 +170,7 @@ fn buffer_layout( /// Returns the appropriate bind group layout vec based on the parameters fn layout_entries( clustered_forward_buffer_binding_type: BufferBindingType, + visibility_ranges_buffer_binding_type: BufferBindingType, layout_key: MeshPipelineViewLayoutKey, render_device: &RenderDevice, ) -> Vec { @@ -258,9 +260,19 @@ fn layout_entries( (10, uniform_buffer::(true)), // Light probes (11, uniform_buffer::(true)), - // Screen space ambient occlusion texture + // Visibility ranges ( 12, + buffer_layout( + visibility_ranges_buffer_binding_type, + false, + Some(Vec4::min_size()), + ) + .visibility(ShaderStages::VERTEX), + ), + // Screen space ambient occlusion texture + ( + 13, texture_2d(TextureSampleType::Float { filterable: false }), ), ), @@ -269,9 +281,9 @@ fn layout_entries( // EnvironmentMapLight let environment_map_entries = environment_map::get_bind_group_layout_entries(render_device); entries = entries.extend_with_indices(( - (13, environment_map_entries[0]), - (14, environment_map_entries[1]), - (15, environment_map_entries[2]), + (14, environment_map_entries[0]), + (15, environment_map_entries[1]), + (16, environment_map_entries[2]), )); // Irradiance volumes @@ -279,16 +291,16 @@ fn layout_entries( let irradiance_volume_entries = irradiance_volume::get_bind_group_layout_entries(render_device); entries = entries.extend_with_indices(( - (16, irradiance_volume_entries[0]), - (17, irradiance_volume_entries[1]), + (17, irradiance_volume_entries[0]), + (18, irradiance_volume_entries[1]), )); } // Tonemapping let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); entries = entries.extend_with_indices(( - (18, tonemapping_lut_entries[0]), - (19, tonemapping_lut_entries[1]), + (19, tonemapping_lut_entries[0]), + (20, tonemapping_lut_entries[1]), )); // Prepass @@ -298,7 +310,7 @@ fn layout_entries( { for (entry, binding) in prepass::get_bind_group_layout_entries(layout_key) .iter() - .zip([20, 21, 22, 23]) + .zip([21, 22, 23, 24]) { if let Some(entry) = entry { entries = entries.extend_with_indices(((binding as u32, *entry),)); @@ -309,10 +321,10 @@ fn layout_entries( // View Transmission Texture entries = entries.extend_with_indices(( ( - 24, + 25, texture_2d(TextureSampleType::Float { filterable: true }), ), - (25, sampler(SamplerBindingType::Filtering)), + (26, sampler(SamplerBindingType::Filtering)), )); entries.to_vec() @@ -323,10 +335,16 @@ fn layout_entries( pub fn generate_view_layouts( render_device: &RenderDevice, clustered_forward_buffer_binding_type: BufferBindingType, + visibility_ranges_buffer_binding_type: BufferBindingType, ) -> [MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT] { array::from_fn(|i| { let key = MeshPipelineViewLayoutKey::from_bits_truncate(i as u32); - let entries = layout_entries(clustered_forward_buffer_binding_type, key, render_device); + let entries = layout_entries( + clustered_forward_buffer_binding_type, + visibility_ranges_buffer_binding_type, + key, + render_device, + ); #[cfg(debug_assertions)] let texture_count: usize = entries @@ -379,6 +397,7 @@ pub fn prepare_mesh_view_bind_groups( globals_buffer: Res, tonemapping_luts: Res, light_probes_buffer: Res, + visibility_ranges: Res, ) { if let ( Some(view_binding), @@ -387,6 +406,7 @@ pub fn prepare_mesh_view_bind_groups( Some(globals), Some(fog_binding), Some(light_probes_binding), + Some(visibility_ranges_buffer), ) = ( view_uniforms.uniforms.binding(), light_meta.view_gpu_lights.binding(), @@ -394,6 +414,7 @@ pub fn prepare_mesh_view_bind_groups( globals_buffer.buffer.binding(), fog_meta.gpu_fogs.binding(), light_probes_buffer.binding(), + visibility_ranges.buffer().buffer(), ) { for ( entity, @@ -433,7 +454,8 @@ pub fn prepare_mesh_view_bind_groups( (9, globals.clone()), (10, fog_binding.clone()), (11, light_probes_binding.clone()), - (12, ssao_view), + (12, visibility_ranges_buffer.as_entire_binding()), + (13, ssao_view), )); let environment_map_bind_group_entries = RenderViewEnvironmentMapBindGroupEntries::get( @@ -450,9 +472,9 @@ pub fn prepare_mesh_view_bind_groups( sampler, } => { entries = entries.extend_with_indices(( - (13, diffuse_texture_view), - (14, specular_texture_view), - (15, sampler), + (14, diffuse_texture_view), + (15, specular_texture_view), + (16, sampler), )); } RenderViewEnvironmentMapBindGroupEntries::Multiple { @@ -461,9 +483,9 @@ pub fn prepare_mesh_view_bind_groups( sampler, } => { entries = entries.extend_with_indices(( - (13, diffuse_texture_views.as_slice()), - (14, specular_texture_views.as_slice()), - (15, sampler), + (14, diffuse_texture_views.as_slice()), + (15, specular_texture_views.as_slice()), + (16, sampler), )); } } @@ -484,21 +506,21 @@ pub fn prepare_mesh_view_bind_groups( texture_view, sampler, }) => { - entries = entries.extend_with_indices(((16, texture_view), (17, sampler))); + entries = entries.extend_with_indices(((17, texture_view), (18, sampler))); } Some(RenderViewIrradianceVolumeBindGroupEntries::Multiple { ref texture_views, sampler, }) => { entries = entries - .extend_with_indices(((16, texture_views.as_slice()), (17, sampler))); + .extend_with_indices(((17, texture_views.as_slice()), (18, sampler))); } None => {} } let lut_bindings = get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image); - entries = entries.extend_with_indices(((18, lut_bindings.0), (19, lut_bindings.1))); + entries = entries.extend_with_indices(((19, lut_bindings.0), (20, lut_bindings.1))); // When using WebGL, we can't have a depth texture with multisampling let prepass_bindings; @@ -508,7 +530,7 @@ pub fn prepare_mesh_view_bind_groups( for (binding, index) in prepass_bindings .iter() .map(Option::as_ref) - .zip([20, 21, 22, 23]) + .zip([21, 22, 23, 24]) .flat_map(|(b, i)| b.map(|b| (b, i))) { entries = entries.extend_with_indices(((index, binding),)); @@ -524,7 +546,7 @@ pub fn prepare_mesh_view_bind_groups( .unwrap_or(&fallback_image_zero.sampler); entries = - entries.extend_with_indices(((24, transmission_view), (25, transmission_sampler))); + entries.extend_with_indices(((25, transmission_view), (26, transmission_sampler))); commands.entity(entity).insert(MeshViewBindGroup { value: render_device.create_bind_group("mesh_view_bind_group", layout, &entries), diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index 20a3578af4..4fd1f2327d 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -35,58 +35,64 @@ @group(0) @binding(10) var fog: types::Fog; @group(0) @binding(11) var light_probes: types::LightProbes; -@group(0) @binding(12) var screen_space_ambient_occlusion_texture: texture_2d; +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 +@group(0) @binding(12) var visibility_ranges: array>; +#else +@group(0) @binding(12) var visibility_ranges: array>; +#endif + +@group(0) @binding(13) var screen_space_ambient_occlusion_texture: texture_2d; #ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY -@group(0) @binding(13) var diffuse_environment_maps: binding_array, 8u>; -@group(0) @binding(14) var specular_environment_maps: binding_array, 8u>; +@group(0) @binding(14) var diffuse_environment_maps: binding_array, 8u>; +@group(0) @binding(15) var specular_environment_maps: binding_array, 8u>; #else -@group(0) @binding(13) var diffuse_environment_map: texture_cube; -@group(0) @binding(14) var specular_environment_map: texture_cube; +@group(0) @binding(14) var diffuse_environment_map: texture_cube; +@group(0) @binding(15) var specular_environment_map: texture_cube; #endif -@group(0) @binding(15) var environment_map_sampler: sampler; +@group(0) @binding(16) var environment_map_sampler: sampler; #ifdef IRRADIANCE_VOLUMES_ARE_USABLE #ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY -@group(0) @binding(16) var irradiance_volumes: binding_array, 8u>; +@group(0) @binding(17) var irradiance_volumes: binding_array, 8u>; #else -@group(0) @binding(16) var irradiance_volume: texture_3d; +@group(0) @binding(17) var irradiance_volume: texture_3d; #endif -@group(0) @binding(17) var irradiance_volume_sampler: sampler; +@group(0) @binding(18) var irradiance_volume_sampler: sampler; #endif // NB: If you change these, make sure to update `tonemapping_shared.wgsl` too. -@group(0) @binding(18) var dt_lut_texture: texture_3d; -@group(0) @binding(19) var dt_lut_sampler: sampler; +@group(0) @binding(19) var dt_lut_texture: texture_3d; +@group(0) @binding(20) var dt_lut_sampler: sampler; #ifdef MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(20) var depth_prepass_texture: texture_depth_multisampled_2d; +@group(0) @binding(21) var depth_prepass_texture: texture_depth_multisampled_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(21) var normal_prepass_texture: texture_multisampled_2d; +@group(0) @binding(22) var normal_prepass_texture: texture_multisampled_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(22) var motion_vector_prepass_texture: texture_multisampled_2d; +@group(0) @binding(23) var motion_vector_prepass_texture: texture_multisampled_2d; #endif // MOTION_VECTOR_PREPASS #else // MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(20) var depth_prepass_texture: texture_depth_2d; +@group(0) @binding(21) var depth_prepass_texture: texture_depth_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(21) var normal_prepass_texture: texture_2d; +@group(0) @binding(22) var normal_prepass_texture: texture_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(22) var motion_vector_prepass_texture: texture_2d; +@group(0) @binding(23) var motion_vector_prepass_texture: texture_2d; #endif // MOTION_VECTOR_PREPASS #endif // MULTISAMPLED #ifdef DEFERRED_PREPASS -@group(0) @binding(23) var deferred_prepass_texture: texture_2d; +@group(0) @binding(24) var deferred_prepass_texture: texture_2d; #endif // DEFERRED_PREPASS -@group(0) @binding(24) var view_transmission_texture: texture_2d; -@group(0) @binding(25) var view_transmission_sampler: sampler; +@group(0) @binding(25) var view_transmission_texture: texture_2d; +@group(0) @binding(26) var view_transmission_sampler: sampler; diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index 7421f1e381..7b94f3cdf8 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -11,6 +11,7 @@ #else #import bevy_pbr::{ forward_io::{VertexOutput, FragmentOutput}, + pbr_functions, pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing}, pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT, } @@ -34,6 +35,12 @@ fn fragment( let is_front = true; #endif + // If we're in the crossfade section of a visibility range, conditionally + // discard the fragment according to the visibility pattern. +#ifdef VISIBILITY_RANGE_DITHER + pbr_functions::visibility_range_dither(in.position, in.visibility_range_dither); +#endif + // generate a PbrInput struct from the StandardMaterial bindings var pbr_input = pbr_input_from_standard_material(in, is_front); diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 51a914ced4..9f6b5fb240 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -21,6 +21,55 @@ #import bevy_core_pipeline::tonemapping::{screen_space_dither, powsafe, tone_mapping} +// This is the standard 4x4 ordered dithering pattern from [1]. +// +// We can't use `array, 4>` because they can't be indexed dynamically +// due to Naga limitations. So instead we pack into a single `vec4` and extract +// individual bytes. +// +// [1]: https://en.wikipedia.org/wiki/Ordered_dithering#Threshold_map +const DITHER_THRESHOLD_MAP: vec4 = vec4( + 0x0a020800, + 0x060e040c, + 0x09010b03, + 0x050d070f +); + +// Processes a visibility range dither value and discards the fragment if +// needed. +// +// Visibility ranges, also known as HLODs, are crossfades between different +// levels of detail. +// +// The `dither` value ranges from [-16, 16]. When zooming out, positive values +// are used for meshes that are in the process of disappearing, while negative +// values are used for meshes that are in the process of appearing. In other +// words, when the camera is moving backwards, the `dither` value counts up from +// -16 to 0 when the object is fading in, stays at 0 while the object is +// visible, and then counts up to 16 while the object is fading out. +// Distinguishing between negative and positive values allows the dither +// patterns for different LOD levels of a single mesh to mesh together properly. +#ifdef VISIBILITY_RANGE_DITHER +fn visibility_range_dither(frag_coord: vec4, dither: i32) { + // If `dither` is 0, the object is visible. + if (dither == 0) { + return; + } + + // If `dither` is less than -15 or greater than 15, the object is culled. + if (dither <= -16 || dither >= 16) { + discard; + } + + // Otherwise, check the dither pattern. + let coords = vec2(floor(frag_coord.xy)) % 4u; + let threshold = i32((DITHER_THRESHOLD_MAP[coords.y] >> (coords.x * 8)) & 0xff); + if ((dither >= 0 && dither + threshold >= 16) || (dither < 0 && 1 + dither + threshold <= 0)) { + discard; + } +} +#endif + fn alpha_discard(material: pbr_types::StandardMaterial, output_color: vec4) -> vec4 { var color = output_color; let alpha_mode = material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index dbe8779c67..83e934e78d 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -107,7 +107,11 @@ impl Plugin for ViewPlugin { .register_type::() .init_resource::() // NOTE: windows.is_changed() handles cases where a window was resized - .add_plugins((ExtractResourcePlugin::::default(), VisibilityPlugin)); + .add_plugins(( + ExtractResourcePlugin::::default(), + VisibilityPlugin, + VisibilityRangePlugin, + )); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::().add_systems( diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index f10d47b710..f23a01e441 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -1,14 +1,15 @@ +mod range; mod render_layers; use std::any::TypeId; -use bevy_derive::Deref; -use bevy_ecs::query::QueryFilter; +pub use range::*; pub use render_layers::*; use bevy_app::{Plugin, PostUpdate}; use bevy_asset::{Assets, Handle}; -use bevy_ecs::prelude::*; +use bevy_derive::Deref; +use bevy_ecs::{prelude::*, query::QueryFilter}; use bevy_hierarchy::{Children, Parent}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_transform::{components::GlobalTransform, TransformSystem}; @@ -395,6 +396,7 @@ fn reset_view_visibility(mut query: Query<&mut ViewVisibility>) { pub fn check_visibility( mut thread_queues: Local>>, mut view_query: Query<( + Entity, &mut VisibleEntities, &Frustum, Option<&RenderLayers>, @@ -410,13 +412,18 @@ pub fn check_visibility( Option<&Aabb>, &GlobalTransform, Has, + Has, ), QF, >, + visible_entity_ranges: Option>, ) where QF: QueryFilter + 'static, { - for (mut visible_entities, frustum, maybe_view_mask, camera, no_cpu_culling) in &mut view_query + let visible_entity_ranges = visible_entity_ranges.as_deref(); + + for (view, mut visible_entities, frustum, maybe_view_mask, camera, no_cpu_culling) in + &mut view_query { if !camera.is_active { continue; @@ -435,6 +442,7 @@ pub fn check_visibility( maybe_model_aabb, transform, no_frustum_culling, + has_visibility_range, ) = query_item; // Skip computing visibility for entities that are configured to be hidden. @@ -448,6 +456,15 @@ pub fn check_visibility( return; } + // If outside of the visibility range, cull. + if has_visibility_range + && visible_entity_ranges.is_some_and(|visible_entity_ranges| { + !visible_entity_ranges.entity_is_in_range_of_view(entity, view) + }) + { + return; + } + // If we have an aabb, do frustum culling if !no_frustum_culling && !no_cpu_culling { if let Some(model_aabb) = maybe_model_aabb { diff --git a/crates/bevy_render/src/view/visibility/range.rs b/crates/bevy_render/src/view/visibility/range.rs new file mode 100644 index 0000000000..62485e42bd --- /dev/null +++ b/crates/bevy_render/src/view/visibility/range.rs @@ -0,0 +1,437 @@ +//! Specific distances from the camera in which entities are visible, also known +//! as *hierarchical levels of detail* or *HLOD*s. + +use std::{ + hash::{Hash, Hasher}, + ops::Range, +}; + +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{Changed, With}, + schedule::IntoSystemConfigs as _, + system::{Query, Res, ResMut, Resource}, +}; +use bevy_math::{vec4, FloatOrd, Vec4}; +use bevy_reflect::Reflect; +use bevy_transform::components::GlobalTransform; +use bevy_utils::{prelude::default, EntityHashMap, HashMap}; +use nonmax::NonMaxU16; +use wgpu::BufferUsages; + +use crate::{ + camera::Camera, + render_resource::BufferVec, + renderer::{RenderDevice, RenderQueue}, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, +}; + +use super::{check_visibility, VisibilitySystems, WithMesh}; + +/// We need at least 4 storage buffer bindings available to enable the +/// visibility range buffer. +/// +/// Even though we only use one storage buffer, the first 3 available storage +/// buffers will go to various light-related buffers. We will grab the fourth +/// buffer slot. +pub const VISIBILITY_RANGES_STORAGE_BUFFER_COUNT: u32 = 4; + +/// A plugin that enables [`VisibilityRange`]s, which allow entities to be +/// hidden or shown based on distance to the camera. +pub struct VisibilityRangePlugin; + +impl Plugin for VisibilityRangePlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .init_resource::() + .add_systems( + PostUpdate, + check_visibility_ranges + .in_set(VisibilitySystems::CheckVisibility) + .before(check_visibility::), + ); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .add_systems(ExtractSchedule, extract_visibility_ranges) + .add_systems( + Render, + write_render_visibility_ranges.in_set(RenderSet::PrepareResourcesFlush), + ); + } +} + +/// Specifies the range of distances that this entity must be from the camera in +/// order to be rendered. +/// +/// This is also known as *hierarchical level of detail* or *HLOD*. +/// +/// Use this component when you want to render a high-polygon mesh when the +/// camera is close and a lower-polygon mesh when the camera is far away. This +/// is a common technique for improving performance, because fine details are +/// hard to see in a mesh at a distance. To avoid an artifact known as *popping* +/// between levels, each level has a *margin*, within which the object +/// transitions gradually from invisible to visible using a dithering effect. +/// +/// You can also use this feature to replace multiple meshes with a single mesh +/// when the camera is distant. This is the reason for the term "*hierarchical* +/// level of detail". Reducing the number of meshes can be useful for reducing +/// drawcall count. Note that you must place the [`VisibilityRange`] component +/// on each entity you want to be part of a LOD group, as [`VisibilityRange`] +/// isn't automatically propagated down to children. +/// +/// A typical use of this feature might look like this: +/// +/// | Entity | `start_margin` | `end_margin` | +/// |-------------------------|----------------|--------------| +/// | Root | N/A | N/A | +/// | ├─ High-poly mesh | [0, 0) | [20, 25) | +/// | ├─ Low-poly mesh | [20, 25) | [70, 75) | +/// | └─ Billboard *imposter* | [70, 75) | [150, 160) | +/// +/// With this setup, the user will see a high-poly mesh when the camera is +/// closer than 20 units. As the camera zooms out, between 20 units to 25 units, +/// the high-poly mesh will gradually fade to a low-poly mesh. When the camera +/// is 70 to 75 units away, the low-poly mesh will fade to a single textured +/// quad. And between 150 and 160 units, the object fades away entirely. Note +/// that the `end_margin` of a higher LOD is always identical to the +/// `start_margin` of the next lower LOD; this is important for the crossfade +/// effect to function properly. +#[derive(Component, Clone, PartialEq, Reflect)] +pub struct VisibilityRange { + /// The range of distances, in world units, between which this entity will + /// smoothly fade into view as the camera zooms out. + /// + /// If the start and end of this range are identical, the transition will be + /// abrupt, with no crossfading. + /// + /// `start_margin.end` must be less than or equal to `end_margin.start`. + pub start_margin: Range, + + /// The range of distances, in world units, between which this entity will + /// smoothly fade out of view as the camera zooms out. + /// + /// If the start and end of this range are identical, the transition will be + /// abrupt, with no crossfading. + /// + /// `end_margin.start` must be greater than or equal to `start_margin.end`. + pub end_margin: Range, +} + +impl Eq for VisibilityRange {} + +impl Hash for VisibilityRange { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + FloatOrd(self.start_margin.start).hash(state); + FloatOrd(self.start_margin.end).hash(state); + FloatOrd(self.end_margin.start).hash(state); + FloatOrd(self.end_margin.end).hash(state); + } +} + +impl VisibilityRange { + /// Creates a new *abrupt* visibility range, with no crossfade. + /// + /// There will be no crossfade; the object will immediately vanish if the + /// camera is closer than `start` units or farther than `end` units from the + /// model. + /// + /// The `start` value must be less than or equal to the `end` value. + #[inline] + pub fn abrupt(start: f32, end: f32) -> Self { + Self { + start_margin: start..start, + end_margin: end..end, + } + } + + /// Returns true if both the start and end transitions for this range are + /// abrupt: that is, there is no crossfading. + #[inline] + pub fn is_abrupt(&self) -> bool { + self.start_margin.start == self.start_margin.end + && self.end_margin.start == self.end_margin.end + } + + /// Returns true if the object will be visible at all, given a camera + /// `camera_distance` units away. + /// + /// Any amount of visibility, even with the heaviest dithering applied, is + /// considered visible according to this check. + #[inline] + pub fn is_visible_at_all(&self, camera_distance: f32) -> bool { + camera_distance >= self.start_margin.start && camera_distance < self.end_margin.end + } + + /// Returns true if the object is completely invisible, given a camera + /// `camera_distance` units away. + /// + /// This is equivalent to `!VisibilityRange::is_visible_at_all()`. + #[inline] + pub fn is_culled(&self, camera_distance: f32) -> bool { + !self.is_visible_at_all(camera_distance) + } +} + +/// Stores information related to [`VisibilityRange`]s in the render world. +#[derive(Resource)] +pub struct RenderVisibilityRanges { + /// Information corresponding to each entity. + entities: EntityHashMap, + + /// Maps a [`VisibilityRange`] to its index within the `buffer`. + /// + /// This map allows us to deduplicate identical visibility ranges, which + /// saves GPU memory. + range_to_index: HashMap, + + /// The GPU buffer that stores [`VisibilityRange`]s. + /// + /// Each [`Vec4`] contains the start margin start, start margin end, end + /// margin start, and end margin end distances, in that order. + buffer: BufferVec, + + /// True if the buffer has been changed since the last frame and needs to be + /// reuploaded to the GPU. + buffer_dirty: bool, +} + +/// Per-entity information related to [`VisibilityRange`]s. +struct RenderVisibilityEntityInfo { + /// The index of the range within the GPU buffer. + buffer_index: NonMaxU16, + /// True if the range is abrupt: i.e. has no crossfade. + is_abrupt: bool, +} + +impl Default for RenderVisibilityRanges { + fn default() -> Self { + Self { + entities: default(), + range_to_index: default(), + buffer: BufferVec::new( + BufferUsages::STORAGE | BufferUsages::UNIFORM | BufferUsages::VERTEX, + ), + buffer_dirty: true, + } + } +} + +impl RenderVisibilityRanges { + /// Clears out the [`RenderVisibilityRanges`] in preparation for a new + /// frame. + fn clear(&mut self) { + self.entities.clear(); + self.range_to_index.clear(); + self.buffer.clear(); + self.buffer_dirty = true; + } + + /// Inserts a new entity into the [`RenderVisibilityRanges`]. + fn insert(&mut self, entity: Entity, visibility_range: &VisibilityRange) { + // Grab a slot in the GPU buffer, or take the existing one if there + // already is one. + let buffer_index = *self + .range_to_index + .entry(visibility_range.clone()) + .or_insert_with(|| { + NonMaxU16::try_from(self.buffer.push(vec4( + visibility_range.start_margin.start, + visibility_range.start_margin.end, + visibility_range.end_margin.start, + visibility_range.end_margin.end, + )) as u16) + .unwrap_or_default() + }); + + self.entities.insert( + entity, + RenderVisibilityEntityInfo { + buffer_index, + is_abrupt: visibility_range.is_abrupt(), + }, + ); + } + + /// Returns the index in the GPU buffer corresponding to the visible range + /// for the given entity. + /// + /// If the entity has no visible range, returns `None`. + #[inline] + pub fn lod_index_for_entity(&self, entity: Entity) -> Option { + self.entities.get(&entity).map(|info| info.buffer_index) + } + + /// Returns true if the entity has a visibility range and it isn't abrupt: + /// i.e. if it has a crossfade. + #[inline] + pub fn entity_has_crossfading_visibility_ranges(&self, entity: Entity) -> bool { + self.entities + .get(&entity) + .is_some_and(|info| !info.is_abrupt) + } + + /// Returns a reference to the GPU buffer that stores visibility ranges. + #[inline] + pub fn buffer(&self) -> &BufferVec { + &self.buffer + } +} + +/// Stores which entities are in within the [`VisibilityRange`]s of views. +/// +/// This doesn't store the results of frustum or occlusion culling; use +/// [`super::ViewVisibility`] for that. Thus entities in this list may not +/// actually be visible. +/// +/// For efficiency, these tables only store entities that have +/// [`VisibilityRange`] components. Entities without such a component won't be +/// in these tables at all. +/// +/// The table is indexed by entity and stores a 32-bit bitmask with one bit for +/// each camera, where a 0 bit corresponds to "out of range" and a 1 bit +/// corresponds to "in range". Hence it's limited to storing information for 32 +/// views. +#[derive(Resource, Default)] +pub struct VisibleEntityRanges { + /// Stores which bit index each view corresponds to. + views: EntityHashMap, + + /// Stores a bitmask in which each view has a single bit. + /// + /// A 0 bit for a view corresponds to "out of range"; a 1 bit corresponds to + /// "in range". + entities: EntityHashMap, +} + +impl VisibleEntityRanges { + /// Clears out the [`VisibleEntityRanges`] in preparation for a new frame. + fn clear(&mut self) { + self.views.clear(); + self.entities.clear(); + } + + /// Returns true if the entity is in range of the given camera. + /// + /// This only checks [`VisibilityRange`]s and doesn't perform any frustum or + /// occlusion culling. Thus the entity might not *actually* be visible. + /// + /// The entity is assumed to have a [`VisibilityRange`] component. If the + /// entity doesn't have that component, this method will return false. + #[inline] + pub fn entity_is_in_range_of_view(&self, entity: Entity, view: Entity) -> bool { + let Some(visibility_bitmask) = self.entities.get(&entity) else { + return false; + }; + let Some(view_index) = self.views.get(&view) else { + return false; + }; + (visibility_bitmask & (1 << view_index)) != 0 + } + + /// Returns true if the entity is in range of any view. + /// + /// This only checks [`VisibilityRange`]s and doesn't perform any frustum or + /// occlusion culling. Thus the entity might not *actually* be visible. + /// + /// The entity is assumed to have a [`VisibilityRange`] component. If the + /// entity doesn't have that component, this method will return false. + #[inline] + pub fn entity_is_in_range_of_any_view(&self, entity: Entity) -> bool { + self.entities.contains_key(&entity) + } +} + +/// Checks all entities against all views in order to determine which entities +/// with [`VisibilityRange`]s are potentially visible. +/// +/// This only checks distance from the camera and doesn't frustum or occlusion +/// cull. +pub fn check_visibility_ranges( + mut visible_entity_ranges: ResMut, + view_query: Query<(Entity, &GlobalTransform), With>, + mut entity_query: Query<(Entity, &GlobalTransform, &VisibilityRange)>, +) { + visible_entity_ranges.clear(); + + // Early out if the visibility range feature isn't in use. + if entity_query.is_empty() { + return; + } + + // Assign an index to each view. + let mut views = vec![]; + for (view, view_transform) in view_query.iter().take(32) { + let view_index = views.len() as u8; + visible_entity_ranges.views.insert(view, view_index); + views.push((view, view_transform.translation_vec3a())); + } + + // Check each entity/view pair. Only consider entities with + // [`VisibilityRange`] components. + for (entity, entity_transform, visibility_range) in entity_query.iter_mut() { + let mut visibility = 0; + for (view_index, &(_, view_position)) in views.iter().enumerate() { + if visibility_range + .is_visible_at_all((view_position - entity_transform.translation_vec3a()).length()) + { + visibility |= 1 << view_index; + } + } + + // Invisible entities have no entry at all in the hash map. This speeds + // up checks slightly in this common case. + if visibility != 0 { + visible_entity_ranges.entities.insert(entity, visibility); + } + } +} + +/// Extracts all [`VisibilityRange`] components from the main world to the +/// render world and inserts them into [`RenderVisibilityRanges`]. +pub fn extract_visibility_ranges( + mut render_visibility_ranges: ResMut, + visibility_ranges_query: Extract>, + changed_ranges_query: Extract>>, +) { + if changed_ranges_query.is_empty() { + return; + } + + render_visibility_ranges.clear(); + for (entity, visibility_range) in visibility_ranges_query.iter() { + render_visibility_ranges.insert(entity, visibility_range); + } +} + +/// Writes the [`RenderVisibilityRanges`] table to the GPU. +pub fn write_render_visibility_ranges( + render_device: Res, + render_queue: Res, + mut render_visibility_ranges: ResMut, +) { + // If there haven't been any changes, early out. + if !render_visibility_ranges.buffer_dirty { + return; + } + + // If the buffer is empty, push *something* so that we allocate it. + if render_visibility_ranges.buffer.is_empty() { + render_visibility_ranges.buffer.push(default()); + } + + // Schedule the write. + render_visibility_ranges + .buffer + .write_buffer(&render_device, &render_queue); + render_visibility_ranges.buffer_dirty = false; +} diff --git a/examples/3d/visibility_range.rs b/examples/3d/visibility_range.rs new file mode 100644 index 0000000000..19dbffcbf4 --- /dev/null +++ b/examples/3d/visibility_range.rs @@ -0,0 +1,337 @@ +//! Demonstrates visibility ranges, also known as HLODs. + +use std::f32::consts::PI; + +use bevy::{ + input::mouse::MouseWheel, + math::vec3, + pbr::{light_consts::lux::FULL_DAYLIGHT, CascadeShadowConfigBuilder}, + prelude::*, + render::view::VisibilityRange, +}; + +// Where the camera is focused. +const CAMERA_FOCAL_POINT: Vec3 = vec3(0.0, 0.3, 0.0); +// Speed in units per frame. +const CAMERA_KEYBOARD_ZOOM_SPEED: f32 = 0.05; +// Speed in radians per frame. +const CAMERA_KEYBOARD_PAN_SPEED: f32 = 0.01; +// Speed in units per frame. +const CAMERA_MOUSE_MOVEMENT_SPEED: f32 = 0.25; +// The minimum distance that the camera is allowed to be from the model. +const MIN_ZOOM_DISTANCE: f32 = 0.5; + +// The visibility ranges for high-poly and low-poly models respectively, when +// both models are being shown. +static NORMAL_VISIBILITY_RANGE_HIGH_POLY: VisibilityRange = VisibilityRange { + start_margin: 0.0..0.0, + end_margin: 3.0..4.0, +}; +static NORMAL_VISIBILITY_RANGE_LOW_POLY: VisibilityRange = VisibilityRange { + start_margin: 3.0..4.0, + end_margin: 8.0..9.0, +}; + +// A visibility model that we use to always show a model (until the camera is so +// far zoomed out that it's culled entirely). +static SINGLE_MODEL_VISIBILITY_RANGE: VisibilityRange = VisibilityRange { + start_margin: 0.0..0.0, + end_margin: 8.0..9.0, +}; + +// A visibility range that we use to completely hide a model. +static INVISIBLE_VISIBILITY_RANGE: VisibilityRange = VisibilityRange { + start_margin: 0.0..0.0, + end_margin: 0.0..0.0, +}; + +// Allows us to identify the main model. +#[derive(Component, Debug, Clone, Copy, PartialEq)] +enum MainModel { + // The high-poly version. + HighPoly, + // The low-poly version. + LowPoly, +} + +// The current mode. +#[derive(Default, Resource)] +struct AppStatus { + // Whether to show only one model. + show_one_model_only: Option, +} + +// Sets up the app. +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Visibility Range Example".into(), + ..default() + }), + ..default() + })) + .init_resource::() + .add_systems(Startup, setup) + .add_systems( + Update, + ( + move_camera, + set_visibility_ranges, + update_help_text, + update_mode, + ), + ) + .run(); +} + +// Set up a simple 3D scene. Load the two meshes. +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + asset_server: Res, + app_status: Res, +) { + // Spawn a plane. + commands.spawn(PbrBundle { + mesh: meshes.add(Plane3d::default().mesh().size(50.0, 50.0)), + material: materials.add(Color::srgb(0.1, 0.2, 0.1)), + ..default() + }); + + // Spawn the two HLODs. + + commands + .spawn(SceneBundle { + scene: asset_server.load("models/FlightHelmet/FlightHelmet.gltf#Scene0"), + ..default() + }) + .insert(MainModel::HighPoly); + + commands + .spawn(SceneBundle { + scene: asset_server.load("models/FlightHelmetLowPoly/FlightHelmetLowPoly.gltf#Scene0"), + ..default() + }) + .insert(MainModel::LowPoly); + + // Spawn a light. + commands.spawn(DirectionalLightBundle { + directional_light: DirectionalLight { + illuminance: FULL_DAYLIGHT, + shadows_enabled: true, + ..default() + }, + transform: Transform::from_rotation(Quat::from_euler( + EulerRot::ZYX, + 0.0, + PI * -0.15, + PI * -0.15, + )), + cascade_shadow_config: CascadeShadowConfigBuilder { + maximum_distance: 30.0, + first_cascade_far_bound: 0.9, + ..default() + } + .into(), + ..default() + }); + + // Spawn a camera. + commands + .spawn(Camera3dBundle { + transform: Transform::from_xyz(0.7, 0.7, 1.0).looking_at(CAMERA_FOCAL_POINT, Vec3::Y), + ..default() + }) + .insert(EnvironmentMapLight { + diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), + specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), + intensity: 150.0, + }); + + // Create the text. + commands.spawn( + TextBundle { + text: app_status.create_text(&asset_server), + ..TextBundle::default() + } + .with_style(Style { + position_type: PositionType::Absolute, + bottom: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }), + ); +} + +// We need to add the `VisibilityRange` components manually, as glTF currently +// has no way to specify visibility ranges. This system watches for new meshes, +// determines which `Scene` they're under, and adds the `VisibilityRange` +// component as appropriate. +fn set_visibility_ranges( + mut commands: Commands, + mut new_meshes: Query>>, + parents: Query<(Option<&Parent>, Option<&MainModel>)>, +) { + // Loop over each newly-added mesh. + for new_mesh in new_meshes.iter_mut() { + // Search for the nearest ancestor `MainModel` component. + let (mut current, mut main_model) = (new_mesh, None); + while let Ok((parent, maybe_main_model)) = parents.get(current) { + if let Some(model) = maybe_main_model { + main_model = Some(model); + break; + } + match parent { + Some(parent) => current = **parent, + None => break, + } + } + + // Add the `VisibilityRange` component. + match main_model { + Some(MainModel::HighPoly) => { + commands + .entity(new_mesh) + .insert(NORMAL_VISIBILITY_RANGE_HIGH_POLY.clone()) + .insert(MainModel::HighPoly); + } + Some(MainModel::LowPoly) => { + commands + .entity(new_mesh) + .insert(NORMAL_VISIBILITY_RANGE_LOW_POLY.clone()) + .insert(MainModel::LowPoly); + } + None => {} + } + } +} + +// Process the movement controls. +fn move_camera( + keyboard_input: Res>, + mut mouse_wheel_events: EventReader, + mut cameras: Query<&mut Transform, With>, +) { + let (mut zoom_delta, mut theta_delta) = (0.0, 0.0); + + // Process zoom in and out via the keyboard. + if keyboard_input.pressed(KeyCode::KeyW) || keyboard_input.pressed(KeyCode::ArrowUp) { + zoom_delta -= CAMERA_KEYBOARD_ZOOM_SPEED; + } else if keyboard_input.pressed(KeyCode::KeyS) || keyboard_input.pressed(KeyCode::ArrowDown) { + zoom_delta += CAMERA_KEYBOARD_ZOOM_SPEED; + } + + // Process left and right pan via the keyboard. + if keyboard_input.pressed(KeyCode::KeyA) || keyboard_input.pressed(KeyCode::ArrowLeft) { + theta_delta -= CAMERA_KEYBOARD_PAN_SPEED; + } else if keyboard_input.pressed(KeyCode::KeyD) || keyboard_input.pressed(KeyCode::ArrowRight) { + theta_delta += CAMERA_KEYBOARD_PAN_SPEED; + } + + // Process zoom in and out via the mouse wheel. + for event in mouse_wheel_events.read() { + zoom_delta -= event.y * CAMERA_MOUSE_MOVEMENT_SPEED; + } + + // Update the camera transform. + for transform in cameras.iter_mut() { + let transform = transform.into_inner(); + + let direction = transform.translation.normalize_or_zero(); + let magnitude = transform.translation.length(); + + let new_direction = Mat3::from_rotation_y(theta_delta) * direction; + let new_magnitude = (magnitude + zoom_delta).max(MIN_ZOOM_DISTANCE); + + transform.translation = new_direction * new_magnitude; + transform.look_at(CAMERA_FOCAL_POINT, Vec3::Y); + } +} + +// Toggles modes if the user requests. +fn update_mode( + mut meshes: Query<(&mut VisibilityRange, &MainModel)>, + keyboard_input: Res>, + mut app_status: ResMut, +) { + // Toggle the mode as requested. + if keyboard_input.just_pressed(KeyCode::Digit1) || keyboard_input.just_pressed(KeyCode::Numpad1) + { + app_status.show_one_model_only = None; + } else if keyboard_input.just_pressed(KeyCode::Digit2) + || keyboard_input.just_pressed(KeyCode::Numpad2) + { + app_status.show_one_model_only = Some(MainModel::HighPoly); + } else if keyboard_input.just_pressed(KeyCode::Digit3) + || keyboard_input.just_pressed(KeyCode::Numpad3) + { + app_status.show_one_model_only = Some(MainModel::LowPoly); + } else { + return; + } + + // Update the visibility ranges as appropriate. + for (mut visibility_range, main_model) in meshes.iter_mut() { + *visibility_range = match (main_model, app_status.show_one_model_only) { + (&MainModel::HighPoly, Some(MainModel::LowPoly)) + | (&MainModel::LowPoly, Some(MainModel::HighPoly)) => { + INVISIBLE_VISIBILITY_RANGE.clone() + } + (&MainModel::HighPoly, Some(MainModel::HighPoly)) + | (&MainModel::LowPoly, Some(MainModel::LowPoly)) => { + SINGLE_MODEL_VISIBILITY_RANGE.clone() + } + (&MainModel::HighPoly, None) => NORMAL_VISIBILITY_RANGE_HIGH_POLY.clone(), + (&MainModel::LowPoly, None) => NORMAL_VISIBILITY_RANGE_LOW_POLY.clone(), + } + } +} + +// A system that updates the help text. +fn update_help_text( + mut text_query: Query<&mut Text>, + app_status: Res, + asset_server: Res, +) { + for mut text in text_query.iter_mut() { + *text = app_status.create_text(&asset_server); + } +} + +impl AppStatus { + // Creates and returns help text reflecting the app status. + fn create_text(&self, asset_server: &AssetServer) -> Text { + Text::from_section( + format!( + "\ +{} (1) Switch from high-poly to low-poly based on camera distance +{} (2) Show only the high-poly model +{} (3) Show only the low-poly model +Press 1, 2, or 3 to switch which model is shown +Press WASD or use the mouse wheel to move the camera", + if self.show_one_model_only.is_none() { + '>' + } else { + ' ' + }, + if self.show_one_model_only == Some(MainModel::HighPoly) { + '>' + } else { + ' ' + }, + if self.show_one_model_only == Some(MainModel::LowPoly) { + '>' + } else { + ' ' + }, + ), + TextStyle { + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 24.0, + ..default() + }, + ) + } +} diff --git a/examples/README.md b/examples/README.md index 8e696659b8..733df8933f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -160,6 +160,7 @@ Example | Description [Two Passes](../examples/3d/two_passes.rs) | Renders two 3d passes to the same window from different perspectives [Update glTF Scene](../examples/3d/update_gltf_scene.rs) | Update a scene from a glTF file, either by spawning the scene as a child of another entity, or by accessing the entities of the scene [Vertex Colors](../examples/3d/vertex_colors.rs) | Shows the use of vertex colors +[Visibility range](../examples/3d/visibility_range.rs) | Demonstrates visibility ranges [Wireframe](../examples/3d/wireframe.rs) | Showcases wireframe rendering ## Animation