Navigate

Search topics across all sections

GitHub
Meshes & ObjectsOrganization

Groups & Hierarchy

Imagine Russian nesting dolls. The biggest doll is a group. Inside it are smaller dolls -- meshes, lights, or even more groups. Move the outer doll, and everything inside moves with it. Rotate it, and everything rotates together. That is what a <group> does in R3F: it bundles objects so you can transform them as one unit.

You build a robot arm. You want the forearm to swing, but when you rotate the forearm mesh directly, the hand stays in place. The arm separates at the elbow like a broken toy. The hand should follow the forearm, but it does not because the hand is not nested inside the forearm group.

terminal
Forearm rotates but hand stays stationary.
Hand is a sibling of forearm, not a child.
Expected: hand follows forearm rotation.
Actual: hand is disconnected from forearm transform.

Real-world

Think of Russian nesting dolls (matryoshka). The outermost doll is the biggest group. Open it, and there is a smaller doll inside. Open that one, another inside. Move the outer doll to a shelf, and every doll inside comes along for the ride.

Now imagine a robot arm. The base is the outer doll. Inside the base is the upper arm (smaller doll). Inside the upper arm is the forearm. Inside the forearm is the hand. Rotate the base, and the entire arm swings. Rotate just the forearm, and only the hand follows. Each level only affects its children.

Outer Group

Move this, everything moves

Inner Group

A joint in the chain

Mesh

The visible part

Building a Robot Arm

Let us build a simple robot arm with three joints. Each joint is a group, and the nesting creates the hierarchy. Rotate one joint and everything below it follows.

Step 1Create the base (outer doll)

The base sits on the ground and rotates left to right. It is the outermost group -- everything else is inside it.

RobotArm.tsxTSX
<group ref={baseRef}> {/* Base joint */}
  <mesh> {/* Base platform */}
    <cylinderGeometry args={[0.5, 0.5, 0.2]} />
    <meshStandardMaterial color="gray" />
  </mesh>
</group>

Rotate baseRef around the Y axis and the entire arm swings left and right. The base is the root of our hierarchy.

Step 2Add the upper arm (middle doll)

The upper arm group is nested inside the base. It sits on top of the base and tilts forward and backward.

RobotArm.tsxTSX
<group ref={baseRef}>
  {/* base mesh... */}
  <group ref={upperArmRef} position={[0, 0.15, 0]}>
    <mesh position={[0, 0.75, 0]}> {/* Arm */}
      <boxGeometry args={[0.2, 1.5, 0.2]} />
      <meshStandardMaterial color="steelblue" />
    </mesh>
  </group>
</group>

Notice the upper arm is positioned at [0, 0.15, 0] -- that is relative to the base. When the base rotates, the upper arm follows because it is a child.

Step 3Nest the forearm (inner doll)

The forearm goes inside the upper arm group. Now we have three levels of nesting, and each joint affects everything below it.

RobotArm.tsxTSX
<group ref={upperArmRef} position={[0, 0.15, 0]}>
  {/* upper arm mesh... */}
  <group ref={forearmRef} position={[0, 1.5, 0]}>
    <mesh position={[0, 0.5, 0]}> {/* Forearm */}
      <boxGeometry args={[0.15, 1, 0.15]} />
      <meshStandardMaterial color="orange" />
    </mesh>
  </group>
</group>

The forearm is at [0, 1.5, 0] relative to the upper arm group. Rotate the base? Everything moves. Rotate the upper arm? The forearm follows. Rotate the forearm? Only it moves. Each joint is independent but cascades downward.

What you just learned

A <group> is an invisible container that transforms all its children together

Groups cost nothing to render -- no vertices, no draw calls

Nested groups create transform chains (like joints in an arm)

Each level only controls its own rotation, but the effect cascades down

Child positions are always relative to their parent group

Groups are perfect for toggling visibility of many objects at once

Question

If you want a cube to orbit around a point (like a planet around a star), do you rotate the cube or something else?

You rotate a parent group, not the cube itself. Place the cube at an offset from the group's center (like [3, 0, 0]), then rotate the group. The cube sweeps in a circle around the group's origin. Rotating the cube directly would just spin it in place -- like a basketball spinning on a finger instead of orbiting the court.

Think about it...

A group at position [2, 0, 0] is rotated 90 degrees around the Y axis. Inside it, a mesh is at position [3, 0, 0]. Where does the mesh end up in world space?

Hint: Rotation changes the DIRECTION of the child's offset. A 90-degree Y rotation turns the X axis into the -Z axis.

Try These Challenges

Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.

Try This!

Beginner

Set speed to 0 -- use manual sliders to pose the arm.

Try This!

Beginner

Set baseRotation to PI (3.14) -- the arm faces backward!

Try This!

Intermediate

Max out elbowAngle -- does the arm break?

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Setting world-space positions on children
Child transforms are LOCAL to their parent
Don't do this
Scene.tsxTSX
// Group at [5,0,0], child at [3,0,0]
// World result: [8,0,0] -- not [3,0,0]!
<group position={[5, 0, 0]}>
  <mesh position={[3, 0, 0]}>
    <boxGeometry />
  </mesh>
</group>
All child transforms are in the parent's local coordinate space. A child at [0,0,0] appears at the parent's world position. Design children relative to the parent's origin, and move the parent to position the whole group.
2
Non-uniform scale on parent groups
Parent scale multiplies into all children
Don't do this
Scene.tsxTSX
// Parent stretches all children along X!
<group scale={[2, 1, 1]}>
  <mesh> {/* Sphere becomes an ellipsoid */}
    <sphereGeometry args={[0.5, 32, 32]} />
  </mesh>
</group>
Parent scale is inherited by all children. A non-uniform scale like [2,1,1] stretches every child along X, distorting spheres into ellipsoids. Use uniform scaling on groups (a single number).
3
Rotating the wrong object for orbiting
Objects rotate around their own origin
Don't do this
Orbit.tsxTSX
// This spins the cube IN PLACE, not orbiting
const ref = useRef<THREE.Mesh>(null)
useFrame((_, d) => {
  ref.current!.rotation.y += d
})
<mesh ref={ref} position={[3, 0, 0]} />
Rotating an object spins it around its own local origin. To orbit around a different point, wrap the mesh in a group centered at the orbit point and rotate the group instead.

Best Practices

Use groups as pivot points

To orbit an object around a point, wrap it in a group centered at the orbit point and rotate the group. The child sweeps in a circle.

Keep uniform scale on groups

Non-uniform scale distorts all children and breaks normals. Apply non-uniform scale only to individual meshes that need it, not to group containers.

Toggle visibility with groups

Setting visible={false} on a group hides everything inside it with a single prop. Much cleaner than hiding each mesh individually.

Think in local coordinates

Design child positions relative to the parent center, not world space. This makes compound objects portable -- move the parent and everything stays intact.