Skip to content

BodyBlend v0.3 Implementation Guide

maiesen edited this page Apr 18, 2024 · 5 revisions

BodyBlend.dll Implementation Guide

This guide assumes that you have at least gone through the skin mod creation tutorial: Creating skin for vanilla characters with custom model and has built your mod in Unity.

If it was not clear, this guide is for Risk of Rain 2 modding.

Preparing your model for Unity BlendShapes

If you want to use BlendShapes, your model needs to have them first. The equivalent of BlendShapes in Blender is called Shape Keys. It is important that you check the option Import BlendShapes in the model import settings.

image

You can test your BlendShapes by dragging your model file into the scene Hierarchy, clicking on the mesh with Skinned Mesh Renderer component, and changing the BlendShapes values.

image

Creating your configuration file.

In version 0.3.x of BodyBlend or later, configuring the skin must be done with a .json file. The file must be named as {SKIN_TOKEN}.json and be put into your package ZIP file next to the .dll. You can find the skin token in your generated code after building the skin mod (Assets/SkinMods/ModName/ModNamePlugin.cs).

...
skin.nameToken = "MAIESEN_SKIN_STEAMPUNKLOADERSKIN_NAME";
...

Your file name should look something like this: MAIESEN_SKIN_STEAMPUNKLOADERSKIN_NAME.json

