Sitemap
Mirian Ezeamama

Mirian Ezeamama

Tech Enthusiast

Day 16 of Learning Three.js: Understanding Surface Normals in React Three Fiber

7 min readNov 3, 2026

--

Press enter or click to view image in full size

How I learned to understand which direction a 3D surface is facing and use that information to place markers correctly.

On Day 15 of my Three.js learning journey, I learned how to click a 3D model and place markers on its surface.

I used raycasting to find the clicked position and stored that position using React state.

But I noticed something.

Sometimes my markers were half-buried inside the box.

At first, I wondered whether I was placing them at the wrong coordinates.

But the problem was actually much simpler.

I was placing the center of each sphere exactly on the surface.

Since the sphere has a radius, part of it naturally extends inside the model.

That led me to today’s topic:

Surface normals.

1. What Is a Surface Normal?

A surface normal is a vector that points perpendicular to a surface.

Imagine a flat table.

If I place a pencil on the table so that it stands straight up, the pencil points in a direction perpendicular to the table.

That’s similar to a surface normal.

But normals don’t always point upward.

A vertical wall has a sideways normal.

A sloping surface has a diagonal normal.

The direction depends on the surface’s orientation.

This is important because it tells us which way a surface is facing.

2. A Normal Is a Vector3

We’ve already learned about Vector3 in earlier lessons.

A Vector3 contains three numbers: X, Y, and Z.

It can represent a position or a direction.

A surface normal is a direction vector.

For example:

const normal = new THREE.Vector3(0, 1, 0);

This points upward along the positive Y-axis.

Another example:

const normal = new THREE.Vector3(1, 0, 0);

This points along the positive X-axis.

And:

const normal = new THREE.Vector3(0, 0, -1);

points along the negative Z-axis.

Surface normals are usually represented as unit vectors.

That means their length is 1.

This makes them useful for calculating directions and offsets.

3. Why Do We Need Surface Normals?

Surface normals are useful for several reasons.

First, they help with lighting.

When light reaches a surface, the renderer needs to know which direction the surface faces.

Second, normals help us position objects relative to surfaces.

For example, if I want to place a marker slightly above a model, I need to know which direction points away from the surface.

Third, normals can help orient objects.

If I want an arrow to point outward from a selected surface, the normal can help determine its direction.

Today, I’m focusing on marker placement.

4. Understanding the Marker Problem

Let’s revisit our Day 15 example.

We clicked a surface and obtained:

const point = event.point;

Then we placed a marker at that point.

<mesh position={[point.x, point.y, point.z]}>
<sphereGeometry args={[0.08, 16, 16]} />
<meshBasicMaterial color="red" />
</mesh>

The problem is that the marker’s position represents its center.

So if the marker has a radius of 0.08, half of it may extend inside the model.

What I actually want is to move the marker slightly outward.

To do that, I need the surface normal.

The idea is simple:

New marker position = clicked point + normal × offset

The normal tells us which direction to move.

The offset tells us how far to move.

5. Getting the Surface Normal from Raycasting

React Three Fiber gives us information about the intersected face.

For triangle-based meshes, we can access:

event.face?.normal

The question mark means the face might not exist.

So we should check before using it.

if (!event.face) return;

Then we can access the normal:

const normal = event.face.normal;

But there’s something important.

event.face.normal is in the mesh's local coordinate system.

Meanwhile, event.point is in world coordinates.

That means we cannot always use them together directly.

If the mesh is rotated, its local normal might point in a different direction from the corresponding world-space normal.

So we need to convert it.

6. Converting the Normal into World Space

Three.js provides a useful tool called a normal matrix.

A normal matrix helps transform surface normals correctly, even when an object has nonuniform scaling.

Here’s the code:

import { Matrix3 } from "three";
const normalMatrix = new Matrix3().getNormalMatrix(
event.object.matrixWorld
);
const worldNormal = event.face.normal
.clone()
.applyMatrix3(normalMatrix)
.normalize();

Let’s understand it step by step.

Matrix3 represents a 3-by-3 matrix.

getNormalMatrix() calculates the transformation needed for normals.

event.object.matrixWorld contains the object's world transformation.

.clone() creates a copy of the original normal.

.applyMatrix3(normalMatrix) transforms the normal into world space.

And .normalize() makes sure the resulting direction has a length of 1.

Get Mirian Ezeamama’s stories in your inbox

Join Medium for free to get updates from this writer.

Now I have a world-space normal that I can use with event.point.

7. Moving the Marker Outward

Once I have the world-space normal, I can calculate the marker position.

const markerPosition = event.point
.clone()
.addScaledVector(worldNormal, 0.08);

Let’s break it down.

First:

event.point.clone()

creates a copy of the clicked position.

Then:

