As I’m working on the morph targets, I also do the x3d extension, so they can be stored and accessed like everything else in CGE. No problems in coding here so far, however, I was thinking about the x3d nodes in the way that is:
- Fully aligned with CGE’s ways,
- Is fully aligned with x3d philosophy where things are named in a descriptive ways and independent of specific hardware or software.
I’m asking about it now, before I start implementing it, as it’s wiser than producing something “big” and then ask. Also, it’ll be nice to validate the auto-generation script before I proceed.
So, my proposal is:
-
The x3d node for morphs is named BlendShape, it contains the list of morphs and weights. The name BlendShape and BlendShapeTarget are descriptive and very commonly used, more than MorphTarget. The weights are per morph, so for X morphs equal X weights are needed. When x3d data is loaded (or glTF data), the list of weights will be truncated / extended with zeroes in case of a mismatch.
-
The node used for targets is named BlendShapeTarget.
-
“name” - optional field useful for debugging and editor. It can be “Smile”, “Frown”, etc.
-
“deltaCoords”, “deltaNormals”, “deltaTangents” - as the name says, it contains the difference as a Vector3F / Vector4F. The count of the deltas must strictly equal the count of vertices in the base model. That helps the GPU calculate stuff efficiently, and it’s expected by glTF. X3D doesn’t enforce it. I don’t intend to keep the “count” as a field in the x3d structure as it may happen that we load the morph list even before we load the model, which in turn means that we have the count anyway (Array.Length) and only need to validate the morph before we try to apply it to the model. The internal record TMorphTargetData holds the BaseVertexCount for speed though.
-
“bakeAtLoad” - which indicates if the morph is applied at load time, and then discarded.
It is useful when a morph (e.g. overall body shape such as muscular, tall,
stylized) affects the geometry but is not animated.
Although glTF does not support static morphs that are applied only
at load time, the proposed Castle X3D extension BlendShapeTarget does.
Every morph can be stored separately from the mesh and reused across models,
but static morphs can be discarded once the model is loaded,
because after that they do nothing.
This helps reduce memory usage and avoids keeping morph data that is
never needed at runtime. Every single morph needs a full list of deltas (at least coords) that includes every vertex. In Blender, for example, you could assign some of the vertices to a Vertex Group, and add a Shape Key (a.k.a. Morph) only to the group. When exported to glTF every vertex that isn’t in the group has assigned (0,0,0) value. It makes sense, it’s easier for GPU as there’s no need for conditional execution, but for detailed figures with dozens of morphs it really matters. Having the morphs a) split from meshes into separate morph files, and b) having some morphs discarded when not needed after load makes a big difference.
Having this field I consider more universal that having several separate models for every overall shape. It would let players customization of their avatar. Also it helps preventing situations where all NPCs look alike because of limited models. 1 model + many static morphs = multitude of options.
Further optimization would include separating head from body, also separate eyes, lashes and other stuff but I believe it depends more on programmers choice and expertise, and their choice of models, so I’m not going to touch that in my morph target implementation.
Initially I was thinking about a name for this field “OnlyOnLoad” but it’s very technical, and x3d standard wouldn’t be happy about it. The word “bake” is widely used and understood by artists, non-programmers, it’s aligned with programmers intuition and is consistent with 3d authoring tools.
I have decided to use internally (inside TAbstractGeometryNode) a structure named TMorphTargetData, because MorphTarget would be more intuitive from programming point (also because @michalis is using it
). That draws a clear distinction between the internal CGE-related structures from x3d.
The TMorphTargetData contains also HasNormals, HasTangents boolean fields determined by existence of the data in the source file. To the best of my knowledge the 2 are supported by glTF, however not always supported by others. I may be outdated but for Blender, Unity, Unreal, Maya and FBX tangents are always recomputed, while normals are optional. So I think having these Boolean fields is easier than always validating the corresponding arrays.
The main reason behind asking for approval is that the rest of my work on it depends on these steps to some extent, and your opinion matters. Most importantly, CGE has some influence on x3d standards, so maybe our implementation of morphs could get some attention and I’d love to hear it was accepted
The closer we get to possible real extension, the less work in the future too.
Here’s the “script to auto-generate the x3d nodes with x3d-to-pascal. It’s attached to CastleEngineExtensions.txt. I still have limited knowledge in this part of CGE, so I may be wrong with my definitions. Eg. do we need chEverything on “name” change.
BlendShapeTarget : X3DChildNode {
SFString [in,out] name ""
change: chEverything
doc: """
Optional name of this morph target. For example 'Smile'.
"""
MFVec3d [in,out] deltaCoords []
change: chEverything
doc: """
Required.
@italic(Each delta array (coordinates, normals, and tangents) must match the base mesh vertex count).
"""
MFVec3d [in,out] deltaNormals []
change: chEverything
doc: """
Optional.
"""
MFVec4d [in,out] deltaTangents []
change: chEverything
doc: """
Optional.
"""
SFBool [in,out] bakeAtLoad FALSE
change: chEverything
doc: """
It is useful when a morph (e.g. overall body shape such as muscular, tall, stylized) affects the geometry but is not animated.
Every morph can be stored separately from the mesh and reused across models, but @italic(static) morphs can be discarded once the model is loaded,
because after that they do nothing.
This helps reduce memory usage and avoids keeping morph data that is never needed at runtime.
It aligns well with 3d engines and modelling tools, where you can @italic(bake) morphs, actions, and deformations.
"""
}
BlendShape : X3DChildNode {
SFString [in,out] name ""
change: chEverything
doc: """
Optional name of this morph group. For example 'Facial_expressions'.
"""
MFNode [in,out] targets []
change: chEverything
range: BlendShapeTarget
doc: """
List of Blend Shape targets, also known as Morph Targets (glTF) and Shape Keys (Blender)
"""
MFFloat [in,out] weights []
doc: """
Morph target weights. Must have the same count as @italic(targets)targets.
"""
}