Rift of the NecroDancer

Rift of the NecroDancer

30 ratings
Custom Portraits, Backgrounds, and More!
By 96 LB
Want to make your workshop level stand out with unique character art? This guide explains everything you need to know about designing and adding custom portraits to your tracks, as well as extra customizations to the visual effects such as character-specific particles and background colors.
2
3
2
   
Award
Favorite
Favorited
Unfavorite
Introduction
The 10th Anniversary update snuck in a huge new feature without much fanfare:



That's right—workshop levels can now feature completely custom portraits to fight monsters from the rift alongside (or in place of) Cadence! Since the update, you may have already seen some levels make use of custom portraits. While you can find some introductory directions at the the official advanced features link[vortexbuffer.com], this guide aims to thoroughly break down all the information you need to add portraits to your own levels. It'll also explain all the little quirks, bugs, and undocumented features that exist so that you can perfect the look and feel of your custom level.

This guide was last updated on 24 June, 2025 and references version 1.5.1. If any further patches have been released since then, some information may be outdated.

Before we start, here are some frequently asked questions:

Do I need a mod to create custom portraits? Do players need a mod to view them?
No! While the feature originated as a mod[github.com], the developers were kind enough to support it officially in the base game! Now, no mods or DLCs are required to enjoy custom portraits, and all charts originally made with the mod are automatically compatible with the unmodded game.

What format should my portrait animations be in?
Each frame of your animation should be a separate image in PNG format. Do not use an animated format or a spritesheet. Each sprite will be scaled up to fit the available space, so the size isn't too important, but the official portraits are usually 2048×2048 pixels. You don't need a specific number of frames, but 5 or 6 is usually a good amount.

Why is my background green, even when I'm not in practice mode?
There's currently a bug which causes custom portraits to use the Beastmaster's background instead of the one you specify in the level editor. Luckily, there's a workaround for this in the Advanced Information section of this guide!

Can I customize the particles in the background?
Yes! You can change their color, use sprites from a different character, or even upload your own custom sprites. More details are in the advanced information section.

Can I remove the portrait frame? I want my character to appear with no background.
Yes, but it requires the use of undocumented features. Check out the advanced information section for more details on how to create and edit a 'portrait.json' file.

Can I add custom voicelines?
No, this is not currently supported. However, future updates to the game may bring this feature.
Basic Usage
Adding custom portraits to your level is done outside of the usual level editor. Instead, you'll add images directly to your track directory, and the game will automatically load them as portraits.

Opening your track directory
The best way to find the folder which holds all of your track information is through the level editor. Press the 'Open' button or keybind to bring up the list of tracks, and select the 'Open track directory' option.



For easy access in the future, consider bookmarking the CustomTracks folder in your file explorer. You can also directly navigate to the folder with one of the following paths:

[Windows] %USERPROFILE%\AppData\LocalLow\Brace Yourself Games\Rift of the NecroDancer\CustomTracks [Linux] ~/.local/share/Steam/steamapps/compatdata/3154920/pfx/drive_c/users/steamuser/AppData/LocalLow/Brace Yourself Games/Rift of the NecroDancer/CustomTracks/

Inside your CustomTracks directory, you will see a list of all of your custom levels. Open up the one you want to add custom portraits to—this folder is where we will do all of our work!




Setting up your folders
The game searches for images to load as custom portraits within a very specific directory pattern. Make sure you follow these steps closely, and watch for any typos!

First, create a folder called 'CustomPortRifts' inside your track directory. This is the name of the mod which originally introduced this feature, and the vanilla game inherited this naming convention. Make sure the new folder exists in the same spot as the 'info.json' file for your level.



Inside of your 'CustomPortRifts' folder, add a new folder called 'Hero' if you want to replace Cadence, and/or 'Counterpart' if you want to replace the character on the right side. If you want to replace both, add both folders! The process for creating your portraits will be the same regardless of which side you pick.



Finally, create up to four folders inside your 'Hero' and/or 'Counterpart', one for each pose that you intend to create custom sprites for. The possible poses are as follows:
  1. VibePower: Displayed when vibe power is active
  2. DoingPoorly: Displayed when the player has less than 3/10 HP
  3. DoingWell: Displayed when the player has 80+ combo
  4. Normal: Displayed at all other times
