(UPDATED AS OF 4/23/19)
The goal of this tutorial is to provide a technical specification for how Rthro avatars are scaled, so that you can understand and predict it’s behavior when using custom rigs, and so you may create your own Rthro avatars that can be scaled down to classic character proportions.
Avatar Terminology
Rthro is a subset of Roblox’s R15 avatar that uses scales to make the character taller and skinnier. The changes in scale allow R15 to be divided into two sets of body types:
- Classic
- Rthro
Additionally, Rthro itself has two sets of body proportions:
- Normal
- Slender
All of these sets are blended using two NumberValues inside of the Humanoid, each whose values are constrained between [0 - 1]
BodyTypeScale | Blends the avatar's body type between Classic and Rthro. |
BodyProportionScale | Blends the avatar's Rthro proportions between Normal and Slender. |
local function getHumanoidScaleValue(humanoid, bodyValue, default)
local value = humanoid:FindFirstChild(bodyValue)
if value and value:IsA("NumberValue") then
return value.Value
else
return default
end
end
Avatar Part Scale Types
Parts that are in the character’s assembly (limbs, accessories, etc) can have a StringValue inside of them named AvatarPartScaleType
to define what proportions are used when the part is using its original scale. There are 2 valid values:
"ProportionsNormal"
"ProportionsSlender"
If the AvatarPartScaleType
value isn’t present, or its value isn’t one of the two listed above, then the part’s scale type is inferred to be for R15. Some accessories use a value of "Classic"
, although it isn’t required for it to be identified correctly.
local function getAvatarPartScaleType(part)
local scaleType = part:FindFirstChild("AvatarPartScaleType")
if scaleType and scaleType:IsA("StringValue") then
return scaleType.Value
else
return "Classic"
end
end
Avatar Scaling Tables
There are 4 constant scaling tables that are used to scale the body parts of the avatar. These scales are applied depending on what the scale type of each body part is. Before I show these tables as Lua, here is a data-set that describes each of the tables, and what configurations they are used by.
- The Part Scale Type column describes what
AvatarPartScaleType
StringValue is in use when this table is used for scaling. - The BodyType column describes what the
BodyTypeScale
value needs to be for the scales in the table to be applied 100% - The Proportion column describes what the
BodyProportionScale
value needs to be for the scales in the table to be applied 100%
Table Name | Description | Part Scale Type | BodyType | Proportion |
---|---|---|---|---|
R15_TO_RTHRO_NORMAL |
Scales the body parts of an R15 rig to an Rthro Normal rig. |
"Classic" |
1 | 0 |
R15_TO_RTHRO_SLENDER |
Scales the body parts of an R15 rig to an Rthro Slender rig. |
"Classic" |
1 | 1 |
RTHRO_NORMAL_TO_R15 |
Scales the body parts of an Rthro Normal rig to an R15 rig. |
"ProportionsNormal" |
0 | 0 |
RTHRO_SLENDER_TO_R15 |
Scales the body parts of an Rthro Slender rig to an R15 rig. |
"ProportionsSlender" |
0 | 0 |
-- Scale R15 -> Rthro Normal
local R15_TO_RTHRO_NORMAL =
{
Head = Vector3.new(0.942, 0.942, 0.942);
UpperTorso = Vector3.new(1.033, 1.310, 1.140);
LowerTorso = Vector3.new(1.033, 1.309, 1.140);
LeftUpperArm = Vector3.new(1.129, 1.342, 1.132);
LeftLowerArm = Vector3.new(1.129, 1.342, 1.132);
LeftHand = Vector3.new(1.066, 1.174, 1.231);
RightUpperArm = Vector3.new(1.129, 1.342, 1.132);
RightLowerArm = Vector3.new(1.129, 1.342, 1.132);
RightHand = Vector3.new(1.066, 1.174, 1.231);
LeftUpperLeg = Vector3.new(1.023, 1.506, 1.023);
LeftLowerLeg = Vector3.new(1.023, 1.506, 1.023);
LeftFoot = Vector3.new(1.079, 1.267, 1.129);
RightUpperLeg = Vector3.new(1.023, 1.506, 1.023);
RightLowerLeg = Vector3.new(1.023, 1.506, 1.023);
RightFoot = Vector3.new(1.079, 1.267, 1.129);
};
-- Scale R15 -> Rthro Slender
local R15_TO_RTHRO_SLENDER =
{
Head = Vector3.new(0.896, 0.942, 0.896);
UpperTorso = Vector3.new(0.905, 1.204, 1.013);
LowerTorso = Vector3.new(0.986, 1.004, 1.013);
LeftUpperArm = Vector3.new(1.004, 1.207, 1.006);
LeftLowerArm = Vector3.new(1.004, 1.207, 1.006);
LeftHand = Vector3.new(0.948, 1.174, 1.094);
RightUpperArm = Vector3.new(1.004, 1.208, 1.006);
RightLowerArm = Vector3.new(1.004, 1.208, 1.006);
RightHand = Vector3.new(0.948, 1.174, 1.094);
LeftUpperLeg = Vector3.new(0.976, 1.401, 0.909);
LeftLowerLeg = Vector3.new(0.976, 1.301, 0.909);
LeftFoot = Vector3.new(1.030, 1.133, 1.004);
RightUpperLeg = Vector3.new(0.976, 1.401, 0.909);
RightLowerLeg = Vector3.new(0.976, 1.301, 0.909);
RightFoot = Vector3.new(1.030, 1.133, 1.004);
};
-- Scale Rthro Normal -> R15
local RTHRO_NORMAL_TO_R15 =
{
Head = Vector3.new(1.600, 1.600, 1.600);
UpperTorso = Vector3.new(1.014, 0.814, 0.924);
LowerTorso = Vector3.new(1.014, 0.814, 0.924);
LeftUpperArm = Vector3.new(1.121, 0.681, 0.968);
LeftLowerArm = Vector3.new(1.121, 0.681, 0.968);
LeftHand = Vector3.new(1.390, 0.967, 1.201);
RightUpperArm = Vector3.new(1.121, 0.681, 0.968);
RightLowerArm = Vector3.new(1.121, 0.681, 0.968);
RightHand = Vector3.new(1.390, 0.967, 1.201);
LeftUpperLeg = Vector3.new(0.978, 0.814, 1.056);
LeftLowerLeg = Vector3.new(0.978, 0.814, 1.056);
LeftFoot = Vector3.new(1.404, 0.953, 0.931);
RightUpperLeg = Vector3.new(0.978, 0.814, 1.056);
RightLowerLeg = Vector3.new(0.978, 0.814, 1.056);
RightFoot = Vector3.new(1.404, 0.953, 0.931);
};
-- Scale Rthro Slender -> R15
local RTHRO_SLENDER_TO_R15 =
{
Head = Vector3.new(1.600, 1.600, 1.600);
UpperTorso = Vector3.new(1.156, 0.885, 1.039);
LowerTorso = Vector3.new(1.063, 1.061, 1.040);
LeftUpperArm = Vector3.new(1.261, 0.756, 1.089);
LeftLowerArm = Vector3.new(1.261, 0.756, 1.089);
LeftHand = Vector3.new(1.564, 0.967, 1.351);
RightUpperArm = Vector3.new(1.261, 0.756, 1.089);
RightLowerArm = Vector3.new(1.261, 0.756, 1.089);
RightHand = Vector3.new(1.564, 0.967, 1.351);
LeftUpperLeg = Vector3.new(1.025, 0.875, 1.188);
LeftLowerLeg = Vector3.new(1.025, 0.943, 1.188);
LeftFoot = Vector3.new(1.471, 1.065, 1.047);
RightUpperLeg = Vector3.new(1.025, 0.875, 1.188);
RightLowerLeg = Vector3.new(1.025, 0.943, 1.188);
RightFoot = Vector3.new(1.471, 1.065, 1.047);
};
Keeping a Record of Original Values
When a Humanoid determines that a part is attached to the avatar, it first looks for a Vector3Value inside of the part called OriginalSize
. If this value isn’t found, it is created using the current size of the part as its value. Its important to make sure this value is accurate and kept around, otherwise you may end up unintentionally adding additional scaling to the avatar that you did not intend.
Additionally, parts that are attached to the avatar will have their children scanned for Attachments. Similarly to parts, Attachments will receive a Vector3Value named OriginalPosition
, if it doesn’t have one already. This is used to scale the positional offset of the attachment relative to the scaling that is being done to the part.
local function getOriginalVector3(object, label, default)
local original
for _,child in pairs(object:GetChildren()) do
if child:IsA("Vector3Value") and child.Name == label then
original = child
break
end
end
if not original then
original = Instance.new("Vector3Value")
original.Name = label
original.Value = default
original.Parent = object
end
return original
end
local function getOriginalSize(part)
local size = part.Size
return getOriginalVector3(part, "OriginalSize", size)
end
local function getOriginalPosition(attachment)
local position = attachment.CFrame.Position
return getOriginalVector3(attachment, "OriginalPosition", position)
end
Sampling the Avatar Scaling Tables and Scaling
When a Humanoid determines that it needs to be rescaled, each body part of the avatar samples the 4 scaling tables. From there, it needs to select the scales that it will use for R15, Rthro Normal, and Rthro Slender, based on what its scale type is.
This sampling can be described through the following Lua function:
local function sampleScaleTables(humanoid, bodyPart, scaleType)
-- Make sure this is a valid R15 body part.
local partType = humanoid:GetBodyPartR15(bodyPart)
if partType == Enum.BodyPartR15.Unknown then
error("Unknown body part received in sampleScaleTables!")
end
-- Determine the scale type for this body part.
if not scaleType then
scaleType = getAvatarPartScaleType(bodyPart)
end
-- Sample the scaling tables
local limbName = bodyPart.Name
local sampleNoChange = Vector3.new(1, 1, 1)
local sampleR15ToNormal = R15_TO_RTHRO_NORMAL[limbName]
local sampleNormalToR15 = RTHRO_NORMAL_TO_R15[limbName]
local sampleR15ToSlender = R15_TO_RTHRO_SLENDER[limbName]
local sampleSlenderToR15 = RTHRO_SLENDER_TO_R15[limbName]
local sampleNormalToSlender = (sampleR15ToNormal / sampleR15ToSlender)
local sampleSlenderToNormal = (sampleR15ToSlender / sampleR15ToNormal)
-- Select the scales that will be interpolated
local scaleR15, scaleNormal, scaleSlender
if scaleType == "ProportionsNormal" then
scaleR15 = sampleNormalToR15
scaleNormal = sampleNoChange
scaleSlender = sampleNormalToSlender
elseif scaleType == "ProportionsSlender" then
scaleR15 = sampleSlenderToR15
scaleNormal = sampleSlenderToNormal
scaleSlender = sampleNoChange
else
scaleR15 = sampleNoChange
scaleNormal = sampleR15ToNormal
scaleSlender = sampleR15ToSlender
end
return scaleR15, scaleNormal, scaleSlender
end
Using the samples returned from this function, the body part then computes its effective rthro scale using the Body
NumberValues inside of the Humanoid.
local function computeLimbScale(humanoid, bodyPart)
-- If this isn't a valid R15 body part, don't scale it.
local partType = humanoid:GetBodyPartR15(bodyPart)
if partType == Enum.BodyPartR15.Unknown then
return Vector3.new(1, 1, 1)
end
-- Determine the scale type for this body part.
local scaleType = getAvatarPartScaleType(bodyPart)
-- Select the scales we will interpolate
local scaleR15, scaleNormal, scaleSlender = sampleScaleTables(humanoid, bodyPart)
-- Compute the Rthro scaling based on the current proportions and body-type.
local bodyType = getHumanoidScaleValue(humanoid, "BodyTypeScale", 0)
local proportions = getHumanoidScaleValue(humanoid, "BodyProportionScale", 0)
local scaleProportions = scaleNormal:Lerp(scaleSlender, proportions)
local scaleBodyType = scaleR15:Lerp(scaleProportions, bodyType)
-- Handle the rest of the scale values.
if bodyPart.Name == "Head" then
local headScale = getHumanoidScaleValue(humanoid, "HeadScale", 1)
return scaleBodyType * headScale
else
local bodyWidth = getHumanoidScaleValue(humanoid, "BodyWidthScale", 1)
local bodyDepth = getHumanoidScaleValue(humanoid, "BodyDepthScale", 1)
local bodyHeight = getHumanoidScaleValue(humanoid, "BodyHeightScale", 1)
local baseScale = Vector3.new(bodyWidth, bodyHeight, bodyDepth)
return scaleBodyType * baseScale
end
end
Accessory Scaling
Scaling accessories is a bit of a special case, because its possible to have accessories made for one avatar type being applied to a limb that is using a different avatar type.
For example, you may be wearing an Rthro head while using R15 proportions, while wearing an R15 hat. In this circumstance, the Rthro head would be scaled up by 60%.
Mirroring the scale of the head to the hats would be undesirable for hats that were already scaled to work with R15, so the goal in this circumstance is to
- Get the scale that the accessory will have when its attachment parent is at its original scale.
- Scale that scale by the current scale of the body part to get the effective scale of the accessory.
This can be done by sampling the scale of the body part for both the body part’s scale type, and the handle’s scale type. Dividing the handle samples by the body part samples will do the trick properly.
local function computeAccessoryScale(humanoid, limb, accessory)
local handle = accessory:FindFirstChild("Handle")
if not (handle and handle:IsA("BasePart")) then
return Vector3.new(1, 1, 1)
end
local limbScaleType = getAvatarPartScaleType(limb)
local handleScaleType = getAvatarPartScaleType(handle)
local limbScale = computeLimbScale(humanoid, limb)
if (limbScaleType == handleScaleType) then
return limbScale
end
local limbR15, limbNormal, limbSlender = sampleScaleTables(humanoid, limb, limbScaleType)
local handleR15, handleNormal, handleSlender = sampleScaleTables(humanoid, limb, handleScaleType)
local originScale
if handleScaleType == "ProportionsNormal" then
originScale = handleNormal / limbNormal
elseif handleScaleType == "ProportionsSlender" then
originScale = handleSlender / limbSlender
else
originScale = handleR15 / limbR15
end
return originScale * limbScale;
end
Conclusion
Now you should hopefully have a general understanding of how the Rthro scaling system works. If you have any questions, feel free to post them below. Hope this was informative!