Implementation (of character creator) - lexlayer93/FaceSouls GitHub Wiki
In the first page, I described the mathematical fundamentals of the character creator. In this one, I will explain how FromSoftware has implemented these fundamentals, that is, what algorithm makes the character creator work based on FaceGen. In fact, there are two algorithms: the first is the one used in Demon's Souls and Dark Souls and was more faithful to FaceGen; the second, in Bloodborne, Dark Souls 3, and Elden Ring, introduced improvements but also new flaws. As for Dark Souls 2, although similar to the second one, it is a bit different, so I will not address it here.
Sliders
The only thing the two algorithms have in common are the sliders, almost always with the same labels and organized within almost identical categories. In truth, they are nothing more than aliases for the real FaceGen controls. Some examples:
FaceGen | Demon's Souls | Bloodborne |
---|---|---|
Face - brow-nose-chin ratio | General > Shape > Nose Size | Facial Balance > Nose Size |
Cheeks - concave / convex | Cheeks > Cheeks > Cheekbone Asperity | Cheeks > Cheekbone Protrusion |
To show the value of each slider, FromSoftware uses a single byte, which is why they move between 0 and 255. However, FaceGen sliders values are floating-point decimal numbers. Therefore, when the player chooses a value, the game needs to translate it:
SliderFloat = RangeMin + (RangeMax - RangeMin) * SliderByte / 255
This linear relationship can be easily checked in the debug menu of each game (after enabling it). For most sliders, the range is -10 to 10, except, for example, age, gender, and caricature, which have their own ranges. As explained in the fundamentals, all sliders are related. In the words of FaceGen: if slider X is lip thickness and slider Y is age, then decreasing the lip thickness has to affect the age value since lips get thinner as we age. Thus, by moving some sliders, we can make others go beyond the range imposed by the character creator, even if the UI does not show it.
First Algorithm: one at a time
It is applied in Demon's Souls and Dark Souls 1; it works like FaceGen Demo. The character creator only sets one slider at a time, but they all change automatically because they’re not independent. For example, when moving Chin Protrusion, other sliders also change, as can be seen in these screenshots:
After the player moves slider X from A to B and the corresponding floating-point values are calculated, the algorithm is:
- Update the face coefficient vector, depending on which slider X is:
- Age or Gender:
Face += AgeFactor * AgeVector + GndFactor * GndVector.
Given the inverse of the covariance matrix, the factors are:AgeFactor = CovInv[1,1]*(NewAge-OldAge) + CovInv[1,2]*(NewGnd-OldGnd)
GndFactor = CovInv[2,1]*(NewAge-OldAge) + CovInv[2,2]*(NewGnd-OldGnd)
- Caricature:
Face = MeanFace + NewCrc/OldCrc*(FaceDev - FaceDevAG) + FaceDevAG
. Where:FaceDev = Face - MeanFace
FaceDevAG = projectionAgeGnd(FaceDev)
.
- Other sliders:
Face += (NewValue-OldValue)*CtlVector
- Age or Gender:
- Update the values of every other slider, depending on which one it is:
- Age:
Age = AgeOffset + dotProduct(AgeVector, Face)
- Gender:
Gnd = GndOffset + dotProduct(GndVector, Face)
- Caricature:
Crc = norm(matrixProduct(GeoDensity, FaceDev - FaceDevAG))/sqrt(FaceDim-2)
- Other sliders:
Value = dotProduct(CtlVector, Face)
- Age:
- Update each mesh vertex from the coefficients of the new face:
For i from 1 to len(Vertices):
Vertices[i] = RefVertices[i] # Vertex positions for the reference face
For j from 1 to FaceDim:
Vertices[i] += DisplacementVector3D[i][j] * Face[j]
Once the player is satisfied with the face they have made, the game saves the 50 face coefficients, limited from -10 to +10, as bytes ranging from 0 to 255, called faceGeoData
in the game’s parameters, as follows:
faceGeoDataXY = int( (10 + Face[XY])*255/20 )
Since only the result is saved, any information about the creation process is lost, and that’s why it is very difficult to replicate a previously created face.
Second Algorithm: all at once
Used in Bloodborne, Dark Souls 3, and Elden Ring; it attempts to simplify character creation, allowing players to directly observe the value of each slider and trying to make the controls independent from each other. However, it’s all a facade, as the controls are only independent in appearance and the character creator does not actually manage to set the values the player wants. For example, take a look at this screenshot from the debug menu: on the left, in brackets, is the value chosen in the character creator; on the right, in parentheses, is the actual value achieved after applying the algorithm. In some cases, the result is correct; in others, it fails miserably.
In short, after the player moves slider X from A to B and the corresponding floating-point values are calculated, the new algorithm is:
-
Initialize the face coefficient vector to 0:
Face[i] = 0 for i from 1 to FaceDim
-
Start with the first slider that appears in the character creator.
-
Measure the current value of that slider:
- Age:
CurrentAge = AgeOffset + dotProduct(AgeVector, Face)
- Gender:
CurrentGnd = GndOffset + dotProduct(GndVector, Face)
- Caricature:
CurrentCrc = norm(matrixProduct(GeoDensity, FaceDev - FaceDevAG))/sqrt(FaceDim-2)
- Other sliders:
CurrentValue = dotProduct(CtlVector, Face)
- Age:
-
Update the face coefficient vector to the value of that slider chosen by the player:
- Age or Gender:
Face += AgeFactor * AgeVector + GndFactor * GndVector.
Given the inverse of the covariance matrix, the factors are:AgeFactor = CovInv[1,1]*(SliderAge-CurrentAge) + CovInv[1,2]*(SliderGnd-CurrentGnd)
GndFactor = CovInv[2,1]*(SliderAge-CurrentAge) + CovInv[2,2]*(SliderGnd-CurrentGnd)
- Caricature:
Face = MeanFace + SliderCrc/CurrentCrc*(FaceDev - FaceDevAG) + FaceDevAG
. Where:FaceDev = Face - MeanFace
FaceDevAG = projectionAgeGnd(FaceDev)
- Other sliders:
Face += (SliderValue - CurrentValue)*CtlVector
- Age or Gender:
-
Loop throuch each remaining slider until the last, as they appear in the character creator, updating the face coefficient vector each iteration. Beware, there are hidden sliders: some set to 0, others unused.
-
Check whether each coefficient is within range (+-50/7 ~ 7.14 in DS3). If not, clamp it.
-
Update each mesh vertex from the coefficients of the new face:
For i from 1 to len(Vertices): Vertices[i] = RefVertices[i] # Vertices positions for the reference face For j from 1 to FaceDim: Vertices[i] += DisplacementVector3D[i][j] * Face[j]
So, why does it get some sliders right but not others? Quite simple: the last sliders in the character creator will manage to set their value, but the values set by the first sliders will have changed during the process. In other words, order matters: unintentionally, the character creator is giving priority to the last ones (as Jaw Contour, the last slider of the character creator, number 33 in FaceGen). The advantage is that now, instead of saving face coefficients, the game will save the exact values of all the sliders, which will allow another player to create the same character by copying those values.