Writing the configuration (do not include lines with //)

A minimum functioning .json would look like this:

{"parts":[
  {
    "name": "Breasts",
    "controls": [
      {
        "targetRendererIndex": 0,
        "blendControls": [
          {
            "blendIdx": 0,
            "keyframes": [{"val": [0, 0]},{"val": [1, 1]}]
          }
        ]
      }
    ]
  }
]}

However realistically you will need more than that. Below is a configuration used by my Steampunk Loader skin mod.

// MAIESEN_SKIN_STEAMPUNKLOADERSKIN_NAME.json
{
  // Name of the skin that will show up in the in-game BodyBlend settings menu.
 "skinName": "Steampunk Loader",
 "parts":[
  {
    // Name of the body part to control. Currently other mods use "Belly" and "Breasts".
    "name": "Belly",
    // Each control can only target 1 renderer. For skin with BlendShapes on multiple meshes for the same part, additional control is needed.
    "controls": [
      // 1st control
      {
        // Index of a renderer that contains the mesh with BlendShapes. See explanation below for where to get the number.
        "targetRendererIndex": 2,
        // Blend control for each BlendShape you want to use for one body part.
        "blendControls": [
          {
            // The index corresponds to the index of your BlendShape. It's exactly the same as how it's ordered in Blender.
            // You can also see the same order when you import the model into Unity.
            "blendIdx": 0,
            // This will determines how much influence the specified BlendShape has at different input values.
            // Setting [{"val": [0, 0]},{"val": [1, 1]}] here will scale the BlendShape linearly from 0.0 to 1.0.
            "keyframes": [{"val": [0, 0]},{"val": [1, 1]}]
          },
          {
            // This is a corrective BlendShape that adjusts the intermediate blend to be nicer looking.
            "blendIdx": 2,
            // Here it is set up so that the max value (1.0) is reached at 40% (0.4) progress and then go back down to 0.0 at 100% (1.0) progress.
            "keyframes": [{"val": [0, 0]},{"val": [0.4, 1, 0, 0]},{"val": [1, 0]}]
          }
        ],
        // Bone controls are for changing dynamicbones properties in case you want to change the physics of the bones as the body part changes.
        // Bone controls are optional.
        "boneControls": [
          {
              // List of bones that will have their values adjusted.
              "boneNames": ["belly"],
              // Properties available for config: inertCurve, elasticityCurve, stiffnessCurve, dampingCurve
              "inertCurve": [{"val": [0, 0.75]},{"val": [1, 0]}],
              "elasticityCurve": [{"val": [0, 1]},{"val": [0.4, 0]},{"val": [1, 0]}]
          }
        ]
      },
      // 2nd control
      // This is basically the minimum configuration that you need.
      {
        "targetRendererIndex": 1,
        "blendControls": [
          {
            "blendIdx": 0,
            "keyframes": [{"val": [0, 0]},{"val": [1, 1]}]
          }
        ]
      }
    ]
  },
  // Here you can add additional part.
  {
    "name": "Breasts",
    "controls": [
      {
        "targetRendererIndex": 2,
        "blendControls": [
          {
            "blendIdx": 1,
            "keyframes": [{"val": [0, 0]},{"val": [1, 1]}]
          }
        ],
        "boneControls": [
          {
              "boneNames": ["breast.l", "breast.r"],
              "inertCurve": [{"val": [0, 0.5]},{"val": [1, 0]}],
              "elasticityCurve": [{"val": [0, 0.25]},{"val": [1, 0]}]
          }
        ]
      }
    ]
  }
]}

**In order to find which index to use, you can check the tables found here. When counting the renderer indices, skip the ones that doesn't exist in display model. For example, MageMesh is index 3 because Fire, JetsL, JetsR, FireRing do not exist in display model (7 - 4 = 3).

**Each keyframe value is constructed like Unity Keyframe. You can see how to use it here: Keyframe Constructor

  • [time, value]
  • [time, value, inTangent, outTangent]
  • [time, value, inTangent, outTangent, inWeight, outWeight]

...and that's pretty much it. You can reach me in discord at maiesen if you have any issues.

(For non-skin mods) Controlling the BlendShapes

To use BodyBlend, all you need is a reference to the character model object and a bit of code.

// Assuming you have reference to the CharacterBody component (body)
// You can get BodyBlendController component from the character model object.
BodyBlendController controller = body.modelLocator.modelTransform.gameObject.GetComponent<BodyBlendController>();

float value = GetValue();
// Set the target weight (input) with value.
// "Source" is for identifying where the input value come from. It is useful in case multiple mods try to set input to the same "BodyPart"
// I recommend just using your item/equipment/etc. token name as it should be unique
if (controller)
    controller.SetBlendTargetWeight("BodyPart", value, "Source");

...
// Values set by SetBlendTargetWeight is additively evaluated across all sources. 
// With SetBlendTargetWeightPercent, the resulting weight will be calculated after the additive sources are summed up. The value scales from that sum to 1.0f.
// Only the highest percentage value from all sources are evaluated.
if (controller)
    controller.SetBlendTargetWeightPercent("BodyPart", value, "Source");

I recommend using soft dependency for BodyBlend as it only serves cosmetic function with no game-play elements. Mod Compatibility: Soft Dependency

Below is a code for compatibility class. You should check enabled before calling any function to BodyBlend.

// BodyBlendCompatibility.cs
using BodyBlend;
using RoR2;

namespace YOURMOD
{
	class BodyBlendCompatibility
	{
		private static bool? _enabled;

		public static bool enabled
		{
			get
			{
				if (_enabled == null)
				{
					_enabled = BepInEx.Bootstrap.Chainloader.PluginInfos.ContainsKey(BodyBlend.BodyBlendPlugin.GUID);
				}
				return (bool)_enabled;
			}
		}

		// Auto configuration for all loaded body parts.

		// Set the default values you want for the part configs.
		private static Dictionary<string, float> DefaultPartInfluences = new Dictionary<string, float>()
		{
			{ "Belly", 50f },
			{ "Breasts", 50f },
		};

		protected static Dictionary<string, ConfigEntry<float>> PartInfluences = new Dictionary<string, ConfigEntry<float>>();

		// Call this during Start() to hook to BodyBlend after loading all config files.
		public static void HookBodyBlend(ConfigFile config)
		{
			BodyBlendPlugin.AfterBodyBlendLoaded += () =>
			{
				CreateBodyBlendConfig(config);
			};
		}

		// These are for setting up config (and with RiskOfOptions)
		public static void CreateBodyBlendConfig(ConfigFile config)
		{
			foreach(var item in DefaultPartInfluences)
			{
				CreatePartConfig(config, item.Key, item.Value);
			}
			foreach(var part in BodyBlendPlugin.GetBodyBlendParts())
			{
				if (PartInfluences.ContainsKey(part)) continue;
				CreatePartConfig(config, part, 0f);
			}
		}

		public static void CreatePartConfig(ConfigFile config, string part, float defaultInfluence)
		{
			// Make any adjustment to your config name/description here.
			var name = "Item Name";
			var influenceConfig = config.Bind<float>(
					"(BodyBlend) " + name,
					$"{part} Influence", defaultInfluence,
					$"Determine how much \"{part}\" will get influenced by {name}.\n" +
					$"Default: {defaultInfluence:0}"
				);
			ModSettingsManager.AddOption(
				new SliderOption(influenceConfig, new SliderConfig { min = 0f, max = 100f })
			);

			PartInfluences[part] = influenceConfig;
		}

		// Auto configuration done.

		// For Fetching the config values
		public static ICollection<string> GetParts()
		{
			return PartInfluences.Keys;
		}

		public static float GetInfluence(string part)
		{
			// Percentage -> Need to always be divided by 100.
			return PartInfluences[part].Value / 100f;
		}
	}

	// Extension methods for easier call to BodyBlend API.
	static class BodyBlendCompatibilityExt
	{

		public static void SetBlendValue(this CharacterBody body, string name, float value, string source)
		{
			var controller = body.GetBodyBlendController();
			if (controller)
				controller.SetBlendTargetWeight(name, value, source);
		}

		public static void RemoveBlend(this CharacterBody body, string name, string source)
		{
			var controller = body.GetBodyBlendController();
			if (controller)
				controller.RemoveBlendTargetWeight(name, source);
		}

		private static BodyBlendController GetBodyBlendController(this CharacterBody body)
		{
			return body.modelLocator.modelTransform.gameObject.GetComponent<BodyBlendController>();
		}
	}
}

Here is an example of how to use the configurations:

// Set values based on config
private static void SetBlendValues(CharacterBody body, float value, float mult, string token)
{
	foreach (var part in BodyBlendCompatibility.GetParts())
	{
		float influence = BodyBlendCompatibility.GetInfluence(part);
		if (influence > 0.0001f)
		{
			body.SetBlendValue(part, value * influence * mult, token);
		}
		else
		{
			body.RemoveBlend(part, token);
		}
	}
}