Not all four poses are necessary, but it's strongly recommended that you at least add 'Normal'. If you don't add a 'VibePower' pose, then if available the game will use your 'DoingWell' pose while vibe power is active.




Adding your sprites
The last step is to actually add your image files to the directories you've created. This part's pretty straightforward, but there are a few guidelines to keep in mind.

Firstly, all images must be in PNG format! No sprites will be loaded unless they have a .png extension. The size is not too important, because the game will automatically scale the image, but the official portraits are usually 2048×2048 pixels.

If you want the portrait to just be a static image, just throw your image file inside of the folder for the corresponding pose, and you're done! For animations, you should add each frame as a separate image. Do not add an animated file or a spritesheet; these are not supported. The frames will be loaded in alphabetical order by filename, but unlike the folders the specific names you use will not matter. Each frame will display at the start of each beat in quick succession. Using about five or six frames is typically best, but it's totally fine to use more or fewer. For more details on the specifics of the animations, see the following sections.



Once you've added the images, try out your track in-game to see your portraits in action! You will need to exit to the song select menu every time you add or edit your sprites—quick retry doesn't reload custom portraits. If you test it out and you're seeing a vanilla character, double check that you named all the folders correctly and are using PNG images. If you instead see a mannequin, that means the game is recognizing that you have custom portraits but is failing to load your sprites for some reason. This can also happen at the start of the level if you don't have a 'Normal' pose.



If your portraits are loading fine, you're ready to publish to the workshop! You don't have to do anything special for this part; simply upload your track as usual through the official level editor, and the portraits will be attached. To add, edit, or remove portraits in the future, simply follow the above directions again and then republish your level.
Advanced Information
Animations
When multiple images are specified in a pose directory, the game treats it as a sequence of animation frames, in alphabetical order. These frames quickly cycle once per beat. The speed of the animation depends only on the BPM of the song, and not the number of frames you use. The animation will play once per beat, and the exact timing is as follows:
  1. The first frame displays for 3/31ths of a beat
  2. All frames after the first will display for 2/31ths of a beat
  3. If the end of the animation is reached, the animation pauses on the last frame until the start of the next beat
This means that up to 15 frames can be specified, and any frames after the 15th will not have time to show in the beat cycle. Most official portraits only use about 5 or 6 frames, however. One notable exception is Cadence's vibe power animation, which uses all 15 frames in order to animate her rapidly flowing hair and clothing.

Each frame is scaled up to fit the portrait's bounding box and hidden behind a mask. This means you don't need to manually hide the portion of the sprite which sticks out of the bottom of the portrait frame. However, if you want finer control over the masking of your sprite (especially since the far left and right are clipped by the default mask), you can disable the mask entirely using the experimental settings detailed later.

Limitations
  1. There is currently no way to play an animation when the player loses a life, even if you replace Cadence.
  2. Voicelines are disabled when a custom portrait is active. There does exist an experimental setting to turn them back on, but it doesn't seem to work. The developers have stated that there are plans to allow custom voicelines in the future.
  3. Due to a bug, when using a custom counterpart portrait the background color and particles are NOT set by the character you choose in the editor like the official documentation claims. Instead, the background visuals will always appear as if you are in practice mode (green background with Beastmaster particles). Luckily, there is a way to fix this one using the experimental background settings detailed below.

Differences with Custom PortRifts
While the official implementation was designed to be compatible with the original Custom PortRifts mod, there are a few minor differences in implementation. This section is only relevant if you previously created a chart with the mod's GitHub page as a reference.
  1. The order of animation frames is different. The mod rested on the first frame at the end of the animation cycle, whereas the official implementation rests on the final frame.
  2. When poses are missing, the mod used the most similar pose available. The official implementation simply doesn't switch animation states.
  3. The mod did extra processing to the sprites which fixed a rare spriting mistake but greatly increased load times. If your sprites appear with a white border, make sure all fully transparent pixels in your image are set to RGBA(0, 0, 0, 0).

