How to rotate 20 sided Object3D polygon so that a selected face points to the camera?

How do I rotate a 20 sided die model so that a given face points at the camera? (I need to orient the die to show the result of a random roll and I have a list of 20 face normals that I calculate in the code below)

Sorry for the longish question but I wanted to give some background and what I’ve tried so this doesn’t come off sounding like a homework question (which it isn’t, it is for a project I’m working on).

I’m using the model and vertex/face data from this repo: GitHub - evgeny-rov/dice-roller: 3D Dice Rolling App to render a set of 20 sided dice in a grid on the z=0 plane. The vertex/face data for a 20 sided polygon is visible in this file: dice-roller/icosahedron.ts at e6ad3df27df8b5c01d098a94f959209939ec1410 · evgeny-rov/dice-roller · GitHub and I’ve used that to generate the normals using this code (using r124 so that THREE.Geometry is still available):

import { vertices, faces } from "./icosahedron";

const getNormals = () => {
  const geometry = new THREE.Geometry();
  vertices.forEach(({ x, y, z }) => {
    geometry.vertices.push(new THREE.Vector3(x, y, z))
  faces.forEach(([ a, b, c ]) => {
    geometry.faces.push(new THREE.Face3(a, b, c));
  return => face.normal);

I’ve visually verified that the normals are correct by rendering lines for each normal from the center of the model to outside the model using this code when the model loads and all the lines come out perpendicular to the center of each face (I’ve animated a rotation of += 0.01 of x, y, z so I can see all the faces which is not shown here):

const material = new THREE.LineBasicMaterial( { color: 0x0000ff } );
diceModel.position.set(x, y, 0);
normals.forEach(normal => {
  const points = [
    new THREE.Vector3(150 * normal.x, 150 * normal.y, 150 * normal.z)
  const geometry = new THREE.BufferGeometry().setFromPoints(points);
  diceModel.add(new THREE.Line( geometry, material ));

You can see the result here (the red line is a line for the (0,0,1) “up” normal added elsewhere for debugging):

I have a perspective camera looking down using the code below and I’ve used diceModel.lookat( to verify that the red “up” vector points at the camera (before I added the rotation animation).

const aspectRatio = window.innerWidth / window.innerHeight; = new THREE.PerspectiveCamera(80, aspectRatio, 0.1, 1000); = 30;

After researching and thinking about it my intution was to calculate the angular difference between each normal and the objects “up” normalized vector using variations of the following code (note the two ways I tried to calculate the rotation) and then find the final rotation by calling lookAt( and then adding the rotation for the selected face:

const getRotations = (normals: THREE.Vector3[]) => {
  const getRotation = (v1: THREE.Vector2, v2: THREE.Vector2) => {
    // return Math.atan2(v2.y, v2.x) - Math.atan2(v1.y, v1.x)
    return v1.angle() - v2.angle();

  const v = new THREE.Vector3(0, 0, 1)
  return => {
    return {
      x: getRotation(new THREE.Vector2(v.z, v.y), new THREE.Vector2(n.z, n.y)),
      y: getRotation(new THREE.Vector2(v.x, v.z), new THREE.Vector2(n.x, n.z)),
      z: getRotation(new THREE.Vector2(v.x, v.y), new THREE.Vector2(n.x, n.y)),

My thought was that the rotation of each axis would be the 2d rotation with that axis removed but that did not work and I’m stuck (I’ve also tried many other crazy approaches). Searching for this seems to always return answers about rotating the camera around the object and not rotating the object to face the camera.

Any help would be appreciated and I apologize if my bizarre math above caused a spit take.