.addScaledVector(worldNormal, 0.08)

adds the normal multiplied by 0.08.

This moves the marker 0.08 world units outward from the surface.

Since my sphere has a radius of 0.08 world units in this example, its center is now one radius away from the surface.

That should prevent the sphere from being half-buried.

8. My Complete Example

Now let’s combine everything into a React Three Fiber scene.

"use client";
import { useState } from "react";
import { Canvas, type ThreeEvent } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import { Matrix3 } from "three";
type Point = [number, number, number];function MarkableBox() {
const [markers, setMarkers] = useState<Point[]>([]);
function handleClick(event: ThreeEvent<MouseEvent>) {
event.stopPropagation();
if (!event.face) return; event.object.updateWorldMatrix(true, false); const normalMatrix = new Matrix3().getNormalMatrix(
event.object.matrixWorld
);
const worldNormal = event.face.normal
.clone()
.applyMatrix3(normalMatrix)
.normalize();
const markerPosition = event.point
.clone()
.addScaledVector(worldNormal, 0.08);
setMarkers((previous) => [
...previous,
[
markerPosition.x,
markerPosition.y,
markerPosition.z,
],
]);
}
return (
<>
<mesh onClick={handleClick}>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="orange" />
</mesh>
{markers.map((position, index) => (
<mesh key={index} position={position}>
<sphereGeometry args={[0.08, 16, 16]} />
<meshBasicMaterial color="red" />
</mesh>
))}
</>
);
}
export default function Scene() {
return (
<div style={{ height: 450 }}>
<Canvas camera={{ position: [4, 3, 6] }}>
<ambientLight intensity={0.5} />
<directionalLight
position={[3, 5, 2]}
intensity={2}
/>
<MarkableBox /> <OrbitControls />
</Canvas>
</div>
);
}

Here’s what happens when I click the box.

First, React Three Fiber calculates the raycast intersection.

Then event.point gives me the clicked surface position.

event.face.normal gives me the face normal in local coordinates.

I transform that normal into world space using a normal matrix.

Then I move the clicked point slightly outward along that normal.

Finally, I store the new marker position and render a red sphere there.

That’s how the marker is placed outside the surface.

9. What Happens If the Box Is Rotated?

Let’s say I rotate the box:

<mesh
rotation={[0, Math.PI / 4, 0]}
onClick={handleClick}
>

This rotates the box by 45 degrees around the Y-axis.

If I used the local normal directly with the world-space intersection point, the marker could move in the wrong direction.

But because I’m converting the normal into world space, the offset follows the rotated surface.

This is another example of why coordinate systems matter so much in 3D development.

10. Surface Normals and Lighting

I also learned that surface normals play an important role in lighting.

Imagine shining a directional light onto a box.

The renderer needs to know which direction each surface faces.

A surface facing the light generally receives more direct illumination.

A surface facing away receives less.

This is one reason why the same material can appear bright on one face and dark on another.

I also discovered that there are different kinds of normals.

A face normal is perpendicular to a triangle.

A vertex normal is associated with a vertex and can be interpolated across a surface for smooth shading.

For today’s marker example, I’m using the intersected face’s normal.

But for more advanced applications, I’ll need to understand when to use face normals and when to use interpolated normals.

11. Why This Matters in Real Applications

Surface normals are useful in many 3D applications.

They can help position markers on a model.

They can help orient arrows and other objects.

They are important for lighting and shading.

And they can help determine the orientation of a selected surface.

For example, in a 3D measurement application, a user might click a model to place a reference marker.

The surface normal could help position a visible marker slightly above the surface.

Of course, the exact surface point should still be preserved separately if it will be used for measurement.

The outward offset is only for visualization.

That’s an important distinction.

What I Learned on Day 16

Today, I learned that:

  • A surface normal is perpendicular to a surface.
  • Normals are represented using vectors.
  • Normals help with lighting, orientation, and surface offsets.
  • event.face.normal provides the intersected triangle's local-space normal.
  • event.point provides the world-space intersection point.
  • A normal matrix converts local normals into world space correctly.
  • .clone() prevents unwanted changes to the original vector.
  • .addScaledVector() lets us move a point along a direction.
  • We can use normals to position markers slightly outside a model.

But my biggest takeaway is this:

A surface point tells me where something is. A surface normal tells me which direction the surface is facing.

And when I combine the two, I can position objects relative to a 3D surface much more accurately.

What’s Next?

For Day 17, I want to explore measuring distances between two selected points in 3D space.

Now that I can click a surface and place markers, I want to understand how to calculate the distance between two points using Vector3.distanceTo().

I’ll also explore why model units matter when interpreting the result.

One concept at a time.

References

Day 16 of my Three.js learning journey — learning in public, one concept at a time.

--

--