Creating VRChat worlds on Ubuntu


I’ve decided to try my luck with using the VRChat SDK on Ubuntu. Because frankly, nobody likes starting a Windows VM just to use some tools that should work on Linux as well. This post is an unstructured info dump with some tips and tricks to get you started.


The first step is to obtain UnityHub. I’m using the AppImage, since that is the least amount of effort. I simply run ./UnityHub.AppImage --no-sandbox to get it working. Sign in like you would normally.

Next, you need a specific ancient version of Unity. At the time of me writing this, the version VRChat requires wasn’t even offered in the UI anymore, but the download can be triggered via a unityhub URL:

./UnityHub.AppImage --no-sandbox unityhub://2019.4.31f1/bd5abf232a62

Under Installs, you’ll want to enable the “Windows Build Support” module for this installation. Create a new project with this Unity version.

Finally, download the VRCSDK3 World unitypackage from VRChat. You can simply install this unitypackage as usual, such as by dragging and dropping the file into the project in Unity.

Open the VRChat SDK control panel and log in. Create your world scene like you would normally.

Building and running

The fun starts when you want to test your world in VRChat. The VRCSDK3 was not made for Linux and definitely has no clue about Steam’s Proton environment. Some things to know:

  • The VRChat Steam AppID is 438100
  • VRChat keeps its data in %AppData%/LocalLow/VRChat
  • Specifically worlds are normally written to %AppData%/LocalLow/VRChat/VRChat/Worlds/
  • VRCSDK3 will produce build artifacts in a temporary directory using Application.get_temporaryCachePath() (which will be /tmp on Linux)
  • The Proton path will be something like ~/.steam/steam/steamapps/compatdata/438100/pfx/drive_c/users/steamuser/AppData/LocalLow/VRChat/

Issue 1: VRCSDK3 determines the path to LocalLow by calling SHGetKnownFolderPath, which is a Windows-specific API. This call will return null on Linux. Luckily, the behavior in VRCWorldAssetExporter.ExportCurrentSceneResource is robust enough and VRCSDK3 falls back to using the temporary path it builds the world in.

Issue 2: to test the world, VRCSDK3 launches VRChat with a number of commandline arguments. One of the commandline arguments specifies the location of the world file on disk. Due to issue 1, this path will be something like /tmp/DefaultCompany/MyWorld/foo.vcrw which is not accessible in Proton using that name. Luckily we can translate the world path by turning




so that VRChat can find the world file in the Proton environment, since the Z: drive maps to the Linux filesystem in Proton.

Since the SDK allows you to specify the path to the VRChat executable (Settings -> VRChat Client), we can easily write a script that patches the argument and launches the VRChat binary through Steam. An example script that I’ve called launch.exe:


import sys
import subprocess

for i, arg in enumerate(sys.argv):
    # Inject Z: into path for Proton
    if arg.startswith('--url'):
        sys.argv[i] = sys.argv[i].replace('url=file:///', 'url=file:///Z%3a')

    ['steam', '-applaunch', '438100'] + sys.argv[1:],

The only caveat is that if the application is already running Steam will not launch a second process, which means a world reload would not be triggered. You can just rejoin the world to reload, however.

Issue 3: publishing worlds doesn’t work out of the box. The issue seems to be that Unity converts filenames to lowercase during the BuildPipeline.BuildAssetBundles call. This results in VRCSDK3 looking for scene-StandaloneWindows64-SampleScene.vrcw while the file is actually scene-standalonewindows64-samplescene.vrcw on disk. This obviously isn’t an issue on Windows since it doesn’t use case-sensitive filesystem, but on Linux this does not work.

You can fix this by making a symlink to the lowercased file:

cd /tmp/DefaultCompany/MyWorld
ln -s scene-standalonewindows64-samplescene.vrcw scene-StandaloneWindows64-SampleScene.vrcw

Or, you can add the following button to the UI to fix the path variable (which makes the “Last Build” buttons actually work):

// Assets/VRCSDK/SDK3/Editor/VRCSdkControlPanelWorldBuilder3.cs
// in: public override void OnGUIScene()

if (GUILayout.Button("[Linux] Fix lastVRCPath")) {
    string path = EditorPrefs.GetString("lastVRCPath");
    string[] parts = path.Split('/');
    if (parts.Length > 0)
        parts[parts.Length - 1] = parts[parts.Length - 1].ToLower();
    path = string.Join("/", parts);
    EditorPrefs.SetString("lastVRCPath", path);
    Debug.Log("lastVRCPath: "+ EditorPrefs.GetString("lastVRCPath"));

And there you go!

Unity and VRChat running on Ubuntu