ã¯ããã«
å æ¥ãuLipSync ã®ã¿ã¤ã ã©ã¤ã³ã®æ©è½ã§é·ããã¼ã¿ãå²ãå½ã¦ãã¨ããã©ã¼ãã³ã¹ãä½ä¸ãããã¨ã®ã³ã¡ã³ããé ãã¾ãããå®éã« 3 åç¨ã®ã¯ãªãããå²ãå½ã¦ã¦ã¿ãã¨é¡èã«ããã©ã¼ãã³ã¹ãè½ã¡ã¦ãã¾ãã
åå ã調ã¹ã¦ã¿ãã¨ãã©ããã波形æç»ã«ç¨ãã¦ãã AudioCurveRendering
ã®å®è¡ãéãããã§ããããã¯ã¢ã³ãã¨ã¤ãªã¢ã¹ã¤ãã®æ³¢å½¢æç»ãè¡ã£ã¦ããããã®ãªã®ã§ããããã¯ã¹ãã£ãè¿ã API ã§ã¯ãªããæå®ããé åï¼Rect
ï¼ã«æ³¢å½¢ç»åãæç»ãããã®ã«ãªãã¾ãããã®ãããã£ãã·ã¥ãã§ãããæç»æ´æ°ãèµ·ããã¿ã¤ãã³ã°ã§æ¯åçæå¦çãèµ°ã£ã¦ãã¾ãã¾ãã
ããã§ãä»åã¯ãã®ãã¯ã¹ãã£ãèªåã§çæãããã¨ã§å¦çã®æ¹åã試ã¿ã¦ã¿ã¾ãããæç´ãª Texture2D.SetPixels()
ã使ãæ¹å¼ãã Texture2D.GetPixelData()
ã®å©ç¨ããã㦠Job + Burst åã¾ã§è¡ãã¾ããæ¬ã¨ã³ããªã¯è§£èª¬ã¨ããããã¯ä½æ¥ã®åå¿é²ã«ãªãã¾ãã
ãã¦ã³ãã¼ã
åé¡ç¹
以åã¯ãããªæã㧠Timeline ã®ã¨ãã£ã¿æç»ã®æ´æ°ããããã³ã« AudioUtil.GetMinMaxData()
ããã³ AudioCurveRendering.DrawMinMaxFilledCurve()
ãå¼ã°ãã¦ãã¾ããã
CustomTimelineEditor(typeof(uLipSyncClip))] public class uLipSyncClipTimelineEditor : ClipEditor { ... public override void DrawBackground(TimelineClip clip, ClipBackgroundRegion region) { var ls = clip.asset as uLipSyncClip; var data = ls.bakedData; ... EditorUtil.DrawWave(rect, data.audioClip, new EditorUtil.DrawWaveOption() { // ... è²ãä»ããå¦ç }); } } public static class EditorUtil { ... public class DrawWaveOption { public System.Func<float, Color> colorFunc; public float waveScale; } public static void DrawWave(Rect rect, AudioClip clip, DrawWaveOption option) { ... // 波形åå¾ var minMaxData = AudioUtil.GetMinMaxData(clip); ... AudioCurveRendering.AudioMinMaxCurveAndColorEvaluator dlg = delegate( float x, out Color col, out float minValue, out float maxValue) { col = option.colorFunc(x); ... minValue = ...; maxValue = ...; ... }; // æç»å¦ç AudioCurveRendering.DrawMinMaxFilledCurve(rect, dlg); } }
æç»ã®æ´æ°ã¯ä¾ãã° Timeline åçä¸ãªã©ã¯æ¯ãã¬ã¼ã è¡ããã¦ãã¾ããã¾ãå
é¨ã§ã¯ uLipSync.BakedData
ãåç
§ãã¦ããããã®æéã«ã©ã®ãããªé³ç´ ããããã«ãã£ã¦è²ä»ãããã¦ãã¾ãããããçµæ§éãã3 åç¨åº¦ã®æã®ãããªãã¼ã¿ãæç»ãã¦ããå ´åã¯ç§ã®ç°å¢ã§ã¯ 20 fps ç¨åº¦ã¾ã§ããã©ã¼ãã³ã¹ãä½ä¸ãã¦ãã¾ã£ã¦ãã¾ããã
ãã®ããã« Timeline ãªã©æ¯ãã¬ã¼ã æ´æ°ãããå ´æã«è¡¨ç¤ºããé ç®ã®è¨ç®ãéãå ´åã¯ä»ã«ãè²ã ããããã§ããã
対å¦æ³
以åãTimeline ã®è£ 飾ã®è¨äºã§ãæ¸ããã®ã§ãããçæãããã¯ã¹ãã£ããã£ãã·ã¥ãããã¨ã§ããã©ã¼ãã³ã¹ãæ¹åãããã¨ãã§ãã¾ãã
è¦ã¯å ç¨ã¯æ¯åãã¯ã¹ãã£çæå¦çãèµ°ã£ã¦ãã¾ã£ã¦ããã®ã§ããããããå¤æ´ããªãéã使ãåããã¨ãããã®ã§ããã¿ã¤ã ã©ã¤ã³ã®ãºã¼ã çãå¤ãã£ã¦æ¨ªå¹ ãå¤ãã£ããããã¼ã¿ãå¤æ´ããã¦æ³¢å½¢ãå¤ããã¿ã¤ãã³ã°ã§ãã¯ã¹ãã£ãåçæããããã«ãã¦ããã¾ãã
ã©ã³ã¿ã¤ã ã§ããããããã使ãæ©ä¼ããããããããªãã¨æããTexture2D
ã®çæé¢æ°ã BakedData
ã«æ¬¡ã®ããã«è¿½å ãã¦ã¿ã¾ããã
[System.Serializable] public struct BakedFrame { public float volume; public List<BakedPhonemeRatio> phonemes; ... } ... public class BakedData : ScriptableObject { public BakedFrame GetFrame(float t) { ... } ... public static Color[] phonemeColors = new Color[] { Color.red, Color.cyan, Color.yellow, Color.magenta, Color.green, Color.blue, Color.gray, }; public Texture2D CreateTexture(int width, int height) { ... var colors = new Color[width * height]; var currentColor = new Color(); var smooth = 0.15f; for (int x = 0; x < width; ++x) { var t = (float)x / width * duration; var frame = GetFrame(t); var targetColor = new Color(); for (int i = 0; i < frame.phonemes.Count; ++i) { var colorIndex = i % phonemeColors.Length; targetColor += phonemeColors[colorIndex] * frame.phonemes[i].ratio; } currentColor += (targetColor - currentColor) * smooth; for (int y = 0; y < height; ++y) { var index = width * y + x; var color = currentColor; var dy = ((float)y - height / 2f) / (height / 2f); dy = Mathf.Abs(dy); dy = Mathf.Pow(dy, 2f); color.a = dy > frame.volume ? 0f : 1f; colors[index] = color; } } var tex = new Texture2D(width, height); tex.SetPixels(colors); tex.Apply(); return tex; } }
ã¾ãã¯ãã¯ã¹ãã£çæã¯æç´ãª Color[]
é
åã Texture2D.SetPixels()
ããæ¹å¼ã§æ¸ãã¦ã¿ã¾ããããã¯ã¹ãã£ã¯é³éã§é©å½ã«ä¸ä¸ã大ãããã¦é³ç´ ã§è²ä»ãããã³ã¼ããæ¸ãã¦ããã¾ãã
ãã®çæãããã¯ã¹ãã£ããã£ãã·ã¥ããªãã表示ããå ´æ㯠ClipEditor
å´ã§è¡ãã¾ããå°ãé·ãã§ãã次ã®ããã«ååãã¯ãªãããå¤åããã¨ãããºã¼ã çãªã©ã§æ¨ªå¹
ã縦å¹
ã大ããå¤åããã¨ãã«ãã¯ã¹ãã£ãåçæããããã«ãã¦ãã¾ãã
[CustomTimelineEditor(typeof(uLipSyncClip))] public class uLipSyncClipTimelineEditor : ClipEditor { internal class TextureCache { public Texture2D texture; public bool forceUpdate = false; } Dictionary<uLipSyncClip, TextureCache> _textures = new Dictionary<uLipSyncClip, TextureCache>(); void RemoveCachedTexture(uLipSyncClip clip) { if (!_textures.ContainsKey(clip)) return; var cache = _textures[clip]; Object.DestroyImmediate(cache.texture); _textures.Remove(clip); } Texture2D CreateCachedTexture( uLipSyncClip clip, int width, int height) { RemoveCachedTexture(clip); var data = clip.bakedData; if (!data) return null; width = Mathf.Clamp(width, 128, 4096); var tex = data.CreateTexture(width, height); var cache = new TextureCache { texture = tex }; _textures.Add(clip, cache); return tex; } Texture2D GetOrCreateCachedTexture( uLipSyncClip clip, int width, int height) { if (!_textures.ContainsKey(clip)) { return CreateCachedTexture(clip, width, height); } var cache = _textures[clip]; if (cache.forceUpdate) { return CreateCachedTexture(clip, width, height); } var dw = Mathf.Abs(cache.texture.width - width); var dh = Mathf.Abs(cache.texture.height - height); if (dw > 10 || dh > 10) { return CreateCachedTexture(clip, width, height); } return cache.texture; } public override void DrawBackground( TimelineClip clip, ClipBackgroundRegion region) { DrawBackground(region); DrawWave(clip, region); } void DrawBackground(ClipBackgroundRegion region) { EditorUtil.DrawBackgroundRect( region.position, new Color(0f, 0f, 0f, 0.3f), Color.clear); } void DrawWave( TimelineClip timelineClip, ClipBackgroundRegion region) { var clip = timelineClip.asset as uLipSyncClip; var data = clip.bakedData; if (!data) return; var audioClip = data.audioClip; if (!audioClip) return; var rect = region.position; var duration = region.endTime - region.startTime; var width = (float)(rect.width * audioClip.length / duration); var left = Mathf.Max( (float)timelineClip.clipIn, (float)region.startTime); var offset = (float)(width * left / audioClip.length); rect.x -= offset; rect.width = width; var tex = GetOrCreateCachedTexture( clip, (int)rect.width, (int)rect.height); if (!tex) return; GUI.DrawTexture(rect, tex); } public override void OnClipChanged(TimelineClip timelineClip) { var clip = timelineClip.asset as uLipSyncClip; if (!_textures.ContainsKey(clip)) return; _textures[clip].forceUpdate = true; } }
ããã§è¦ãç®ãããããã«ããã©ã¼ãã³ã¹æ¹åãããã¨ãã§ãã¾ããã
Texture2D.GetPixelData()
æ¹å¼
Color[]
ã«è©°ã㦠SetPixels()
ããã®ããªã¼ãã¼ããããããã®ã§ãTexture2D.GetPixelData()
ã使ã£ã¦ã¿ã¾ããããã¯ç´æ¥ã«ã©ã¼ãããã¡ãæ¸ãæãå¯è½ãªã NativeArray
ãè¿ãã¦ããã便å©é¢æ°ã§ãã
以ä¸ã®ããã«æ¸ãã°å¤ã Unity ã§ã¯ SetPixels()
ãã対å¿ãã¦ãããã¼ã¸ã§ã³ã§ã¯ Texture2D.GetPixelData()
ã使ãããã«ã§ãã¾ãã
public Texture2D CreateTexture(int width, int height) { ... var tex = new Texture2D(width, height); #if UNITY_2020_1_OR_NEWER var colors = tex.GetPixelData<Color32>(0); #else var colors = new Color32[width * height]; #endif ... for (int x = 0; x < width; ++x) { ... colors[index] = color; ... } #if !UNITY_2020_1_OR_NEWER tex.SetPixels(colors); #endif tex.Apply(); return tex; }
ã¸ã§ãå
ãã® Texture2D.GetPixelData()
ã¨ã¸ã§ããçµã¿åãããå
¬å¼ãµã³ãã«ã以ä¸ã«å
¬éããã¦ãã¾ãã
ãããå ã« Burst + Job åããã¦ã¿ã¾ããããã¡ãã£ã¨ uLipSync ç¹æã³ã¼ããå¤ããã¾ãåèã«ãªããªãããããã¾ããã...ã
using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; ... [BurstCompile] ... public class BakedData : ScriptableObject { ... [BurstCompile] struct CreateTextureJob : IJob { [WriteOnly] public NativeArray<Color32> texColors; [DeallocateOnJobCompletion][ReadOnly] public NativeArray<Color> phonemeColors; [DeallocateOnJobCompletion][ReadOnly] public NativeArray<float> phonemeRatios; [DeallocateOnJobCompletion][ReadOnly] public NativeArray<float> volumes; [ReadOnly] public int width; [ReadOnly] public int height; [ReadOnly] public int phonemeCount; [ReadOnly] public float smooth; public void Execute() { var currentColor = new Color(); for (int x = 0; x < width; ++x) { var targetColor = new Color(); for (int i = 0; i < phonemeCount; ++i) { var colorIndex = i % phonemeColors.Length; var ratioIndex = x * phonemeCount + i; var color = phonemeColors[colorIndex]; float ratio = phonemeRatios[ratioIndex]; targetColor += color * ratio; } currentColor += (targetColor - currentColor) * smooth; for (int y = 0; y < height; ++y) { var index = width * y + x; var color = currentColor; var dy = ((float)y - height / 2f) / (height / 2f); dy = math.abs(dy); dy = math.pow(dy, 2f); color.a = dy > volumes[x] ? 0f : 1f; texColors[index] = color; } } } } public Texture2D CreateTexture(int width, int height) { if (!isValid) return Texture2D.whiteTexture; var tex = new Texture2D(width, height); var texColors = tex.GetPixelData<Color32>(0); var phonemeColorsTmp = new NativeArray<Color>( phonemeColors, Allocator.TempJob); int phonemeCount = frames[0].phonemes.Count; var phonemeRatiosTmp = new NativeArray<float>( width * phonemeCount, Allocator.TempJob); var volumesTmp = new NativeArray<float>( width, Allocator.TempJob); for (int x = 0; x < width; ++x) { var t = (float)x / width * duration; var frame = GetFrame(t); for (int i = 0; i < phonemeCount; ++i) { int index = x * phonemeCount + i; phonemeRatiosTmp[index] = frame.phonemes[i].ratio; } volumesTmp[x] = frame.volume; } var job = new CreateTextureJob() { texColors = texColors, phonemeColors = phonemeColorsTmp, phonemeRatios = phonemeRatiosTmp, volumes = volumesTmp, width = width, height = height, phonemeCount = phonemeCount, smooth = 0.15f, }; job.Schedule().Complete(); tex.Apply(); return tex; } }
次ã®ãããªæé 㧠Job + Burst åãã¾ãã
- é常ã®é
åã¯
NativeArray
ã«å¤æ - å
¥åã¨ãã¦å¿
è¦ãªãã¼ã¿ã¯äºåã«å¥é
NativeArray
ã«è©°ãã¦ãã - 2 次å
é
åã¯é·ããã 1 次å
NativeArray
ã«ãã¦ãã Allocator.TempJob
ã¨DeallocateOnJobCompletion
ã§ç¢ºä¿ããã¡ã¢ãªã¯èªå解æ¾
æ¯è¼çãæ軽ã«é«éåãã§ãã¾ãããã¾ããä»åã®ã±ã¼ã¹ã§ã¯è²ã®ã¹ã ã¼ã¸ã³ã°ã®ããåã®è²ãå©ç¨ããå½¢ã«ãªã£ã¦ããé¢ä¿ã§ãIJobParallelFor
ã«ã¯ãã¦ãã¾ããã
ãããã«
ã©ã³ã¿ã¤ã ã®ããã©ã¼ãã³ã¹ã°ããã«æ°ãé ã£ã¦ãã¾ããããã¨ãã£ã¿ã®ããã©ã¼ãã³ã¹ã大äºã§ããããç¹ã«ä»åã®ããã«å¤ãªæç»ããããããªã±ã¼ã¹ã§ã¯ããã©ã¼ãã³ã¹ãé¡èã«è½ã¡ãå¯è½æ§ãããã®ã§ãããããã¯ããããªãã¼ã¿ã試ããªããæ¤è¨¼ããã¦ãããã¨æãã¾ããã