# LIVE2D Practice ## 匯入SDK https://www.live2d.com/en/sdk/download/unity/ https://docs.live2d.com/cubism-sdk-tutorials/getting-started/ ## 匯入人物 https://booth.pm/en/search/live2d%E3%83%A2%E3%83%87%E3%83%AB?max_price=0 https://booth.pm/en/items/6235947 # 11/18 ## 視線追蹤 ### 效果 ![image](https://hackmd.io/_uploads/H1ERk7_zyl.png) ### 人物Root放一個CubismLookController ![image](https://hackmd.io/_uploads/H1GylQuMyx.png) ### ParamAngleY放一個CubismLookParameter ![image](https://hackmd.io/_uploads/HJDxgmdGkl.png) ## 看向鼠標 ```csharp= public class LookMouse : MonoBehaviour { public Transform target; //取得Target // Update is called once per frame void Update() { //獲得鼠標位置 print($"mouse position : {Input.mousePosition}");//(0,0)~(1920,1080) var pos = Camera.main.ScreenToWorldPoint(Input.mousePosition); print($"world position : {pos}"); //Target設成鼠標位置 target.position = pos; } } ``` ## 自動眨眼 ![image](https://hackmd.io/_uploads/Hkf49Qdzyx.png) ### 人物Root加入CubismEyeBlinkController跟CubismAutoEyeBlinkInput ![{F3C2F976-A5B5-4B52-8ACA-47ED4D8032CD}](https://hackmd.io/_uploads/B18_G6PMJl.png) ### 眼睛的Parameter加入CubismEyeBlinkParameter ![image](https://hackmd.io/_uploads/Hyweqm_Mke.png) :::info 若MODEL有照標準命名 眨眼會是ParamEyeLOpen、ParamEyeROpen ![image](https://hackmd.io/_uploads/r1ghc7_G1x.png) 來源:https://docs.live2d.com/zh-CHS/cubism-editor-manual/standard-parameter-list/ ::: ## 循環動作 ![{D0C1508E-E43D-4040-94BD-F3D158198850}](https://hackmd.io/_uploads/BkImlTDz1l.png) ## 表情熱鍵 ```charp using System; using System.Collections; using System.Collections.Generic; using Live2D.Cubism.Framework.Expression; using UnityEngine; public class ExpressionHotkey : MonoBehaviour { public CubismExpressionController _controller; [Serializable] public struct ExpressionMapping //熱鍵設定 { public KeyCode key; //鍵位 public int expressionIndex; //表情號碼 public string name; //表情名子 } public List<ExpressionMapping> _mappings; // 熱鍵列表 void Start() { for (int i = 0; i < _controller.ExpressionsList.CubismExpressionObjects.Length; i++) { print($"{i} : {_controller.ExpressionsList.CubismExpressionObjects[i].name}"); } //Keycode -> expressionIndex(int) _mappings = new List<ExpressionMapping>(); int expressionLength = _controller.ExpressionsList.CubismExpressionObjects.Length; for (int i = 0; i < expressionLength; i++) { KeyCode keycode = KeyCode.Alpha0 + i; //Alpha0, Alpha1, Alpha2...... _mappings.Add(new ExpressionMapping() { key = keycode, //Alpha0, Alpha1, Alpha2...... expressionIndex = i, //表情號碼0, 1, 2...... name = _controller.ExpressionsList.CubismExpressionObjects[i].name }); } } // Update is called once per frame void Update() { for (int i = 0; i < _mappings.Count; i++) { if (Input.GetKey(_mappings[i].key)) { _controller.CurrentExpressionIndex = _mappings[i].expressionIndex; } } } } ``` # 11/25 ## UI播動畫 ```csharp using Live2D.Cubism.Core; using Live2D.Cubism.Framework.Motion; using Live2D.Cubism.Framework.MotionFade; using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class MotionHotkey : MonoBehaviour { private CubismModel model; public CubismParameter[] parameters; public CubismFadeController fadeController; public Dictionary<string, CubismParameter> lookupTable = new(); [Serializable] public struct Motion { public CubismParameter[] Parameters; public AnimationCurve[] Curves; } public List<Motion> motionList; private void Start() { if (TryGetComponent(out model)) { parameters = model.Parameters; foreach (var para in parameters)//建立查找表 { lookupTable.Add(para.Id, para);//查找表填入每個ID對應的Parameter } motionList = new List<Motion>(); int length = fadeController.CubismFadeMotionList.CubismFadeMotionObjects.Length;//取得動畫個數 for (int i = 0; i < length; i++)//巡迴每個動畫 { CubismFadeMotionData data = fadeController.CubismFadeMotionList.CubismFadeMotionObjects[i];//動畫資料 int paramerterCount = data.ParameterIds.Length;//動畫資料裡用到ID的總數 Motion motion = new Motion(); motion.Parameters = new CubismParameter[paramerterCount];//建立跟總數一樣長度的Array motion.Curves = new AnimationCurve[paramerterCount];//建立跟總數一樣長度的Array for (int j = 0; j < paramerterCount; j++)//巡迴每個ID { string id = data.ParameterIds[j];//拿到ID AnimationCurve curve = data.ParameterCurves[j];//拿到動畫曲線 if (lookupTable.TryGetValue(id, out CubismParameter para) && para != null)//如果查找表裡知的有叫這個ID的參數 { motion.Parameters[j] = para;//紀錄參數 motion.Curves[j] = curve;//紀錄對應的動畫曲線 } motionList.Add(motion);//加到動畫清單裡 } } } } public void PlayMotion(int index) { if (index >= 0 && index < motionList.Count) { StopAllCoroutines(); Motion motion = motionList[index]; for (int i = 0; i < motion.Parameters.Length; i++) { StartCoroutine(PlayMotion(motion.Parameters[i], motion.Curves[i])); } } } public IEnumerator PlayMotion(CubismParameter parameter,AnimationCurve curve) { float t = 0; while (t <= curve.length) { float value = curve.Evaluate(t); parameter.Value = value; print($"{parameter.Id}:{value}"); t += Time.deltaTime; yield return null; } } } ``` # 12/02 ## 物件跟隨 1. 人物所有部位(ArtMesh)加上CubismRaycastable ![image](https://hackmd.io/_uploads/Sk3k0YqXyg.png) 2. 人物Root加上加上CubismRaycaster ![image](https://hackmd.io/_uploads/BkB-AFcQ1g.png) ### 印出物件名稱 ```csharp using Live2D.Cubism.Core; using Live2D.Cubism.Framework.Raycasting; using UnityEngine; public class ObjectFollow : MonoBehaviour { public CubismRaycaster raycaster; public CubismDrawable selectedDrawable; private void Update() { var ray = Camera.main.ScreenPointToRay(Input.mousePosition); //0,0~ 1920*1080 Debug.DrawRay(ray.origin, ray.direction * Camera.main.farClipPlane, Color.green, 1); CubismRaycastHit[] result = new CubismRaycastHit[1]; if (raycaster.Raycast(ray.origin,ray.direction * Camera.main.farClipPlane,result) > 0) { print($"{result[0].Drawable.name} : {result[0].WorldPosition}"); } } } ``` :::info 結果: ![image](https://hackmd.io/_uploads/BkEqb9qXJg.png) ::: ### 物件貼到座標 假設戴一頂帽子 ![full-m2H7d3m2Z5H7N4b1](https://hackmd.io/_uploads/Hyg40cqQye.png) 帽子圖片放到新建的SpriteRenderer上 ![image](https://hackmd.io/_uploads/B1N7RqcX1l.png) ```charp using Live2D.Cubism.Core; using Live2D.Cubism.Framework.Raycasting; using UnityEngine; public class ObjectFollow : MonoBehaviour { public CubismRaycaster raycaster; public CubismDrawable selectedDrawable; public int closestPointIndex; public Vector3 closestPoint; public Transform Hat; public Vector3 offset; private void Update() { if (Input.GetMouseButtonDown(0)) { var ray = Camera.main.ScreenPointToRay(Input.mousePosition); //0,0~ 1920*1080 Debug.DrawRay(ray.origin, ray.direction * Camera.main.farClipPlane, Color.green, 1); if (raycaster.Raycast(ray.origin,ray.direction * Camera.main.farClipPlane,result) > 0) { print($"{result[0].Drawable.name} : {result[0].WorldPosition}"); selectedDrawable = result[0].Drawable;//滑鼠選到的部位 float min = Mathf.Infinity;//先設無限大 for (int i = 0; i < selectedDrawable.VertexPositions.Length; i++) { float dis = Vector3.Distance(result[0].Drawable.VertexPositions[i], result[0].WorldPosition);//計算滑鼠點擊座標跟每個模型polygon座標的距離 if (dis < min)//才會從無限大慢慢比較越來越小 { closestPointIndex = i;//得到第幾個polygon是離滑鼠最近的 min = dis; } closestPoint = selectedDrawable.VertexPositions[closestPointIndex];//取得模型上距離最近polygon的座標 offset = result[0].WorldPosition - closestPoint;//offset回去滑鼠點到的座標 //強制把帽子Z軸設在相機前面 offset.z = Camera.main.transform.position.z + Camera.main.nearClipPlane;//相機起始點=相機Z軸座標+相機nearClipPlane(開始渲染範圍) } } else { selectedDrawable = null; } } if (selectedDrawable!=null) { closestPoint = selectedDrawable.VertexPositions[closestPointIndex]+transform.position;//取得模型上距離最近polygon的座標 + 人物在場景中的座標 Hat.position = closestPoint + offset; } } ``` 記得把帽子放到Hat欄位 ![image](https://hackmd.io/_uploads/BkjqCqq7kl.png) ### 最終完整程式 ```csharp using Live2D.Cubism.Core; using Live2D.Cubism.Framework.Raycasting; using UnityEngine; public class ObjectFollow : MonoBehaviour { public CubismRaycaster raycaster; public CubismDrawable selectedDrawable; public int closestPointIndex; public int farestPointIndex; public Vector3 closestPoint; public Vector3 farestPoint; public Vector3 startDirection; public float angle; public Transform Hat; public Vector3 offset; public CubismRaycastHit[] result = new CubismRaycastHit[1]; private void Update() { if (Input.GetMouseButtonDown(0)) { var ray = Camera.main.ScreenPointToRay(Input.mousePosition); //0,0~ 1920*1080 Debug.DrawRay(ray.origin, ray.direction * Camera.main.farClipPlane, Color.green, 1); if (raycaster.Raycast(ray.origin,ray.direction * Camera.main.farClipPlane,result) > 0) { print($"{result[0].Drawable.name} : {result[0].WorldPosition}"); selectedDrawable = result[0].Drawable;//滑鼠選到的部位 float min = Mathf.Infinity;//先設無限大 float max = 0;//先設0 for (int i = 0; i < selectedDrawable.VertexPositions.Length; i++) { float dis = Vector3.Distance(result[0].Drawable.VertexPositions[i], result[0].WorldPosition);//計算滑鼠點擊座標跟每個模型polygon座標的距離 if (dis < min)//才會從無限大慢慢越比較越變小 { closestPointIndex = i;//得到第幾個polygon是離滑鼠最近的 min = dis; } if (dis > max) { farestPointIndex = i; max = dis; } closestPoint = selectedDrawable.VertexPositions[closestPointIndex];//取得模型上距離最近polygon的座標 farestPoint = selectedDrawable.VertexPositions[farestPointIndex]; startDirection = farestPoint - closestPoint; offset = result[0].WorldPosition - closestPoint;//offset回去滑鼠點到的座標 //強制把帽子Z軸設在相機前面 offset.z = Camera.main.transform.position.z + Camera.main.nearClipPlane;//相機起始點=相機Z軸座標+相機nearClipPlane(開始渲染範圍) } } else { selectedDrawable = null; } } if (selectedDrawable!=null)//如果有選到人物部位 { closestPoint = selectedDrawable.VertexPositions[closestPointIndex] + transform.position;//取得模型上距離最近polygon的座標 + 人物在場景中的座標 farestPoint = selectedDrawable.VertexPositions[farestPointIndex] + transform.position; angle = Vector3.SignedAngle(startDirection, (farestPoint - closestPoint), startDirection);//計算目前選到部位,最近跟最遠的Polygon方向跟一開始滑鼠點的時候差多少度了 Hat.position = closestPoint + offset; Hat.rotation = Quaternion.Euler(0, 0,angle );//旋轉Z軸 } } } ``` ![image](https://hackmd.io/_uploads/HyMx8nq7Jl.png)