Experimental Portrait Settings
⚠️ As this isn't yet officially documented, all the information in this section is subject to change with future updates. Use at your own risk!

If you want more fine-tuned control over the appearance of your portraits, you can create a configuration file named 'portrait.json' inside the 'Hero' or 'Counterpart' folder.



The contents of the text file should be a JSON object, and you can optionally specify the following keys:
  • OffsetX (float): Shifts the portrait horizontally. The width of the screen is 1920 units.
  • OffsetY (float): Shifts the portrait vertically. The height of the screen is 1080 units.
  • DisableMask (boolean): If set to true, the mask is disabled and none of the portrait will be hidden by frame.
  • InvertReactions (boolean): If set to true, swap the DoingPoorly and DoingWell animations.
  • MuteVO (boolean): If set to true, mutes character voicelines. Has no effect if using a custom character portrait.
  • PortraitID (string): Overrides the character to be loaded. If you change this, your custom portrait will not load, but it can be useful if you want to load a vanilla character and either invert their reactions or mute their voicelines (the other settings have no effect). You can also use the ID 'CustomCounterpartNative' to remove the portrait frame. If the portrait ID you specify is invalid, the portrait and frame will not appear and the background will become a static image of a starry sky, overwriting your background video unless you also use the experimental background settings.
  • Animations (object): Overrides the animation sequence entirely. More detail in a later section.

Here is an example configuration file:
{ "OffsetX": 0, "OffsetY": -100, "DisableMask": true, "MuteVO": true, "InvertReactions": true, "PortraitID": "NecrodancerBurger" }

Experimental Background Settings
⚠️ As this isn't yet officially documented, all the information in this section is subject to change with future updates. Use at your own risk!

Using a custom counterpart portrait completely overwrites the background visual effects, so this section details undocumented features which allow you to customize the background. This includes custom colors and particles. To accomplish this, create a configuration file named 'vfx.json' inside your track directory (NOT the 'CustomPortRifts' subfolder).



This one has a lot of very messy options, so the full specification will be detailed in a later section. Unless you want to do some hardcore customization, you only need to use one setting. To use the background of one the vanilla characters, the contents of your 'vfx.json' file should just be this:

{"CharacterID": "NAME_GOES_HERE"}

In place of 'NAME_GOES_HERE', use the name of the character whose background you want to use (Beastmaster, Coda, Dove, Harmonie, Heph, Matron, Merlin, NecrodancerBurger, NecrodancerCloak, Nocturna, Queen, Reaper, Shopkeeper, Suzu).


Custom Particles
⚠️ As this isn't yet officially documented, all the information in this section is subject to change with future updates. Use at your own risk!

Though it's not stated anywhere in the official documentation, you can actually use custom images for the floating icons in the background! The details for this are a little bit different than custom portraits. Instead of specifying a set of images, you need to create a black and white spritesheet containing all of the possible particle sprites. Color information is completely ignored; instead, the brightness of each pixel is used to determine how opaque it is. Here's a sample spritesheet:



To use the spritesheet, you'll need to create a 'vfx.json' configuration file if you haven't already. See the previous subsection for details. Then, add your image anywhere in your track directory, preferably in the same folder as 'vfx.json'. You need to add three keys to your JSON file to specify the path to the file and the dimensions of the sheet. Here's an example:

{ "CustomParticleImagePath": "particles.png", "CustomParticleSheetWidth": 3, "CustomParticleSheetHeight": 3 }

The path you use should be a relative path, and the width and height are measured by the number of sprites. If you put the spritesheet in the same directory as your configuration file, then you should just write the filename. The default width and height are 2.
Manual Portrait Animation
⚠️ As this isn't yet officially documented, all the information in this section is subject to change with future updates. Use at your own risk!

Instead of relying on the default animation timing and sizing, you instead override the settings and manually specify the details. This allows you to move, scale, modify the timing of, or even change the filepath for each frame of your animations.

It's important to note that overriding animations is all or nothing! If you use this feature, you will have to write out every frame of every animation. There is currently no way to override specific poses or frames.

