Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Issue with Imported vs Calculated normals on a character model

Discussion in 'Editor & General Support' started by dgoyette, Aug 16, 2023.

  1. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,113
    Short version: I want the character's outfit to use Imported normals, but their body parts to use Calculated normals.

    Long version: When importing an FBX, there's a single option for choosing how to handle normals for the whole model. You can choose "Calculate", to let Unity decide, or "Import" to use whatever is specified in the FBX:

    upload_2023-8-16_10-39-56.png

    The issue I'm having with this is that ideally I'd use a different approach on different Skinned Mesh Renderers in the model. Specifically, the character's outfit really benefits from using the imported normals (to get all the hard edges set in the 3D modelling tool), while the character's face benefits from Calculated normals (otherwise facial animations tend to look wrong).

    I'm trying to understand if there's a simple option to having various SMRs in the model use one approach to normals, while other SMRs in the model use the other.

    I see that Mesh has a RecalculateNormals method, but it's not clear to me whether calling RecalculateNormals once is effectively the same as setting the model's normals to "Calculate". It doesn't seem feasible to call RecalculateNormals every frame.

    And to explain why I feel I need to do this:

    Character outfits are often modelled with "hard" surfaces, where you'd want to preserve the sharp normals, such as a suit of armor. If you smooth those normals, it looks really weird. But the character's face needs to continuously be smoothed, otherwise when blend shapes deform the model, it seems to keep using the imported normal for the vertices, which is no longer the actual normal of that vertex. For example, here's the same character side-by-side, where the left uses Calculate, and the right uses Import. With all blend shapes zero'd out, they look the same:

    upload_2023-8-16_11-0-29.png

    But if I adjust the blend shapes, you can see that the model using "Calculate" has reasonable normals, while the model using "Import" has completely messed up normals:

    upload_2023-8-16_10-58-56.png

    I'm trying to get the character's face to always use the approach on the left, without losing the hard edges on the character's outfit.

    Has anyone dealt with this?
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    Why not just make the clothes and head seperate models, and thus, can have separate normals? That's always been my approach.
     
  3. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,113
    I don't think that's possible with a rigged character. The outfit needs to deform as the character's skeleton deforms. Unless I'm missing something? Given that all of the meshes need to be children of the armature, I believe they all need to be in the same FBX, and therefore they all use the same FBX importer settings.
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    Now that I cast my mind back, I did this by exporting the clothes, armour, etc, all separately, and used some code to 're-rig' the skinned mesh renderer to the rig I exported separately.

    Here's the code that I cobbled together from some forum posts:
    Code (CSharp):
    1. using UnityEngine;
    2. using Sirenix.OdinInspector;
    3.  
    4. [AddComponentMenu("LBG/Rigging/Bone Setter")]
    5. public sealed class BoneSetter : MonoBehaviour
    6. {
    7.     [SerializeField]
    8.     private Transform skeletonRoot;
    9.  
    10.     [SerializeField]
    11.     private string[] boneNames;
    12.  
    13.     [SerializeField]
    14.     private Transform[] bones;
    15.  
    16.     [Button("Get Bone Names", ButtonSizes.Medium)]
    17.     [ContextMenu("GetBoneNames()")]
    18.     public void GetBoneNames()
    19.     {
    20.         bool hasSkinnedMeshRenderer = this.TryGetComponent(out SkinnedMeshRenderer skinnedMeshRenderer);
    21.  
    22.         if (!hasSkinnedMeshRenderer)
    23.         {
    24.             Debug.LogWarning("Game Object has no Skinned Mesh Renderer Component.", this);
    25.             return;
    26.         }
    27.  
    28.         Transform[] bones = skinnedMeshRenderer.bones;
    29.  
    30.         boneNames = new string[bones.Length];
    31.  
    32.         for (int i = 0; i < bones.Length; i++)
    33.         {
    34.             boneNames[i] = bones[i].name;
    35.         }
    36.     }
    37.  
    38.     [Button("Set Bones", ButtonSizes.Medium)]
    39.     [ContextMenu("SetBones()")]
    40.     public void SetBones()
    41.     {
    42.         if (skeletonRoot == null)
    43.         {
    44.             Debug.LogWarning("Root Transform is not set!");
    45.             return;
    46.         }
    47.  
    48.         bones = new Transform[boneNames.Length];
    49.  
    50.         for (int i = 0; i < boneNames.Length; i++)
    51.         {
    52.             bones[i] = skeletonRoot.FindInChildren(boneNames[i]);
    53.         }
    54.  
    55.         SkinnedMeshRenderer sRenderer = GetComponent<SkinnedMeshRenderer>();
    56.  
    57.         sRenderer.bones = bones;
    58.  
    59.         sRenderer.rootBone = skeletonRoot;
    60.     }
    61. }
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public static class RigSkinningSetting
    4. {
    5.     public static Transform FindInChildren(this Transform transform, string name)
    6.     {
    7.         return FindInChildrenInternal(transform, name);
    8.     }
    9.  
    10.     private static Transform FindInChildrenInternal(Transform transform, string name)
    11.     {
    12.         if (transform.name == name)
    13.         {
    14.             return transform;
    15.         }
    16.         else
    17.         {
    18.             Transform found;
    19.  
    20.             for (int i = 0; i < transform.childCount; i++)
    21.             {
    22.                 found = FindInChildrenInternal(transform.GetChild(i), name);
    23.                 if (found != null)
    24.                 {
    25.                     return found;
    26.                 }
    27.             }
    28.  
    29.             return null;
    30.         }
    31.     }
    32. }
    Obviously it doesn't need to be a component, I just wrote this a long time ago before I was really aware of Editor scripting. I would make an Editor Window if I were to tackle this kind of problem again in the future.
     
  5. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,113
    That's a really interesting approach. I'll give it a try. I wasn't at all aware you could replace an SMR's rig with another one.
     
    spiney199 likes this.
  6. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,113
    I was pointed to a different solution, which seems a fair bit simpler, and seems to work as hoped.

    It involves leaving normals set to "Import", but enabling "Legacy Blend Shape Normals".

    upload_2023-8-18_11-45-16.png

    The result is that Unity will use Imported normals for all meshes, unless the mesh has Blend Shapes, in which case it will calculate the normals for that mesh. This seems to be what I'm looking for.

    It does concern me somewhat that this is considered a "Legacy" feature, and I wonder if it's going to be deprecated, of if it has some failings I'm not aware of. It's also not clear to me why using "Import" for normals, but "Calculate" for "Blend Shape Normals" doesn't produce the same result. But it seems like under that configuration, the blend shape normals are getting layered onto the imported normals for the face, whereas under the Legacy mode the normals are completely recalculated. Maybe.
     
    Last edited: Aug 18, 2023