Skip to content

Instantly share code, notes, and snippets.

@ayamflow
Last active November 27, 2025 04:32
Show Gist options
  • Select an option

  • Save ayamflow/ad1c9d7b9632e26632992cc3e7b239b2 to your computer and use it in GitHub Desktop.

Select an option

Save ayamflow/ad1c9d7b9632e26632992cc3e7b239b2 to your computer and use it in GitHub Desktop.
RMF (Rotation-minimizing frames) for consistent curve normals
// Compute rotation-minimizing frames to get proper orientation for the instances
// based on the Wang & al paper https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/Computation-of-rotation-minimizing-frames.pdf
// https://stackoverflow.com/a/25458216/2349765
// https://pomax.github.io/bezierinfo/#pointvectors3d
import { CubicBezierCurve3, CurvePath, Vector3 } from 'three';
const up = new Vector3(0, 1, 0);
const tempVec3 = new Vector3();
function computeRMFrames(curve, step) {
const frames = [getVectorFrame(curve, 0)];
for (let t0 = 0; t0 < 1; t0 += step) {
const x0 = frames[frames.length - 1];
const t1 = Math.min(1, t0 + step);
const frame = getRMFrame(x0, getVectorFrame(curve, t1));
frames.push(frame);
}
// recompute first frame
frames[0] = getRMFrame(frames[1], frames[0]);
return frames;
}
function getVectorFrame(curve, t) {
const origin = curve.getPointAt(t); // use getPointAt/getTangentAt if you want equidistant points on the curve
const tanget = curve.getTangentAt(t);
const normal = new Vector3().crossVectors(up, tangent).normalize();
const axis = new Vector3().crossVectors(tangent, normal).normalize();
return {
origin,
tangent,
normal,
axis,
};
}
const v1 = new Vector3();
const v2 = new Vector3();
const riL = new Vector3();
const tiL = new Vector3();
// check the paper or attached links for more understanding of this. I just ported it
function getRMFrame(x0, x1) {
v1.subVectors(x1.origin, x0.origin);
const c1 = v1.dot(v1);
riL.subVectors(x0.axis, v1.clone().scale((2 / c1) * v1.dot(x0.axis)));
tiL.subVectors(x0.tangent, v1.clone().scale((2 / c1) * v1.dot(x0.tangent)));
v2.subVectors(x1.tangent, tiL);
const c2 = v2.dot(v2);
x1.axis.subVectors(riL, v2.clone().scale((2 / c2) * v2.dot(riL)));
x1.normal.crossVectors(x1.axis, x1.tangent);
return x1;
}
// How to use
// Create a threejs Curve/CurvePath
const curve = new CurvePath();
const count = ... // however many points you need on the curve
const step = curve.length / count / curve.length;
const frames = computeRMFrames(curve, step);
for (let i = 0; i < count; i++) {
const frame = frames[i];
const { origin, tangent, normal, axis } = frame;
// create rotation matrix for the frame
const matrix = new Matrix3(
normal.x,
normal.y,
normal.z,
axis.x,
axis.y,
axis.z,
tangent.x,
tangent.y,
tangent.z,
);
const rotation = new Quaternion().setFromRotationMatrix(new Matrix4().setFromMatrix3(matrix).normalize();
object.quaternion.copy(rotation);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment