Using Roblox's avatar 3D api to import users' avatars into a website (or whatever you'd like)

Introduction

Hello! I was implementing Oauth 2.0 into a serverless website that I was making, and I stumbled upon roblox’ avatar 3D api and thought it’d be a good idea to get the actual player model of the user who just logged in with oauth. I made a terrible mistake these have been the worst 5 days of my life. I got it working but trying to research this has been a horrible nightmare because there’s nothing out there explaining this api in the slightest, so today I’ll show you how to use the avatar 3d api, and how to handle the files that it returns.

Example of final product:


The api

The api endpoint is https://thumbnails.roblox.com/v1/users/avatar-3d?userId=[id]. After you call it with the correct userId, it’ll dish out a response that looks like this:

{
  targetId: 2742412611,
  state: 'Completed',
  imageUrl: 'https://t4.rbxcdn.com/25f36fd018c36066eddc810c4abbf9b6'
}

NOTE: if the state property is set to Pending or anything other than Completed the imageUrl will return null and could break your app, so keep that in mind!

Now you have to fetch the data from that imageUrl property. After doing that you’ll get a response that looks like this:

{
  "camera": {
    "position": {
      "x": 0.563166,
      "y": 106.218,
      "z": -11.061
    },
    "direction": {
      "x": 0.0924327,
      "y": 0.258464,
      "z": -0.961589
    },
    "fov": 28.3604
  },
  "aabb": {
    "min": {
      "x": -2.67336,
      "y": 99.9628,
      "z": -1.15667
    },
    "max": {
      "x": 1.58785,
      "y": 105.809,
      "z": 3.63516
    }
  },
  "mtl": "4454f6bdf777bd9d88d3929e41a93d48",
  "obj": "01efa139613a6de7f98d7924596802aa",
  "textures": [
    "9b1e86351ca50dd3f6021578952e0ae7",
    "9a5cbc3b0692eb30259c21ddc83977aa",
    "46e9af1282cd9f26dffcbaca7d0aac9b",
    "57032812c5fc8d6194e41882998d92b6",
    "da7bb59fcf904771a16b8958c4e8bd3a",
    "f582e4cdb727c3b6469fb8977394a764",
    "c39fb360309303733bc873ce2c8166f1",
    "63a9c20adda77d8f68c2d618cd19e9a9",
    "1369e5ee30066a3b4986907d71df2112"
  ]
}

You can probably tell what all of these properties do, but in case you’re wondering what the aabb is, this simply gives us coordinates that make the bounding box of the avatar. This will be very useful later.

Now it’s actually impossible to get the files just from this, even though you have the IDs of these files, you have to ping the correct roblox cdn server. You see for whatever reason, all of these files are on completely different cdn servers and you have to get each file seperately. If you’re not familiar with cdns, a cdn link might look like this: https://t4.rbxcdn.com/.... This means you’re requesting a file from the 4th server region in the cdn.

But fear not! We can actually figure out which cdn server to ping. That’s because the ID is actually a hash and has the exact server we’re looking for inside of it. I personally had 0 idea how to get that server, and it was absolutely not documented anywhere. This answer by @local_ip had the solution and I am forever grateful to have found it. Orrrrr at least it used to… Roblox updated their API, and it’s a different formula. In javascript the NEW function for getting the cdn looks like this:

function get(hash) {
 for (var i = 31, t = 0; t < 38; t++)
   i ^= hash[t].charCodeAt(0);
 return `https://t${(i % 8).toString()}.rbxcdn.com/${hash}`;
}
Code Explanation Essentially what this codes does is it takes the ID, iterates through the first 32 characters, and performs a bitwise XOR operation on each character's integer representation (either directly or through conversion from hexadecimal) with the variable "i". The result of each iteration is stored back in "i" for the next iteration.

After getting the CDN server, you can directly access the player model files directly from there. With a get request to the url.

Handling and rendering the files

For the rest of this tutorial, code examples will be in react using react-three/fiber, which works similarly to a wrapper for three.js. I will be explaining every step though, so you should still be able to understand what I’m doing.

Reminder of the files returned by the API
{
  "camera": {
    "position": {
      "x": 0.563166,
      "y": 106.218,
      "z": -11.061
    },
    "direction": {
      "x": 0.0924327,
      "y": 0.258464,
      "z": -0.961589
    },
    "fov": 28.3604
  },
  "aabb": {
    "min": {
      "x": -2.67336,
      "y": 99.9628,
      "z": -1.15667
    },
    "max": {
      "x": 1.58785,
      "y": 105.809,
      "z": 3.63516
    }
  },
  "mtl": "4454f6bdf777bd9d88d3929e41a93d48",
  "obj": "01efa139613a6de7f98d7924596802aa",
  "textures": [
    "9b1e86351ca50dd3f6021578952e0ae7",
    "9a5cbc3b0692eb30259c21ddc83977aa",
    "46e9af1282cd9f26dffcbaca7d0aac9b",
    "57032812c5fc8d6194e41882998d92b6",
    "da7bb59fcf904771a16b8958c4e8bd3a",
    "f582e4cdb727c3b6469fb8977394a764",
    "c39fb360309303733bc873ce2c8166f1",
    "63a9c20adda77d8f68c2d618cd19e9a9",
    "1369e5ee30066a3b4986907d71df2112"
  ]
}

Setting up the camera

Positioning the camera and setting it up properly can be a little tricky. We can start by using the camera.position, and fov properties that the api provides to set the position and fov accordingly.

<PerspectiveCamera makeDefault far={500} near={0.1} fov={camera.fov - 10} 
position={new Vector3(camera.position.x, camera.position.y, camera.position.z)}/>

Instead of using the camera.direction that the api returns, I want the camera to be looking in the center properly, this is where the aabb comes in handy. If we want the camera to be looking at the center of the model, then we can average out from each corner to get the center of the model. The equation looks like this:

(aabb.min.x + aabb.max.x)/2, (aabb.max.y + aabb.min.y)/2, (aabb.max.z + aabb.min.z)/2)

