Skip to content

Commit eca2eab

Browse files
authored
Merge pull request #356 from GothicVRProject/feature/npc-breathing
NPC breathing and blinking
2 parents f9d9343 + 37666c9 commit eca2eab

20 files changed

+298
-158
lines changed

Assets/GothicVR-Lab/Scenes/Lab.unity

+1
Original file line numberDiff line numberDiff line change
@@ -4054,6 +4054,7 @@ MonoBehaviour:
40544054
vobTypeToSpawn:
40554055
createOcNpcs: 0
40564056
enableNpcRoutines: 0
4057+
enableNpcEyeBlinking: 1
40574058
npcToSpawn:
40584059
enableSounds: 0
40594060
enableMusic: 1

Assets/GothicVR-Lab/Scripts/Handler/LabNpcAnimationHandler.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public class LabNpcAnimationHandler : MonoBehaviour, ILabHandler
5454
(typeof(Wait), new(float0: 1)),
5555
(typeof(PlayAni), new(string0: "T_FOOD_RANDOM_1")),
5656
(typeof(Wait), new(float0: 1)),
57-
(typeof(PlayAni), new(string0: "T_FOOD_RANDOM_1")),
57+
(typeof(PlayAni), new(string0: "T_FOOD_RANDOM_2")),
5858
(typeof(Wait), new(float0: 1)),
5959
(typeof(LabUseItemToState), new(string0: "ItFoApple", int1: -1))
6060
}
@@ -66,7 +66,7 @@ public class LabNpcAnimationHandler : MonoBehaviour, ILabHandler
6666
(typeof(Wait), new(float0: 1)),
6767
(typeof(PlayAni), new(string0: "T_POTION_RANDOM_1")),
6868
(typeof(Wait), new(float0: 1)),
69-
(typeof(PlayAni), new(string0: "T_POTION_RANDOM_1")),
69+
(typeof(PlayAni), new(string0: "T_POTION_RANDOM_3")),
7070
(typeof(Wait), new(float0: 1)),
7171
(typeof(LabUseItemToState), new(string0: "ItFoBeer", int1: -1))
7272
}
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:bca9a700f3edc94a1347a9cb9b359e4e7692b71fff0f65081f7f43f43923e450
2+
oid sha256:b5911e740c14fc9c4ab47dd94bb9df0e499f0a7432241d44b2488093b48afd07
33
size 501760

Assets/GothicVR/Scenes/Bootstrap.unity

+1
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ MonoBehaviour:
345345
vobTypeToSpawn:
346346
createOcNpcs: 0
347347
enableNpcRoutines: 0
348+
enableNpcEyeBlinking: 0
348349
npcToSpawn:
349350
enableSounds: 1
350351
enableMusic: 1

Assets/GothicVR/Scripts/Creator/AnimationCreator.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,14 @@ public static void StopAnimation(GameObject go)
9292
animationComp.Stop();
9393
}
9494

95-
public static void PlayHeadMorphAnimation(NpcProperties props, HeadMorph.HeadMorphType type, bool loop)
95+
public static void PlayHeadMorphAnimation(NpcProperties props, HeadMorph.HeadMorphType type)
9696
{
97-
props.headMorph.StartAnimation(props.BodyData.Head, type, loop);
97+
props.headMorph.StartAnimation(props.BodyData.Head, type);
9898
}
9999

100-
public static void StopHeadMorphAnimation(NpcProperties props)
100+
public static void StopHeadMorphAnimation(NpcProperties props, HeadMorph.HeadMorphType type)
101101
{
102-
props.headMorph.StopAnimation();
102+
props.headMorph.StopAnimation(type);
103103
}
104104

105105
private static AnimationClip LoadAnimationClip(IModelAnimation pxAnimation, IModelHierarchy mdh, GameObject rootBone, bool repeat, string clipName)
@@ -111,7 +111,7 @@ private static AnimationClip LoadAnimationClip(IModelAnimation pxAnimation, IMod
111111
wrapMode = repeat ? WrapMode.Loop : WrapMode.Once
112112
};
113113

114-
var curves = new Dictionary<string, List<AnimationCurve>>((int)pxAnimation.NodeCount);
114+
var curves = new Dictionary<string, List<AnimationCurve>>(pxAnimation.NodeCount);
115115
var boneNames = pxAnimation.NodeIndices.Select(nodeIndex => mdh.Nodes[nodeIndex].Name).ToArray();
116116

117117
// Initialize array

Assets/GothicVR/Scripts/Creator/Meshes/V2/Builder/NpcHeadMeshBuilder.cs

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public override GameObject Build()
2323
// Cache it for faster use during runtime
2424
props.head = headGo.transform;
2525
props.headMorph = headGo.AddComponent<HeadMorph>();
26+
props.headMorph.HeadName = props.BodyData.Head;
2627