To get started, you'll need to create a 'portrait.json' configuration file if you haven't already. See the previous section for details. Then, add a key called 'Animations'. The corresponding value is an object that can have up to four keys, one for each pose. The names for each pose here are NOT the same as the ones used by Custom PortRifts and in the basic usage section—instead, the names should be 'PerformingNormal', 'PerformingPoorly', 'PerformingWell', and 'VibePower'. The specification for these objects is simple:
  • Frames (list): A list of AnimationFrame objects describing the pose's animation.

Of course, all the interesting values are within the AnimationFrame objects, which have the following keys. All of them are technically optional, but omitting them ususally breaks your portrait.
  • FileName (string): The path to the image file, relative to the JSON file. For example, if using the default naming schemes, your path might be 'Normal/frame1.png'.
  • Duration (float): The length of the frame, measured in beats. Should be between 0 and 1. Defaults to 1/30, which is different than the timing used normally.
  • OffsetX (float): Sets the horizontal position of the sprite, relative to the center of the screen (unlike the global OffsetX setting!). The width of the screen is 1920 units. Overwrites the global OffsetX specified in 'portrait.json'. If OffsetY is specified, defaults to 0. Otherwise, defaults to the position specified by the global OffsetX.
  • OffsetY (float): Sets the vertical position of the sprite, relative to the center of the screen (unlike the global OffsetY setting!). The height of the screen is 1080 units. Overwrites the global OffsetY specified in 'portrait.json'. If OffsetX is specified, defaults to 0. Otherwise, defaults to the position specified by the global OffsetY.
  • ScaleX (float): Scales the portrait horizontally by a multiplicative factor. Defaults to 1. If specified, you must also specify ScaleY or it will be set to 0 and hide your portrait.
  • ScaleY (float): Scales the portrait vertically by a multiplicative factor. Defaults to 1. If specified, you must also specify ScaleX or it will be set to 0 and hide your portrait.

As a reminder, you must write out these details for every frame of every animation, which is an extremely tedious task to do manually. It's not recommended to use this unless you really need to override the default animation sizings or timings, which suffice for almost all use cases. Here's a sample (truncated) 'portraits.json' file from the game's Miku DLC, where this feature is used. For readability, I removed about half of the frames and one of the poses.
{ "Animations": { "PerformingNormal": { "Frames": [ { "Duration": 1e-06, "FileName": "Normal/Miku_Normal_v08_011.png", "OffsetX": -0.96640625, "OffsetY": 28.3095703125, "ScaleX": 0.857421875, "ScaleY": 0.857421875 }, { "Duration": 0.096774193548387, "FileName": "Normal/Miku_Normal_v08_001.png", "OffsetX": -45.1236328125, "OffsetY": 5.1591796875, "ScaleX": 0.857421875, "ScaleY": 0.857421875 }, { "Duration": 0.064516129032258, "FileName": "Normal/Miku_Normal_v08_002.png", "OffsetX": -50.2681640625, "OffsetY": 9.017578125, "ScaleX": 0.857421875, "ScaleY": 0.857421875 }, { "Duration": 0.064516129032258, "FileName": "Normal/Miku_Normal_v08_003.png", "OffsetX": -51.554296875, "OffsetY": 17.1630859375, "ScaleX": 0.857421875, "ScaleY": 0.857421875 }, { "Duration": 0.064516129032258, "FileName": "Normal/Miku_Normal_v08_004.png", "OffsetX": -48.124609375, "OffsetY": 22.736328125, "ScaleX": 0.857421875, "ScaleY": 0.857421875 } ] }, "PerformingPoorly": { "Frames": [ { "Duration": 1e-06, "FileName": "DoingPoorly/Miku_Doing_Poorly_v08_011.png", "OffsetX": -21.9732421875, "OffsetY": -41.1416015625, "ScaleX": 0.857421875, "ScaleY": 0.857421875 }, { "Duration": 0.096774193548387, "FileName": "DoingPoorly/Miku_Doing_Poorly_v08_001.png", "OffsetX": -30.5474609375, "OffsetY": -95.587890625, "ScaleX": 0.857421875, "ScaleY": 0.857421875 }, { "Duration": 0.064516129032258, "FileName": "DoingPoorly/Miku_Doing_Poorly_v08_002.png", "OffsetX": -25.4029296875, "OffsetY": -91.7294921875, "ScaleX": 0.857421875, "ScaleY": 0.857421875 }, { "Duration": 0.064516129032258, "FileName": "DoingPoorly/Miku_Doing_Poorly_v08_003.png", "OffsetX": -20.2583984375, "OffsetY": -85.298828125, "ScaleX": 0.857421875, "ScaleY": 0.857421875 }, { "Duration": 0.064516129032258, "FileName": "DoingPoorly/Miku_Doing_Poorly_v08_004.png", "OffsetX": -17.6861328125, "OffsetY": -78.8681640625, "ScaleX": 0.857421875, "ScaleY": 0.857421875 } ] } }, "DisableMask": true, "InvertReactions": false, "MuteVO": true, "PortraitId": "CustomCounterpartNative" }
Full Specification of vfx.json
⚠️ As this isn't yet officially documented, all the information in this section is subject to change with future updates. Use at your own risk!