Then, we can just make the camera look directly at this point in space.

<PerspectiveCamera makeDefault far={500} near={0.1} fov={camera.fov - 10} 
position={new Vector3(camera.position.x, camera.position.y, camera.position.z)}
onUpdate={self => self.lookAt(new Vector3((aabb.min.x + aabb.max.x)/2, (aabb.max.y + aabb.min.y)/2 + 2, (aabb.max.z + aabb.min.z)/2))}/>

Loading the mesh and textures

We can easily load in the obj file with the url for it. It’ll automatically be placed in front of the camera for you.

const LoadModel = (props) => {
    const object = useLoader(OBJLoader, props.url)
    return <primitive object={object} />
};

This should make your scene look like so (I modified my fov and camera positioning to make it look more like a bust):

Now we just need to load the textures! Oh yeah. The textures.
Roblox does this silly little thing where all of its exports have an alpha map that makes the model transparent. Why? Idk don’t ask me, I don’t work with modeling. But thankfully all we have to do is disable the alphamap.

const LoadModel = (props) => {
    const materials = useLoader(MTLLoader, props.mtl)
    
    for (const key in materials.materials) {
      materials.materials[key].transparent = false
    }

    const object = useLoader(OBJLoader, props.url, loader => {
      loader.setMaterials(materials)
    })


    return <primitive object={object} />
};

Woo!!! Our character!!! All textured an-

WHAT THE HELL IS THIS THING

Remember when I said that roblox stores their files on different servers? This applies to all of the textures too. What’s happening is the mtl loader is seeing that the url of the mtl file is on, for example, t1.rbxcdn.com. So it automatically assumes that all of the texture files must be on t1.rbxcdn.com. One solution is to just download all of the files for the character onto your server, but it’s really slow and won’t work in serverless environments.

Okay, so how do we fix this?
It’s actually really simple, we just need to intercept the url that each resource is using, and reformat it to the right cdn. The code looks something like this:

const LoadModel = (props) => {
    const materials = useLoader(MTLLoader, props.mtl)
    
    for (const key in materials.materials) {
      materials.materials[key].transparent = false
    }

    const object = useLoader(OBJLoader, props.url, loader => {
      loader.setMaterials(materials)

      loader.materials.manager.setURLModifier( ( url ) => {
        var id = url.split('com/')[1]
        return get(id);
      });
    })


    return <primitive object={object} />
};

And there you have it! Just add some lighting and your fully rendered character is there. I added some spinny effects to mine :]

Setting a fallback model:

36 Likes

This is an excellent explanation of the Avatar 3D API, you’ve saved me the work of having to figure this out myself!

Yeah, I bet-

5 Likes

Here’s a good example of this being used in my roblox oauth verification system. Works out super nicely!

2 Likes

Oooh, this looks neat! Nice work!

1 Like

Thanks for this tutorial, it was very helpful.
I used it for the avatar on my user card ui element

4 Likes

P.S. I mentioned earlier that sometimes when grabbing the avatar, it could be pending.

You can actually check for this, simply detect if the state is anything other than completed. If it is, then you can use the assets-thumbnail-3d api and import a mesh from the creator marketplace in place of the avatar. The api returns the same values as the avatar api so you can simply replace the avatar-3d api with this one as a fallback.

Here’s an example with me using https://thumbnails.roblox.com/v1/assets-thumbnail-3d?assetId=13334908083 as a fallback.

2 Likes

this is so cool, thank you for this!

1 Like

Hello hello!

Roblox updated their API and made a huge part of my tutorial completely invalid YAAAAAAAAYYYYYYYY

I have updated the tutorial with the correct information, for more information on the new solution to decoding the hashes of the items please see this topic:

2 Likes

oh yeah i dare you to say it :skull:
nah jk i wonder if this got patched, just found it by searching lol.

at least be glad it’s just standard .obj files instead of their own custom mesh format

1 Like

honestly i wouldnt even be that upset it’d be really cool even if it took forever

Hey, is this out updated tutorial out yet!?!

It should be, I just edited the original post.