2728
var headMeshFilter = headGo.AddComponent<MeshFilter>();
2829
var headMeshRenderer = headGo.AddComponent<MeshRenderer>();

Assets/GothicVR/Scripts/Debugging/FeatureFlags.cs

+2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public enum SunMovementPerformance
6262
[Header("__________NPCs__________")]
6363
public bool createOcNpcs;
6464
public bool enableNpcRoutines;
65+
[Tooltip("NPCs blink way too long until now. ;-)")]
66+
public bool enableNpcEyeBlinking;
6567

6668
[Header("__________NPCs - Developer__________")]
6769
[Tooltip("Add the Daedalus ids for NPCs to spawn. Take them from C_NPC instances. (Ignored if empty; No monsters to be named as they always have id=0)")]

Assets/GothicVR/Scripts/Extensions/ZenKitExtension.cs

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics.CodeAnalysis;
4+
using GVR.Vm;
45
using UnityEngine;
56
using ZenKit;
67
using ZenKit.Daedalus;

Assets/GothicVR/Scripts/Misc/AbstractMorphAnimation.cs

-111
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using GVR.Caches;
5+
using GVR.Extensions;
6+
using JetBrains.Annotations;
7+
using UnityEngine;
8+
using ZenKit;
9+
using Mesh = UnityEngine.Mesh;
10+
using Random = UnityEngine.Random;
11+
12+
namespace GVR.Morph
13+
{
14+
/// <summary>
15+
///
16+
/// </summary>
17+
public abstract class AbstractMorphAnimation : MonoBehaviour
18+
{
19+
/// <summary>
20+
/// Multiple morphs can run at the same time. e.g. viseme and eyesblink.
21+
/// We therefore add animations to a list to calculate their positions together.
22+
/// </summary>
23+
private readonly List<MorphAnimationData> _runningMorphs = new();
24+
private Mesh _mesh;
25+
26+
/// <summary>
27+
/// RandomMorphs are blinking eyes in G1.
28+
/// The settings for it contains two options of blinking firstTime and secondTime.
29+
/// </summary>
30+
protected List<(string morphMeshName, string animationName, float firstTimeAverage, float firstTimeVariable, float secondTimeAverage, float secondTimeVariable, float probabilityOfFirst, float timer)> randomAnimations = new ();
31+
protected List<float> randomAnimationTimers = new(); // It's faster to alter a list entry than rewriting the whole Tuple struct above.
32+
33+
protected virtual void Start()
34+
{
35+
// As we don't set component inside Prefab, we need to assign mesh later at runtime.
36+
if (_mesh == null)
37+
_mesh = GetComponent<MeshFilter>().mesh;
38+
}
39+
40+
protected void StartAnimation(string morphMeshName, [CanBeNull] string animationName)
41+
{
42+
var newMorph = new MorphAnimationData();
43+
44+
newMorph.MeshMetadata = AssetCache.TryGetMmb(morphMeshName);
45+
newMorph.AnimationMetadata = animationName == null
46+
? newMorph.MeshMetadata.Animations.First()
47+
: newMorph.MeshMetadata.Animations.First(anim => anim.Name.EqualsIgnoreCase(animationName));
48+
newMorph.AnimationFrameData = MorphMeshCache.TryGetMorphData(morphMeshName, newMorph.AnimationMetadata.Name);
49+
50+
// Reset if already added and playing
51+
if (_runningMorphs.Any(i => i.MeshName == newMorph.MeshName))
52+
StopAnimation(newMorph.AnimationMetadata.Name);
53+
54+
var animationFlags = newMorph.AnimationMetadata.Flags;
55+
56+
// Not yet implemented warning.
57+
if (animationFlags.HasFlag(MorphAnimationFlags.Random)
58+
|| animationFlags.HasFlag(MorphAnimationFlags.Shape)
59+
|| animationFlags.HasFlag(MorphAnimationFlags.ShapeReference)
60+
)
61+
{
62+
Debug.LogWarning($"MorphMesh animation with flags {animationFlags} not yet supported!");
63+
}
64+
65+
// TODO/HINT - Is also handled via -1 second duration value of morphAnimation.Duration
66+
if (animationFlags.HasFlag(MorphAnimationFlags.Loop))
67+
newMorph.IsLooping = true;
68+
69+
_runningMorphs.Add(newMorph);
70+
}
71+
72+
public void StopAnimation(string animationName)
73+
{
74+
var morphToStop = _runningMorphs.FirstOrDefault(i => i.AnimationName.EqualsIgnoreCase(animationName));
75+
76+
if (morphToStop == null)
77+
{
78+
Debug.LogWarning($"MorphAnimation {animationName} not found on {gameObject.name}", gameObject);
79+
return;
80+
}
81+
82+
// Reset to a stable value.
83+
_mesh.vertices = MorphMeshCache.GetOriginalUnityVertices(morphToStop.MeshName);
84+
85+
_runningMorphs.Remove(morphToStop);
86+
}
87+
88+
private void Update()
89+
{
90+
UpdateRunningMorphs();
91+
CheckIfRandomAnimationShouldBePlayed();
92+
}
93+
94+
// FIXME - We currently calculate morphs every frame but could lower it's value with 60, 30, 15 frames in mind (e.g. for each distance culling).
95+
// FIXME - Means less CPU cycles to calculate morphs.
96+
private void UpdateRunningMorphs()
97+
{
98+
// ToList() -> As we might call .Remove() during looping, we need to clone the list to allow it (otherwise we get a CompilerIssue).
99+
foreach (var morph in _runningMorphs.ToList())
100+
{
101+
morph.Time += Time.deltaTime;
102+
103+
CalculateMorphWeights();
104+
105+
// IMorphAnimation.Speed is in milliseconds. We therefore multiply current time by 1000.
106+
var newFrameFloat = (morph.Time * 1000 * morph.AnimationMetadata.Speed);
107+
var newFrameInt = (int)newFrameFloat;
108+
109+
// We can't use animationtime as (e.g.) for R_EYESBLINK we have only one frame which is a time of 0.0f,
110+
// but instead we need to say frame 0 is 0...0.999 of first frame.
111+
if (newFrameInt >= morph.AnimationFrameCount)
112+
{
113+
// We just assume we're exactly at point 0.0f when we reached the end. Not 100% perfect but we're in milliseconds area of error.
114+
if (morph.IsLooping)
115+
{
116+
morph.Time = 0;
117+
newFrameFloat = 0.0f;
118+
newFrameInt = 0;
119+
}
120+
else
121+
{
122+
StopAnimation(morph.AnimationName);
123+
continue;
124+
}
125+
}
126+
127+
var currentMorph = morph.AnimationFrameData[newFrameInt];
128+
129+
// e.g. R_EYESBLINK
130+
if (morph.AnimationFrameCount == 1)
131+
{
132+
// FIXME - We need blendin/blendout otherwise this will be a on-off only.
133+
var calculatedMorph = new Vector3[currentMorph.Length];
134+
for (var i = 0; i < currentMorph.Length; i++)
135+
{
136+
calculatedMorph[i] = currentMorph[i];
137+
}
138+
_mesh.vertices = calculatedMorph;
139+
}
140+
else
141+
{
142+
var nextMorph =
143+
morph.AnimationFrameData[newFrameInt == morph.AnimationMetadata.FrameCount - 1 ? 0 : newFrameInt + 1];
144+
145+
var interpolatedMorph = new Vector3[currentMorph.Length];
146+
for (var i = 0; i < currentMorph.Length; i++)
147+
{
148+
interpolatedMorph[i] =
149+
Vector3.Lerp(currentMorph[i], nextMorph[i], newFrameFloat - MathF.Truncate(newFrameFloat));
150+
}
151+
_mesh.vertices = interpolatedMorph;
152+
}
153+
}
154+
}
155+
156+
private void CalculateMorphWeights()
157+
{
158+
// FIXME - Not yet implemented. But will provide smoother animations for e.g. viseme. Keep in mind there are blendIn and blendOut parameters.
159+
}
160+
161+
private void CheckIfRandomAnimationShouldBePlayed()
162+
{
163+
for (var i = 0; i < randomAnimations.Count; i++)
164+
{
165+
randomAnimationTimers[i] -= Time.deltaTime;
166+
167+
if (randomAnimationTimers[i] > 0)
168+
continue;
169+
170+
var anim = randomAnimations[i];
171+
172+
// FIXME - Set maxWeight for animation based on random value.
173+
// FIXME - var weight = Random.value * 0.4f + 0.6f
174+
StartAnimation(anim.morphMeshName, anim.animationName);
175+
176+
if (Random.value < anim.probabilityOfFirst)
177+
randomAnimationTimers[i] = (Random.value * 2 - 1) * anim.firstTimeVariable + anim.firstTimeAverage;
178+
else
179+
randomAnimationTimers[i] = (Random.value * 2 - 1) * anim.secondTimeVariable + anim.secondTimeAverage;
180+
}
181+
}
182+
}
183+
}

0 commit comments

Comments
 (0)