Listen up. If you're building modern applications, understanding Meshes & Transformations in Three.js 3D WebGL is non-negotiable. This is where simple logic turns into intelligent behavior.
1Why Geometry and Material Aren't Enough
You know what a Geometry is (the shape) and what a Material is (the skin). But you can't add either of them directly to the Scene ā scene.add(geometry) and scene.add(material) will both throw, because neither one is a renderable Object3D on its own.
A BufferGeometry is just vertex, normal, and UV data sitting in memory; a Material is just a description of how surfaces should be shaded. Neither knows where it sits in 3D space or how to be drawn ā that combination step is what a Mesh exists to provide.
This separation is deliberate: it lets you reuse the same geometry across many meshes with different materials, or swap a material on a mesh without touching its underlying shape data.
// scene.add(geometry); // ERROR!
// scene.add(material); // ERROR!3D Scene rendered. Objects: 4, Draw Calls: Optimized.
2Assembling Your First Mesh
To put an object in your scene, you must combine a Geometry and a Material into a Mesh, which is exactly what new THREE.Mesh(geometry, material) does.
The Mesh constructor stores the geometry and material as properties (mesh.geometry, mesh.material) but is itself a full Object3D ā it inherits position, rotation, scale, and the ability to be added to a Scene or a Group. That's the crucial distinction: geometry and material describe appearance, while Mesh provides identity and placement in the 3D world.
Once created, only the Mesh gets passed to scene.add() ā never the raw geometry or material objects, which stay referenced internally on the mesh.
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
3Mesh = Geometry + Material
A Mesh is fundamentally the combination of Geometry and Material ā new THREE.Mesh(geometry, material) is the only way Three.js knows both what shape to draw and how to shade its surface.
Swap either argument and the mesh's appearance changes completely: the same BoxGeometry rendered with a MeshBasicMaterial looks flat and unlit, while the same geometry with a MeshStandardMaterial reacts to scene lighting. The geometry never changes what the material does, and vice versa ā they're fully independent until joined by the Mesh.
This is also why swapping mesh.material at runtime (say, to highlight a selected object) is cheap: you're not touching vertex data at all, just the shading description.
const mesh = new THREE.Mesh(???, ???);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
4The <mesh> Tag in React Three Fiber
In React Three Fiber, the <mesh> tag acts just like new THREE.Mesh(). Any geometry or material you put inside it gets automatically attached!
R3F relies on Three.js's own attach system here: a JSX child like <boxGeometry /> gets instantiated and automatically assigned to the parent mesh's .geometry property, and <meshStandardMaterial /> to .material, purely based on the lowercase-first tag naming convention mapping to THREE.BoxGeometry and THREE.MeshStandardMaterial.
This declarative style hides the imperative new THREE.Mesh(geometry, material) call entirely ā you're describing the same object graph, just as nested JSX instead of constructor arguments, and R3F reconciles it into real Three.js objects behind the scenes.
<mesh>
<boxGeometry />
<meshStandardMaterial />
</mesh>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
5Positioning a Mesh
Once you have a Mesh, you can move it around using its position property, which is a THREE.Vector3 with x, y, and z components you can set individually or all at once.
In vanilla Three.js you mutate mesh.position.x and mesh.position.y directly on the vector object; in R3F you instead pass a position={[x, y, z]} array prop, and Fiber applies it to the underlying Vector3 for you. Both approaches ultimately set the same three numbers.
Because position is relative to the mesh's parent (the Scene by default, or a Group if nested), moving a mesh inside a group changes where it sits relative to that group's own position ā not the world origin.
// Vanilla Three.js
mesh.position.x = 2;
mesh.position.y = 1;
// R3F
<mesh position={[2, 1, 0]}>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
6Rotation Uses Radians, Not Degrees
You can also rotate the mesh. Note that Three.js uses Radians, not Degrees ā Math.PI is 180 degrees, and Math.PI / 2 is 90 degrees.
mesh.rotation is a THREE.Euler, storing rotation around each axis in radians (a full turn is 2 * Math.PI, roughly 6.283). Developers coming from CSS or design tools, where degrees are the norm, frequently plug in a raw degree value like 90 expecting a quarter turn and instead get an object spun more than 14 full rotations.
If you ever need to convert, THREE.MathUtils.degToRad(90) is the safe, readable way to go from a familiar degree value to the radians Three.js actually expects.
mesh.rotation.y = Math.PI / 2;3D Scene rendered. Objects: 4, Draw Calls: Optimized.
7Quick Check: Radian Values
In Three.js, rotations are calculated in Radians ā the value that represents a 180-degree rotation is Math.PI (approximately 3.14159), not the number 180 itself.
It helps to memorize a few common landmarks rather than doing the conversion math every time: Math.PI / 4 is 45 degrees, Math.PI / 2 is 90 degrees, Math.PI is 180 degrees, and Math.PI * 2 is a full 360-degree turn back to the start.
Because rotation.x, .y, and .z each wrap independently, setting mesh.rotation.y = Math.PI * 4 is functionally identical to Math.PI * 2 or even 0 visually ā Three.js doesn't normalize the stored value, but the rendered orientation repeats every full turn.
mesh.rotation.x = ???;3D Scene rendered. Objects: 4, Draw Calls: Optimized.
8Position, Rotation, and Scale Together
Let's see Position, Rotation, and Scale in action. This cube is scaled up, moved to the right, and constantly rotating.
The example uses useFrame to run on every rendered frame, incrementing meshRef.current.rotation.x and .y by delta (the time in seconds since the last frame) ā this is the R3F equivalent of the vanilla mesh.rotation.x += 0.01 pattern, but frame-rate independent because it's scaled by actual elapsed time rather than a fixed constant per call.
position, scale, and rotation are all independent transforms applied in a fixed order (scale, then rotate, then translate) when Three.js builds the mesh's world matrix each frame ā which is why scaling a mesh doesn't affect where its position places it, only its size.
const App = () => {
const meshRef = React.useRef();
useFrame((state, delta) => {
meshRef.current.rotation.x += delta;
meshRef.current.rotation.y += delta;
});
return (
<Canvas camera={{ position: [0, 0, 5] }}>
<ambientLight intensity={1} />
<mesh
ref={meshRef}
position={[2, 0, 0]}
scale={[1.5, 1.5, 1.5]}
>
<boxGeometry />
<meshNormalMaterial />
</mesh>
</Canvas>
);
};
render(<App />);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
9Grouping Meshes with THREE.Group
You can group multiple meshes together using a THREE.Group (or <group> in R3F). If you move the group, all child meshes move with it!
A Group is itself an Object3D with position, rotation, and scale, but it has no geometry or material of its own ā it exists purely as a container node in the scene graph. Any transform applied to the group is automatically inherited by every mesh nested inside it, composed on top of each child's own local transform.
This is the standard way to build compound objects like a car (body + four wheel meshes) or a solar system (sun + orbiting planet meshes): group related parts once, then transform the whole assembly as a single unit instead of updating each mesh's transform individually.
<group position={[0, 5, 0]}>
<mesh>{/* Car Body */}</mesh>
<mesh>{/* Wheels */}</mesh>
</group>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
10A Grouped Sphere and Cube in Motion
Let's group a sphere and a cube. The whole group rotates together like a mini solar system!
Here the group's own rotation.z is animated via useFrame, while the sphere sits at the group's local origin [0, 0, 0] and the cube sits offset at [2, 0, 0] ā so rotating the group swings the cube around the sphere in a circular orbit, without either child mesh having its own rotation logic.
Notice neither the sphere nor the cube mesh has a ref or its own useFrame call; only the parent group does. This is the payoff of grouping ā one animated transform drives the visual motion of every child simultaneously.
const App = () => {
const groupRef = React.useRef();
useFrame((state, delta) => {
groupRef.current.rotation.z -= delta;
});
return (
<Canvas camera={{ position: [0, 0, 5] }}>
<ambientLight intensity={1} />
<group ref={groupRef}>
<mesh position={[0, 0, 0]}>
<sphereGeometry args={[0.5]} />
<meshNormalMaterial />
</mesh>
<mesh position={[2, 0, 0]}>
<boxGeometry args={[0.5, 0.5, 0.5]} />
<meshNormalMaterial />
</mesh>
</group>
</Canvas>
);
};
render(<App />);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
11Meshes Fundamentals Complete
Fantastic! You've combined geometries and materials into meshes, and learned how to transform and group them. You are now officially creating 3D content.
Every visible object you'll build from here forward ā from simple primitives to complex loaded models ā follows this same pattern: shape data plus a shading description, wrapped in a Mesh, positioned with transforms, and optionally grouped with related objects.
The next lesson builds directly on this foundation by digging deeper into lighting, which determines how those materials actually get shaded once a mesh is in the scene.
// š¦ Meshes assembled!3D Scene rendered. Objects: 4, Draw Calls: Optimized.
12Step-by-Step Breakdown
You know what a Geometry is (the shape) and what a Material is (the skin). But you can't add either of them directly to the Scene.
To put an object in your scene, you must combine a Geometry and a Material into a Mesh.
A Mesh is fundamentally the combination of which two things?
- āScene and Camera
- āGeometry and Material
- āVertices and Faces
In React Three Fiber, the <mesh> tag acts just like new THREE.Mesh(). Any geometry or material you put inside it gets automatically attached!
Once you have a Mesh, you can move it around using its position property.
You can also rotate the mesh. Note that Three.js uses Radians, not Degrees. Math.PI is 180 degrees. Math.PI / 2 is 90 degrees.
In Three.js, rotations are calculated in Radians. What value represents a 180-degree rotation?
- ā180
- āMath.PI
- āMath.PI / 2
Let's see Position, Rotation, and Scale in action. This cube is scaled up, moved to the right, and constantly rotating.
You can group multiple meshes together using a THREE.Group (or <group> in R3F). If you move the group, all child meshes move with it!
Let's group a sphere and a cube. The whole group rotates together like a mini solar system!
Which component allows you to combine multiple meshes so you can rotate or move them as a single entity?
- ādiv
- āgroup
- āscene
Fantastic! You've combined geometries and materials into meshes, and learned how to transform and group them. You are now officially creating 3D content.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Mesh Transformations Carry No Semantic Meaning to Assistive Tech
Moving, rotating, or scaling a mesh is purely visual state inside the WebGL canvas ā a screen reader has no way to know a cube 'moved to the right' or that a group represents a car. If a mesh's position or state conveys information the user needs (e.g., a selected object, a completed step), mirror that state as text in a real DOM element outside the canvas.
<span aria-live="polite">{selectedPart} is currently selected</span>SEO Implications
- 1
Mesh Hierarchies Are Invisible to Crawlers, Regardless of Grouping
Whether a mesh is grouped, positioned, or rotated has zero effect on SEO ā search engines never execute WebGL draw calls or inspect the Three.js scene graph. Any product or scene description that matters for search should exist as real page copy, independent of how the meshes are structured.
Best Practices
Reuse Geometries and Materials Across Meshes Instead of Recreating Them
Because Mesh only references a geometry and material rather than owning unique copies, many meshes can safely share the same BoxGeometry or MeshStandardMaterial instance. Creating a fresh geometry or material per mesh (e.g., inside a loop generating hundreds of cubes) wastes GPU memory and increases draw call setup cost for no visual benefit.
Use Groups to Compose Objects Instead of Manually Syncing Child Transforms
When several meshes need to move or rotate together (like wheels and a car body), wrap them in a THREE.Group and transform the group, rather than recalculating each mesh's position every frame. This keeps the relationship between parts declarative and eliminates an entire class of drift bugs where child transforms fall out of sync.
Frequent Bugs
Setting mesh.rotation.y = 90 expecting a quarter turn, but getting a wildly over-rotated or seemingly random-looking orientation.
Three.js rotation values are in radians, not degrees. Use Math.PI / 2 for 90 degrees, or convert explicitly with THREE.MathUtils.degToRad(90) if you're more comfortable thinking in degrees.
Real-World Examples
Assembling a Vehicle from Grouped Meshes
A 3D car configurator groups a car body mesh with four wheel meshes into a single THREE.Group, so animating the whole car's position or a steering-style rotation only requires transforming the group, while each wheel can still spin independently on its own local rotation.x.
const carGroup = new THREE.Group();
carGroup.add(bodyMesh, wheelFL, wheelFR, wheelRL, wheelRR);
carGroup.position.x = drivenDistance;