The full set of configuration options for background effects uses two new types. The first is Color, which is an object with the following keys (NOT optional, except for a):
  • r (float): The red component of the color. Ranges from 0 to 1.
  • g (float): The green component of the color. Ranges from 0 to 1.
  • b (float): The blue component of the color. Ranges from 0 to 1.
  • a (float): The alpha component (opacity) of the color. Ranges from 0 to 1. Defaults to 1.

The second is Gradient, which is an array, not an object. The maximum length of the array is 8. The elements of the array should be objects of type GradientStop, which is essentially a Color but with an additional key to specify the time:
  • r (float): The red component of the color. Ranges from 0 to 1.
  • g (float): The green component of the color. Ranges from 0 to 1.
  • b (float): The blue component of the color. Ranges from 0 to 1.
  • a (float): The alpha component (opacity) of the color. Ranges from 0 to 1. Defaults to 1.
  • t (float): The time at which this color is active. Ranges from 0 to 1. If not specified, stops in a gradient are spaced evenly from 0 to 1.

Now, here's the full specification of 'vfx.json'. Note that many options come in pairs of colors or gradients. When this is the case, it usually means the color is randomly sampled from all colors between the two values. If the settings are a gradient, the colors used for the random sampling vary over time. All keys are optional, and you probably don't want to change them unless you know what you're doing.
  • CharacterId (string): The character whose background should be loaded to set the default values of the other keys.
  • CoreStartColor1 (Gradient): The starting color of the clouds emanating from the rift.
  • CoreStartColor2 (Gradient): See above.
  • CoreColorOverLifetime (Gradient): Affects the color of the clouds over time. Multiplied by the starting color.
  • SpeedlinesStartColor (Gradient): The starting color of the thin streaks emanating from the rift.
  • SpeedlinesColorOverLifetime (Gradient): Affects the color of the thin streaks over time. Multiplied by the starting color.
  • BackgroundColor1 (Color): The color of the gradient overtop of the background.
  • BackgroundColor2 (Color): The base color of the background. This is the more important one.
  • BackgroundGradientIntensity (float): The intensity of the gradient.
  • BackgroundAdditiveIntensity (float): The base intensity of the color of the background.
  • RiftGlowColor (Color): The color of the glow around the rift.
  • StrobeColor1 (Color): The color of the flash when using the now deprecated Animation Trigger event.
  • StrobeColor2 (Color): The color of the lines when using the now deprecated Animation Trigger event.
  • CustomParticleColor1 (Gradient): The starting color of the character-specific icons emanating from the rift
  • CustomParticleColor2 (Gradient): See above.
  • CustomParticleColorOverLifetime (Gradient): Affects the color of the character-specific icons. Multiplied by the starting color.
  • CustomParticleImagePath (string): The relative filepath of the custom particle spritesheet. See the custom particle section for more information.
  • CustomParticleSheetWidth (int): The width of the custom particle spritesheet.
  • CustomParticleSheetHeight (int): The height of the custom particle spritesheet.

