Scene graph (pgraph)
This cluster is the beating heart of Panda3D: the scene-graph data structure (PandaNode and its many subclasses), the immutable, reference-counted, slot-indexed state system (RenderState/RenderAttrib, TransformState, RenderEffect/RenderEffects), the high-level navigation/editing API (NodePath), and the per-frame cull traversal that walks the graph, accumulates net state/transform, view-frustum-culls, and emits drawable Geoms into sorted output queues (CullBins). panda/src/pgraph holds the core machinery; panda/src/pgraphnodes holds higher-level semantic nodes (lights, LOD, sequence/switch, callback, compute); panda/src/cull holds the concrete CullBin sorting implementations and the two CullHandlers that consume cull results. The two hardest ideas for new contributors live here: (1) state composition — how attribs override and merge as you descend the tree — and (2) the copy-on-write PipelineCycler that lets the cull thread read a stable snapshot of the graph while the app thread mutates it.
pgraph
What it is. The core scene-graph engine. It defines the universal node base class PandaNode, the path/handle abstraction NodePath, the immutable state objects (RenderState, TransformState, RenderAttrib, RenderEffect, RenderEffects), the cull-traversal framework (CullTraverser, CullTraverserData, CullResult, CullableObject, CullHandler), the renderable leaf GeomNode, the Camera/LensNode that drives a view, and a large family of concrete RenderAttrib subclasses (TextureAttrib, ColorAttrib, TransparencyAttrib, CullBinAttrib, ClipPlaneAttrib, DepthOffsetAttrib, etc.) and RenderEffect subclasses (BillboardEffect, CompassEffect, DecalEffect). This is also where the bin registry (CullBinManager) and the slot registry (RenderAttribRegistry) live.
Central abstraction & inheritance.
PandaNode(panda/src/pgraph/pandaNode.h) — base of every scene-graph node. InheritsTypedWritableReferenceCount(bam-serializable + ref-counted) andNamable. It owns parent/child link lists (Up/Down, stored copy-on-write viaCOWPT), aTransformState, aRenderState, aRenderEffects, draw/collide masks, bounds, and tags — all wrapped in aPipelineCycler<CData>(line 701) so they can be cycled across pipeline stages. Noteis_renderable()isfinaland gated by aset_renderable()flag; the cull traversal callsadd_for_draw()(overridden byGeomNode) for renderable nodes.RenderState(renderState.h) — inheritsNodeCachedReferenceCount. An immutable, interned set ofRenderAttribs. You never construct one; you callRenderState::make(...)or compose existing ones. Internally aSimpleHashMapof(slot -> Attribute{attrib, override})plus a precomputed hash; states are uniquified globally so pointer-equality == value-equality.RenderAttrib(renderAttrib.h) — inheritsTypedWritableReferenceCount. Base of one renderable property (texture, color, depth-test…). Each subclass returns a unique integer slot fromget_slot()(pure virtual, line 87). Attribs are interned in a static set keyed bycompare_to_impl; identical attribs share one pointer. Key virtuals to override when adding a new attrib:compose_impl,invert_compose_impl,compare_to_impl,get_hash_impl, and the bam I/O.TransformState(transformState.h) —final, inheritsNodeCachedReferenceCount. Immutable transform (componentwise pos/hpr/scale/shear or a raw matrix), interned and reference-counted exactly likeRenderState.RenderEffect(renderEffect.h) — inheritsTypedWritableReferenceCount. The crucial doc difference fromRenderAttrib(renderEffect.h:31-46): aRenderAttribpropagates down to the leaves regardless of which node it sits on, whereas aRenderEffectis applied immediately at the node where it is encountered during cull (billboarding, compass, decal).RenderEffectsis the per-node container of effects.CullTraverser(cullTraverser.h) —TypedReferenceCount. Performs the depth-first walk.CullTraverserData(cullTraverserData.h) is the per-node accumulator (net transform, accumulatedRenderState, view frustum,CullPlanes, instance list, draw mask).GeomNode(geomNode.h) — the renderable leaf. Holds a copy-on-write list of(Geom, RenderState)entries; itsadd_for_draw()(geomNode.cxx) composes the accumulated state with each Geom’s state and hands aCullableObjectto theCullHandler.Camera(camera.h) inheritsLensNode; carries thecamera_mask(DrawMask),initial_state,lod_center/lod_scale,cull_center, and tag-state map that the traversal reads.
The state/attrib system (the part newcomers find hardest). A RenderState is “a collection of RenderAttribs, like TextureAttrib, ColorAttrib… The RenderState and RenderAttrib objects are const, so when you want to change one, you make a new one” (community, Discord: https://discord.com/channels/524691714909274162/533048345791299634/800606173916692482). Each attrib type owns a fixed slot assigned at startup by RenderAttribRegistry::register_slot (renderAttribRegistry.h:52). A RenderState is therefore a sparse array indexed by slot, and a SlotMask (BitMask32, renderAttribRegistry.h:46) records which slots are filled — making compose()/compare_to() fast bitwise/array operations rather than map merges. Composition (RenderState::compose) walks slots: a child’s attrib for a slot replaces the parent’s, unless the parent set a higher override. This is how “set a texture high in the tree, override it lower down” works. The slot scheme came from the 2008 dev_slots_2008 branch (commit 26754b86, “merge dev_slots_2008: slot-based RenderState implementation”) and the later move to SimpleHashMap + garbage_collect() (commit 229cf67a) was groundwork for threaded pipelining.
Gotcha — only 32 attrib slots.
_max_slots = 32(renderAttribRegistry.h:50) and the mask is aBitMask32. Panda already ships ~30 built-in attribs, so registering many custom attribs can exhaust the slots: GitHub issue #500 “Running out of attrib slots” (https://github.com/panda3d/panda3d/issues/500). If you add a built-in attrib you are consuming a global slot. The header comment itself flags that the next step is a 64-bit mask.
The copy-on-write / PipelineCycler pattern (the other hard part). Every mutable field of a PandaNode lives inside PandaNode::CData, owned by PipelineCycler<CData> _cycler (pandaNode.h:701). Readers use CDReader/CDStageReader; writers use CDWriter (typedefs at pandaNode.h:702-706). With threaded pipelining enabled, the cull thread reads one pipeline stage while the app thread writes another, and writes are copy-on-write so the cull thread never sees a half-mutated node. The child/parent lists and the Geom list are themselves COWPT/CopyOnWriteObj, copied lazily only on first write (pandaNode.h:683-687). This pattern is real and fragile: the threaded PipelineCycler “has not been reliably thread-safe” and is the subject of the in-progress epoch-based-reclamation rewrite, PR #1853 “Implement EBR to solve scenegraph threading” (https://github.com/panda3d/panda3d/pull/1853), whose stack traces show contention right at PipelineCyclerTrueImpl::write_stage. If you touch CData layout or locking, that PR is required reading.
How it plugs into the rest of the engine. display/GraphicsEngine sets up a SceneSetup and calls CullTraverser::traverse(root) once per DisplayRegion per frame; the traverser reads the active Camera’s lens, mask and initial state. CullTraverser::do_traverse (cullTraverser.cxx:178) inspects each node’s fancy_bits (the FancyBits enum, pandaNode.h:319: FB_transform, FB_state, FB_effects, FB_cull_callback, FB_renderable, FB_decal, …) to skip uninteresting nodes cheaply; for interesting ones it calls data.apply_transform_and_state(this) (which composes the node’s transform/state/effects into the accumulator, and runs RenderEffect::cull_callback), then node->cull_callback() for nodes that override it (LOD/Switch/Sequence/Callback), then node->add_for_draw() for renderables, then recurses into children via traverse_down. The downstream CullHandler (either DrawCullHandler for immediate draw or BinCullHandler for sorted draw) receives CullableObjects and stuffs them into CullResult/CullBins. NodePath is the app-facing wrapper that everything else (loader, actor, GUI, collision) uses to manipulate the graph.
Where to start reading (entry points).
To understand a frame:
cullTraverser.cxxtraverse()→do_traverse()(lines 178-241) →cullTraverserData.cxxapply_transform_and_state()/apply_transform()(line 96) →geomNode.cxxadd_for_draw().To understand state:
renderState.cxxmake(),compose(),do_compose;renderAttrib.cxxinterning;renderAttribRegistry.cxx.To add a new render attribute: copy an existing small one (e.g.
colorWriteAttrib.h/.cxx/.I), implementmake(),compose_impl,compare_to_impl,get_hash_impl, bam I/O, register the slot inconfig_pgraph.cxx, and add it toCMakeLists.txt.To debug threading/CoW:
pandaNode.hCData+_cycler, and PR #1853.
Relevant config variables (config_pgraph.cxx).
state-cache/transform-cache(bool) — enable the global compose/compare caches forRenderState/TransformState.garbage-collect-states(bool, default true) +garbage-collect-states-rate(double) — how interned states are reclaimed.uniquify-states/uniquify-transforms/uniquify-attribs(bool) — force global de-duplication so pointer equality implies value equality (and enables fastcompare_sort).paranoid-compose,paranoid-const,auto-break-cycles,detect-graph-cycles,unambiguous-graph— debugging/validation aids for the graph and the compose caches.fake-view-frustum-cull(bool) — render frustum-culled geometry in red wireframe instead of dropping it (seedo_fake_cull, cullTraverser.cxx:271).depth-offset-decals(bool) — implementDecalEffectviaDepthOffsetAttribrather than the legacy two-pass decal.m-dual,m-dual-opaque,m-dual-transparent,m-dual-flash— controlM_dualtransparency (binary-alpha opaque pass + sorted blended pass).flatten-geoms,premunge-data,preserve-geom-nodes— flatten/optimization behavior.
Gotcha — flatten doesn’t cross ModelNodes.
flatten_strong()stops atModelNodes; callclear_model_nodes()first (community: https://discourse.panda3d.org/t/11911). And flattening only pays off with many nodes (https://discourse.panda3d.org/t/27492). Noteflatten_strong()was later changed to alsounify()geoms (commit5c623893).
pgraphnodes
What it is. The library of specialized PandaNode subclasses that add semantic behavior on top of the bare node: the light family, level-of-detail and animation-selection nodes, user-callback and GPU-compute nodes, and the fixed-function ShaderGenerator. Anything that needs a cull_callback to make per-frame decisions (which child to show, what distance bucket we’re in) tends to live here rather than in pgraph.
Key classes & roles.
LightNode(lightNode.h) — multiply-inheritsLight(the abstract light interface, inpanda/src/pgraph/light.h) andPandaNode. Base for lights that don’t need a lens.AmbientLight(ambientLight.h) derives from it.LightLensNode(lightLensNode.h) — multiply-inheritsLightandCamera(lightLensNode.h:33), because shadow-casting lights need a lens/view to render a shadow map. Holds shadow-buffer state (set_shadow_caster,_shadow_map,_sbuffers).DirectionalLight,PointLight,Spotlight, andRectangleLightderive from this directly (SphereLightderives fromPointLight, so transitively fromLightLensNode). (Spotlight needs a lens, which is why it inherits the lens path rather than plainLightNode— see lightNode.h:23-25.)LODNode(lodNode.h) —PandaNodewhosecull_callback(lodNode.cxx) measures the distance from the LOD center (the camera’slod_center, scaled bylod_scale) and shows exactly one child whose[in, out)switch range contains that distance.add_switch(in, out)within > out(“in” = far switch-in, “out” = near switch-out; see lodNode.h:49-53).FadeLODNode(fadeLodNode.h) cross-fades between two levels overlod-fade-time. Community confirms both “render exactly one of their children, according to the distance of that node from the camera” (https://discourse.panda3d.org/t/1133).SelectiveChildNode(selectiveChildNode.h) — “now vestigial” base (selectiveChildNode.h:26) for nodes that show one child;SequenceNodeandSwitchNodeand historicallyLODNoderelate to it.SequenceNode(sequenceNode.h) — cycles through children over time like a flipbook (anAnimInterface);SwitchNode(switchNode.h) shows a single explicitly-selected child. Both overridehas_single_child_visibility()/get_visible_child().CallbackNode(callbackNode.h) — lets app code registerCallbackObjects fired at cull/draw time (e.g.set_cull_callback,set_draw_callback); the hook for custom rendering without subclassing.ComputeNode(computeNode.h) — dispatches GPU compute shaders during the draw pass (add_dispatch(x,y,z)).UvScrollNode(uvScrollNode.h) — animates a texture matrix over time.ShaderGenerator(shaderGenerator.h, ~80 KB.cxx) — the fixed-function-to-GLSL auto-shader: given aRenderStateit synthesizes a shader implementing that state. This is the single largest and most-edited file in the cluster and the place to look when “auto-shader” output is wrong.SceneGraphAnalyzer(sceneGraphAnalyzer.h) — a diagnostic that walks a subgraph counting nodes/verts/geoms; useful for understanding flatten payoff.
Central abstraction & inheritance. Everything here is PandaNode (directly or via Camera/LensNode). The recurring pattern is override cull_callback(CullTraverser*, CullTraverserData&) to make a per-frame decision and prune/select children, returning false to stop the traversal descending. Lights additionally implement the Light mixin so they can be attached to a LightAttrib (in pgraph) and consumed by the GSG.
How it plugs in. These nodes are added to the graph like any other and are visited by the same CullTraverser. Their cull_callback flag sets FB_cull_callback so do_traverse (cullTraverser.cxx:205) calls them. Lights are referenced by LightAttrib/LightRampAttrib (in pgraph) which the cull traversal accumulates into the RenderState; LightLensNodes additionally hand a shadow buffer to the GraphicsEngine. ShaderGenerator is invoked by the GSG (display) when ShaderAttrib::make_auto() is in effect.
Where to start reading. lodNode.cxx::cull_callback is the canonical example of a decision node (and the most-asked-about: see the LOD forum threads). For lights, read lightLensNode.cxx (shadow setup) and directionalLight.cxx/pointLight.cxx. To add a new decision node, subclass PandaNode, override cull_callback + has_single_child_visibility/get_visible_child, and register it in config_pgraphnodes.cxx (init_type + register_with_read_factory).
Gotcha — LOD prunes geometry, not collisions. Because
LODNodeonly selects which child to render, collision solids parented under non-visible LOD levels can still be active/processed; users repeatedly hit this (https://discourse.panda3d.org/t/9308, https://discourse.panda3d.org/t/9957). The node is doing exactly what it’s designed to: cull-time visual selection, nothing more.
Relevant config variables (config_pgraphnodes.cxx).
default-lod-type(pop|fade) — whatLODNode::make_default_lod()produces.support-fade-lod(bool, default true) — turn off to makeFadeLODNodebehave like a plainLODNode(handy for measuring fade cost).lod-fade-time(double, 0.5) — default cross-fade duration.lod-fade-bin-name(defaultfixed),lod-fade-bin-draw-order,lod-fade-state-override(1000) — how the fading half-level is drawn/overridden.verify-lods(bool, NDEBUG-only) — assert each LOD child’s geometry fits inside its switch radius.parallax-mapping-samples(int, 3) andparallax-mapping-scale(double, 0.1) — feed theShaderGenerator.
cull
What it is. The output stage of the cull pipeline. pgraph produces a stream of CullableObjects during traversal; this directory provides (a) the concrete CullBin subclasses that group and sort those objects per draw strategy, and (b) the two CullHandler implementations that decide whether to draw immediately or accumulate into bins. It is deliberately split out from pgraph so the abstract CullBinManager (in pgraph) can register bin constructors by type without pgraph depending on the concrete sorters — the factory wiring happens in config_cull.cxx::init_libcull().
Key classes & roles.
CullBin(abstract base, declared inpanda/src/pgraph/cullBin.h) — “a collection of Geoms and their associated state, for a particular scene” (cullBin.h:32). Pure virtualsadd_object()anddraw(); helpermake_result_graph(). Concrete subclasses live here incull:CullBinUnsorted(cullBinUnsorted.h) — keeps objects in traversal order; cheapest.CullBinStateSorted(cullBinStateSorted.h) — sorts to group identical state together (minimize GSG state changes), and front-to-back within a state to exploit hierarchical-Z early-out (cullBinStateSorted.h:26-34). This is the default for theopaquebin.CullBinBackToFront(cullBinBackToFront.h) — sorts by distance, far-to-near; required for correct alpha blending. Default for thetransparentbin.CullBinFrontToBack(cullBinFrontToBack.h) — near-to-far.CullBinFixed(cullBinFixed.h) — honors an explicit per-object draw order; used forbackgroundandfixedbins and for FadeLOD’s fading half.
BinCullHandler(binCullHandler.h) —CullHandlerthat routes each recordedCullableObjectinto the rightCullBinof aCullResult(deferred, sorted draw). This is the normal path.DrawCullHandler(drawCullHandler.h) —CullHandlerthat draws each object the instant it’s recorded, with no binning/sorting. Used when you don’t want the cost/latency of bins.
Central abstraction & inheritance. CullBin : TypedReferenceCount, CullBinEnums; each concrete bin is EXPCL_PANDA_CULL. BinType (BT_unsorted, BT_state_sorted, BT_back_to_front, BT_front_to_back, BT_fixed) is defined in cullBinEnums.h (in pgraph); the CullBinManager maps a registered bin name+sort to a BinType, and at draw time CullResult::make_new_bin asks the manager to instantiate the right concrete class via the constructor registered in init_libcull().
How it plugs in. Per frame: GraphicsEngine makes a CullResult for the DisplayRegion, wraps it in a BinCullHandler, and runs the CullTraverser. Each GeomNode::add_for_draw builds a CullableObject and calls CullHandler::record_object (geomNode.cxx). CullResult::add_object (cullResult.cxx:108) reads the object’s CullBinAttrib (via RenderState::get_bin_index()), creates the bin lazily, and calls bin->add_object. Transparency handling also happens here: CullResult may split an object with M_dual transparency into an opaque part (binary alpha, into the opaque bin) and a transparent part (re-binned into a back-to-front bin), and can route a wireframe overlay into the fixed bin (cullResult.cxx:149, 203-256). After traversal, CullBin::finish_cull sorts and draw() emits to the GSG.
The default bins (defined in cullBinManager.cxx::setup_initial_bins, lines 276-288). In sort order: background (BT_fixed, sort 10), opaque (BT_state_sorted, 20), transparent (BT_back_to_front, 30), fixed (BT_fixed, 40), unsorted (BT_unsorted, 50). The official manual explains the global CullBinManager and these five defaults: “How to Control Render Order” (https://docs.panda3d.org/1.10/python/programming/rendering-process/controlling-render-order). You change render order either by moving an object to a named bin via CullBinAttrib / NodePath.setBin(name, order), or by reconfiguring a bin’s type/sort on the global CullBinManager (community walkthrough: https://discourse.panda3d.org/t/15220).
Where to start reading. cullResult.cxx::add_object is the dispatch point (bin selection + transparency splitting). To understand a sort, read the tiny cullBinStateSorted.cxx/cullBinBackToFront.cxx (their ObjectData::operator< is the whole sort). To add a new sorting strategy: subclass CullBin, implement add_object/finish_cull/draw/fill_result_graph, add a BT_* enum value in cullBinEnums.h, and register its constructor in config_cull.cxx::init_libcull().
Gotcha — back-to-front sorting + nested transparency. Sibling/parent-child transparent objects in a single back-to-front bin sort only by centroid distance, so a parent decal and its transparent child can z-fight or sort wrong; one proposed fix is an extra sort key on back-to-front bins (GitHub issue #938, DecalEffect discrepancy: https://github.com/panda3d/panda3d/issues/938). Also note: if you put everything in front-to-back/state-sorted bins with no transparency, the binning overhead can dominate for tiny scenes (https://discourse.panda3d.org/t/26309).
Relevant config. config_cull.cxx registers no ConfigVariables of its own (it only wires up the bin-type factory and the cull notify category). The bins themselves are configured at runtime through CullBinManager, or seeded at startup via cull-bin <name> <sort> <type> lines parsed by setup_initial_bins (cullBinManager.cxx:230-267). Transparency-related behavior is governed by the m-dual* variables in config_pgraph.cxx (above).
Known shortcomings & footguns
The constructive picture above is only half the story. The same design choices that make the scene graph fast and composable — immutable interned state, cached bounding volumes, copy-on-write nodes, cull-bin sorting — also produce a recognizable set of traps that the community has hit for years. The entries below are community-sourced opinion/history (with maintainer quotes preserved verbatim); they describe how the subsystem breaks, complementing the “how it works” material above.
setColor replaces vertex colors; setColorScale multiplies — confusable
Severity: minor (very common) · Status: by-design
setColor() discards a model’s vertex colors by applying a flat ColorAttrib; setColorScale() applies a ColorScaleAttrib that multiplies the existing colors. They are interchangeable only when the model has no vertex colors — so code works in testing against an untinted model, then breaks on a vertex-colored one. (Both are ordinary RenderAttribs composed during cull, per the state/attrib discussion above.)
“np.setColor() will completely replace the vertex colors, but np.setColorScale() will simply multiply the existing vertex colors.” — rdb (maintainer), t/13629
flattenStrong() breaks already-played Actors and won’t flatten past ModelNode
Severity: major · Status: mitigated (clearModelNodes + ordering)
The standard perf fix silently does nothing on a naive model.flattenStrong(): it won’t merge past the ModelRoot/ModelNode that every loaded model sits under (see the “flatten doesn’t cross ModelNodes” gotcha in pgraph above — call clearModelNodes() first). Worse, calling it after binding/playing animations breaks the Actor.
“calling flattenStrong() will break an actor if you have already loaded up and played some animations.” — drwr (maintainer), t/4310
Too-many-Geoms is a pervasive perf trap with only manual mitigation
Severity: major · Status: by-design (manual flatten; no auto-batching)
Asset pipelines routinely yield 1000+ Geoms, and Panda has no dynamic batching: the only remedy is the manual clearModelNodes() + flattenStrong() dance — which carries its own footguns (above) and only pays off with many nodes.
re: Toontown/Pirates perf — “The ‘automatic Geom merging method’ you describe already exists in the form of clearModelNodes() followed by flattenStrong().” — rdb (maintainer), #301
Culling uses stale bounds for morphed / CPU-animated / instanced geometry
Severity: major · Status: by-design (workaround: OmniBoundingVolume)
The cull traversal frustum-culls by each node’s cached bounding volume and won’t recompute it for runtime-morphed geometry (terrain, CPU vertex animation, hardware instancing, off-screen Actors). The symptom is objects that vanish or never appear. The blunt workaround is to attach an OmniBoundingVolume to defeat culling entirely for that node.
“Panda does not automatically recompute the bounding volume of an Actor, because Actors move their vertices around a lot.” — drwr (maintainer), t/1262
Hardware-animated models duplicate GeomVertexData per copy
Severity: major · Status: still-open (#421)
Each independently-animated copy duplicates the entire GeomVertexArrayData because
the TransformTable lives under the Geom’s GeomVertexData and must be copied per
Character — wasteful for crowds.
RenderState/RenderAttrib are immutable; mutator-looking methods silently no-op
Severity: minor · Status: by-design
Consistent with the immutable, interned state system described above, set_attrib/add_attrib return a new RenderState and leave the original untouched. The names read like in-place setters, so ignoring the return value silently does nothing.
“state.add_attrib returns a new RenderState and leaves state untouched. So if you’re using this, then you haven’t actually been [applying it].” — rdb (maintainer), t/31476
Transparency is order-dependent (no OIT) and breaks when the camera moves
Severity: major · Status: by-design
Default M_alpha blending requires strict back-to-front draw order, which the transparent cull bin provides by sorting on centroid distance (see the cull bin discussion above). Panda has no order-independent transparency, so overlapping/nested transparent objects render wrong, and a “fix” tuned for one camera angle breaks on rotation.
“Sorting will help if the camera direction does not change, if it changes, it will not work.” — forum, t/28748
DecalEffect/DepthOffset conflict with transparency sorting
Severity: major · Status: still-open (#938)
Decals rely on a fixed depth offset, but transparent objects are re-sorted back-to-front by distance, so a decalled transparent child can render behind its parent (this is the “nested transparency” gotcha noted in the cull section). A proper fix needs a topological sort over a partial ordering (rdb), e.g. an extra sort key on back-to-front bins.
Z-fighting / setDepthOffset has no real-world unit; default depth range wastes precision
Severity: minor · Status: by-design (#377 proposes 0..1 range, open)
DepthOffsetAttrib (via setDepthOffset) exposes only an opaque integer with no metric meaning; its effect varies with camera distance and near/far. Panda also still defaults to GL’s -1..1 depth range, wasting float precision in the middle.
Where to start (this cluster)
If you are new and want to get oriented fast, read in this order:
panda/src/pgraph/pandaNode.h— the universal node, theCData/PipelineCyclercopy-on-write story (line 701), and theFancyBitsenum (line 319) that drives cull-time dispatch.panda/src/pgraph/renderState.h+renderAttrib.h+renderAttribRegistry.h— the immutable, slot-indexed, interned state-composition system. ReadRenderState::composeandRenderAttrib::compose_implto internalize “child overrides parent unless override wins.”panda/src/pgraph/cullTraverser.cxxdo_traverse()(line 178) +cullTraverserData.cxxapply_transform()(line 96) — one frame of culling end to end, including frustum/cull-plane transforms andRenderEffectapplication.panda/src/pgraph/geomNode.cxxadd_for_draw()— how a leaf turns accumulated state into aCullableObject.panda/src/cull/cullResult.cxxadd_object()+ the five tinycullBin*.cxxfiles — how draw order/sorting and transparency actually happen.For threading/CoW work, PR #1853 (EBR) (https://github.com/panda3d/panda3d/pull/1853); for the slot system’s history, commit
26754b86(dev_slots_2008). All four config files (config_pgraph.cxx,config_pgraphnodes.cxx,config_cull.cxx) double as an index of every tunable and every registered type in the cluster.