{"record":{"id":"3fe31e5eec2ac075","repo":"bevyengine/bevy","slug":"gltf-model-must-be-a-tree-found-cycle-instead-at","errorCode":null,"errorMessage":"GLTF model must be a tree, found cycle instead at node indices: {0:?}","messagePattern":"GLTF model must be a tree, found cycle instead at node indices: (.+?)","errorType":"exception","errorClass":"GltfError","httpStatus":null,"severity":"error","filePath":"crates/bevy_gltf/src/loader/mod.rs","lineNumber":134,"sourceCode":"    InvalidImageUri(String, ParseAssetPathError),\n    /// Failed to read bytes from an asset path.\n    #[error(\"failed to read bytes from an asset path: {0}\")]\n    ReadAssetBytesError(#[from] ReadAssetBytesError),\n    /// Failed to load asset from an asset path.\n    #[error(\"failed to load asset from an asset path: {0}\")]\n    AssetLoadError(#[from] AssetLoadError),\n    /// Missing sampler for an animation.\n    #[error(\"Missing sampler for animation {0}\")]\n    #[from(ignore)]\n    MissingAnimationSampler(usize),\n    /// Failed to generate tangents.\n    #[error(\"failed to generate tangents: {0}\")]\n    GenerateTangentsError(#[from] bevy_mesh::GenerateTangentsError),\n    /// Failed to generate morph targets.\n    #[error(\"failed to generate morph targets: {0}\")]\n    MorphTarget(#[from] bevy_mesh::morph::MorphBuildError),\n    /// Circular children in Nodes\n    #[error(\"GLTF model must be a tree, found cycle instead at node indices: {0:?}\")]\n    #[from(ignore)]\n    CircularChildren(String),\n    /// Failed to load a file.\n    #[error(\"failed to load file: {0}\")]\n    Io(#[from] Error),\n}\n\n/// Loads glTF files with all of their data as their corresponding bevy representations.\n#[derive(TypePath)]\npub struct GltfLoader {\n    /// List of compressed image formats handled by the loader.\n    pub supported_compressed_formats: CompressedImageFormats,\n    /// Custom vertex attributes that will be recognized when loading a glTF file.\n    ///\n    /// Keys must be the attribute names as found in the glTF data, which must start with an underscore.\n    /// See [this section of the glTF specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview)\n    /// for additional details on custom attributes.\n    pub custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>,","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/bevyengine/bevy/blob/396ca727080776bd313bb892423b7d94e03b81b4/crates/bevy_gltf/src/loader/mod.rs#L116-L152","documentation":"check_is_part_of_cycle (crates/bevy_gltf/src/loader/gltf_ext/scene.rs:47-68) walks the node graph with a FixedBitSet; if any node index is revisited while still on the current DFS path, it returns CircularChildren(\"glTF nodes form a cycle: a -> b -> ... -> n\"). Bevy requires the glTF node hierarchy to be a tree, matching the spec's rule that nodes must not be their own descendants.","triggerScenarios":"Node A lists B in children while B (or its descendant) lists A; a node listing itself as a child; hand-edited or programmatically generated scene graphs with bidirectional parenting.","commonSituations":"Manual JSON surgery to 'share' children between parents, scene-graph exporters that encode DAGs (shared instances) as cycles, corrupted files.","solutions":["Run gltf-validator — it reports node cycles explicitly.","Fix the hierarchy in a DCC: parent-child links must be strictly acyclic; shared nodes must be duplicated, not cross-linked.","If the DAG sharing was intentional, instantiate the shared subtree as separate root-level nodes instead."],"exampleFix":"// (before) cycle: node0 -> node1, node1 -> node0\n\"nodes\": [\n  { \"children\": [1] },\n  { \"children\": [0] }\n]\n// (after) tree: node1 parented under node0 only\n\"nodes\": [\n  { \"children\": [1] },\n  {}\n]","handlingStrategy":"validation","validationCode":"fn node_graph_is_tree(doc: &gltf::Document) -> bool {\n    fn visit(node: &gltf::scene::Node, seen: &mut std::collections::HashSet<usize>) -> bool {\n        if !seen.insert(node.index()) { return false; }\n        let ok = node.children().all(|c| visit(&c, seen));\n        seen.remove(&node.index());\n        ok\n    }\n    doc.nodes().all(|n| {\n        let mut seen = std::collections::HashSet::new();\n        seen.insert(n.index());\n        n.children().all(|c| visit(&c, &mut seen))\n    })\n}","typeGuard":null,"tryCatchPattern":"match err {\n    GltfError::CircularChildren(chain) => {\n        error!(\"node hierarchy has a cycle: {chain}; fix parenting in the source file\");\n    }\n    other => return Err(other.into()),\n}","preventionTips":["Never hand-link children bidirectionally in glTF JSON.","Use gltf-validator; it detects node cycles.","Duplicate shared subtrees instead of encoding DAGs as cycles."],"tags":["gltf","scene-graph","cycle","hierarchy","bevy"],"backgroundTag":"scene-graph-cycle-detected","analyzedSha":"396ca727080776bd313bb892423b7d94e03b81b4","analyzedAt":"2026-08-20T16:12:39.808Z","contentChangedAt":"2026-08-20T16:12:39.808Z","schemaVersion":2},"datasetVersion":"2026-09-09T06:17:21.866Z"}