Here's an example file—this is the one I actually use for my Tetoris map.
{ "CharacterId": "NecrodancerBurger", "RiftGlowColor": { "r": 0.980, "g": 0.314, "b": 0.440, "a": 1 }, "CoreStartColor1": [ { "r": 0.980, "g": 0.314, "b": 0.440, "a": 0.250, "t": 0 } ], "CoreStartColor2": [ { "r": 0.980, "g": 0.314, "b": 0.440, "a": 0.500, "t": 0 } ], "CoreColorOverLifetime": [ { "r": 1, "g": 1, "b": 1, "a": 1, "t": 0.75 }, { "r": 1, "g": 0, "b": 0, "a": 0, "t": 1 } ], "BackgroundColor2": { "r": 1, "g": 0.851, "b": 0.322, "a": 1 }, "BackgroundColor1": { "r": 0, "g": 0, "b": 0, "a": 0 }, "CustomParticleImagePath": "particles.png", "CustomParticleSheetWidth": 3, "CustomParticleSheetHeight": 3, "CustomParticleColor1": [ { "r": 1, "g": 0.133, "b": 0.5, "a": 1, "t": 0.3 }, { "r": 0.133, "g": 1, "b": 0.5, "a": 1, "t": 0.301 } ], "CustomParticleColor2": [ { "r": 0.133, "g": 0.133, "b": 1, "a": 1, "t": 1 } ], "CustomParticleColorOverLifetime": [ { "r": 1, "g": 1, "b": 1, "a": 0, "t": 0 }, { "r": 1, "g": 1, "b": 1, "a": 1, "t": 0.125 } ], "SpeedlinesStartColor": [ { "r": 1, "g": 1, "b": 0, "a": 0.5, "t": 0 }, { "r": 1, "g": 1, "b": 0, "a": 0.75, "t": 1 } ], "SpeedlinesColorOverLifetime": [ { "r": 1, "g": 1, "b": 1, "a": 1, "t": 0 } ] }
Closing Remarks
I've done my best to make this guide as thorough and accurate as possible, but I make mistakes! If there's anything missing or incorrect in this guide, please leave a comment to let me know.

I'd like to extend a huge thank you to the developers (especially Marukyu!) for officially supporting my mod. It really makes me happy to be able to have an impact on the game.

Here's the final product using all the examples throughout the guide:


Try it out yourself!
https://steamhost.cn/steamcommunity_com/sharedfiles/filedetails/?id=3422450367
12 Comments
潭巢 31 Jul @ 11:30pm 
Well, it's maybe a bug. I checked your tetoris and found the speedlines are red instead of yellow which is the same with the necroburger
潭巢 31 Jul @ 8:33am 
Sorry for being late but I know what I was doing because I can see opacity of what are changed when I change the value. I can change their opacity but not their color
96 LB  [author] 28 Jul @ 9:20am 
@潭巢 The speedlines might not be what you expect them to be. They're the thin streaks in the background and are easily confused with the core particles. They also have weird color interactions - they can only lighten the background.
潭巢 28 Jul @ 8:29am 
Why things about speedlines don't work😭 I just copied yours and only found they're completely white not yellow
Doctor Timbrwulf 19 Jun @ 11:58am 
This will be great for making TimbrWulf (my OC for Rytmik Studio tunes, remixes, and AI manipulation) actually animated! Anyone willing to make my OC come to life?
96 LB  [author] 12 Jun @ 1:19pm 
Updated the guide with new information from the latest patch!
96 LB  [author] 10 Jun @ 1:33pm 
@Orez Glad I could help! I don't believe there's a way to use custom portraits in Shopkeeper mode or practice mode right now
Orez 9 Jun @ 5:44pm 
Thank you for this guide! It has been very helpful. (and for making the original mod?? Thank you!)

Do you know if there is a way to use a custom portrait in Shopkeeper Mode?
96 LB  [author] 31 May @ 7:39am 
Yup, that was it! Updated the guide.
96 LB  [author] 29 May @ 7:17pm 
@Clone Fighter I'll take a look at that in a few days once my work clears up - thanks for the tip