using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using Sirenix.OdinInspector; using System.Linq; using UnityEngine.InputSystem; using Michsky.MUIP; #if UNITY_EDITOR using UnityEditor; #endif namespace Ichni.Editor { [RequireComponent(typeof(RawImage))] public class KeyframeVisualizer : MonoBehaviour { public AnimationCurve curve; public float timeRange = 10f; public float valueRange = 10f; public float keyframeSize = 0.2f; public float tangentLength = 1f; public Color curveColor = Color.green; public Color keyframeColor = Color.red; public Color tangentColor = Color.blue; public int segments = 5; [Header("Curve Preview")] public RawImage curveRawImage; public Vector2Int curveTextureSize = new Vector2Int(256, 128); [System.Serializable] public struct KeyframeImageInfo { public Image keyImage; public Image inTangentImage; public Image outTangentImage; } public List keyframeImages = new List(); // 合并min/max到类成员,便于全局一致使用 private float minTime, maxTime, minValue = 0f, maxValue = 1f; private void UpdateCurveRange() { if (curve == null || curve.length < 2) { minTime = maxTime = valueRange = timeRange = 0; minValue = 0f; maxValue = 1f; return; } minTime = 0f; maxTime = 1f; minValue = 0f; // 固定下界为0 maxValue = 1f; // 固定上界为1 valueRange = Mathf.Max(0.0001f, maxValue - minValue); timeRange = Mathf.Max(0.0001f, maxTime - minTime); } [Button("Draw Curve To RawImage")] public void DrawCurveToRawImage() { UpdateCurveRange(); if (curveRawImage == null || curve == null || curve.length < 2) return; int texWidth = curveTextureSize.x; int texHeight = curveTextureSize.y; Texture2D tex = new Texture2D(texWidth, texHeight, TextureFormat.ARGB32, false); tex.filterMode = FilterMode.Point; tex.wrapMode = TextureWrapMode.Clamp; // 清空 for (int i = 0; i < texWidth; i++) for (int j = 0; j < texHeight; j++) tex.SetPixel(i, j, new Color(0, 0, 0, 0)); // === 新增:绘制网格 === int gridXCount = 8; int gridYCount = 4; Color gridColor = new Color(0.3f, 0.3f, 0.3f, 0.7f); for (int gx = 1; gx < gridXCount; gx++) { int x = gx * texWidth / gridXCount; DrawVerticalLineOnTexture(tex, x, 0, texHeight - 1, gridColor); } for (int gy = 1; gy < gridYCount; gy++) { int y = gy * texHeight / gridYCount; DrawHorizontalLineOnTexture(tex, 0, texWidth - 1, y, gridColor); } // === 新增:绘制边框 === Color borderColor = Color.white; DrawHorizontalLineOnTexture(tex, 0, texWidth - 1, 0, borderColor); DrawHorizontalLineOnTexture(tex, 0, texWidth - 1, texHeight - 1, borderColor); DrawVerticalLineOnTexture(tex, 0, 0, texHeight - 1, borderColor); DrawVerticalLineOnTexture(tex, texWidth - 1, 0, texHeight - 1, borderColor); int lastY = -1; for (int x = 0; x < texWidth; x++) { float t = (float)x / (texWidth - 1); float time = Mathf.Lerp(minTime, maxTime, t); float value = curve.Evaluate(time); float normY = (value - minValue) / valueRange; int y = (int)(normY * (texHeight - 1)); bool outOfRange = y < 0 || y >= texHeight; int drawY = y; Color drawColor = outOfRange ? new Color(1f - curveColor.r, 1f - curveColor.g, 1f - curveColor.b, 1f) : curveColor; if (lastY >= 0) { int y0 = lastY; int y1 = y; int x0 = x - 1; int x1 = x; // === 修正:补全断点 === if (x0 >= 0 && x0 < texWidth && x1 >= 0 && x1 < texWidth) { if (Mathf.Abs(y1 - y0) > 1) { int step = y1 > y0 ? 1 : -1; for (int yyy = y0; yyy != y1; yyy += step) { if (yyy >= 0 && yyy < texHeight) tex.SetPixel(x0, yyy, curveColor); } } } int dy = Mathf.Abs(y1 - y0); int sy = y0 < y1 ? 1 : -1; int err = dy / 2; int yy = y0; for (int xx = x0; xx <= x1; xx++) { bool segOutOfRange = yy < 0 || yy >= texHeight; Color segColor = segOutOfRange ? new Color(1f - curveColor.r, 1f - curveColor.g, 1f - curveColor.b, 1f) : curveColor; if (xx >= 0 && xx < texWidth) { for (int j = lastY; j < yy; j++) tex.SetPixel(x0, j, curveColor); for (int j = lastY; j > yy; j--) tex.SetPixel(x0, j, curveColor); tex.SetPixel(xx, yy, segColor); } err -= dy; if (err < 0) { yy += sy; err += (x1 - x0); } } } if (x >= 0 && x < texWidth) tex.SetPixel(x, drawY, drawColor); lastY = y; } foreach (var key in curve.keys) { float tangentScale = (maxTime - minTime) * 0.08f; if (!float.IsInfinity(key.inTangent)) { float t0 = key.time; float v0 = key.value; float t1 = t0 - tangentScale; float v1 = v0 - key.inTangent * tangentScale; int x0 = (int)(((t0 - minTime) / timeRange) * (texWidth - 1)); int y0 = (int)(((v0 - minValue) / valueRange) * (texHeight - 1)); int x1 = (int)(((t1 - minTime) / timeRange) * (texWidth - 1)); int y1 = (int)(((v1 - minValue) / valueRange) * (texHeight - 1)); DrawLineOnTexture(tex, x0, y0, x1, y1, tangentColor); } if (!float.IsInfinity(key.outTangent)) { float t0 = key.time; float v0 = key.value; float t1 = t0 + tangentScale; float v1 = v0 + key.outTangent * tangentScale; int x0 = (int)(((t0 - minTime) / timeRange) * (texWidth - 1)); int y0 = (int)(((v0 - minValue) / valueRange) * (texHeight - 1)); int x1 = (int)(((t1 - minTime) / timeRange) * (texWidth - 1)); int y1 = (int)(((v1 - minValue) / valueRange) * (texHeight - 1)); DrawLineOnTexture(tex, x0, y0, x1, y1, tangentColor * 0.8f); } } tex.Apply(); curveRawImage.texture = tex; } private void DrawLineOnTexture(Texture2D tex, int x0, int y0, int x1, int y1, Color color) { int dx = Mathf.Abs(x1 - x0), sx = x0 < x1 ? 1 : -1; int dy = Mathf.Abs(y1 - y0), sy = y0 < y1 ? 1 : -1; int err = (dx > dy ? dx : -dy) / 2, e2; int lastY = y0; while (true) { for (int j = lastY; j < y0; j++) tex.SetPixel(x0, j, color); for (int j = lastY; j > y0; j--) tex.SetPixel(x0, j, color); tex.SetPixel(x0, y0, color); lastY = y0; if (x0 == x1 && y0 == y1) break; e2 = err; if (e2 > -dx) { err -= dy; x0 += sx; } if (e2 < dy) { err += dx; y0 += sy; } } } /// /// 获取关键帧在RawImage中的localPosition(以RawImage中心为原点) /// public Vector2 GetKeyframeLocalPositionInRawImage(Keyframe key, float scale = 1f) { UpdateCurveRange(); if (curveRawImage == null || curve == null || curve.length < 2) return Vector2.zero; // 这里直接使用固定的minValue/maxValue float normX = (key.time - minTime) / timeRange; float normY = (key.value - 0f) / Mathf.Max(0.0001f, 1f - 0f); float px = normX * curveTextureSize.x; float py = normY * curveTextureSize.y; Vector2 localPos = new Vector2( px - curveTextureSize.x * 0.5f, py - curveTextureSize.y * 0.5f ); return localPos * scale; } [Button("Create Keyframe Images")] public void CreateKeyframeImages() { UpdateCurveRange(); if (curveRawImage == null || curve == null || curve.length < 1) return; for (int i = curveRawImage.transform.childCount - 1; i >= 0; i--) { var child = curveRawImage.transform.GetChild(i); #if UNITY_EDITOR if (!Application.isPlaying) DestroyImmediate(child.gameObject); else #endif Destroy(child.gameObject); } keyframeImages.Clear(); for (int i = 0; i < curve.length; i++) { Keyframe key = curve.keys[i]; Vector2 localPos = GetKeyframeLocalPositionInRawImage(key); GameObject go = new GameObject($"KeyframeImage_{i}", typeof(RectTransform), typeof(Image)); go.transform.SetParent(curveRawImage.transform, false); RectTransform rt = go.GetComponent(); rt.sizeDelta = Vector2.one * 16f; rt.anchoredPosition = localPos; Image img = go.GetComponent(); img.color = keyframeColor; img.raycastTarget = false; PointMover pointMover = go.AddComponent(); pointMover.parent = this; pointMover.keyIndex = i; Image inImg = null; if (!float.IsInfinity(key.inTangent)) { Vector2 tangentLocalPos = GetTangentLocalPosition(key, true); GameObject tgo = new GameObject($"TangentInImage_{i}", typeof(RectTransform), typeof(Image)); tgo.transform.SetParent(curveRawImage.transform, false); RectTransform trt = tgo.GetComponent(); trt.sizeDelta = Vector2.one * 10f; trt.anchoredPosition = tangentLocalPos; inImg = tgo.GetComponent(); inImg.color = tangentColor; inImg.raycastTarget = false; PointMover pointMover1 = tgo.AddComponent(); pointMover1.IO = 1; pointMover1.parent = this; pointMover1.keyIndex = i; pointMover.subpointMover.Add(pointMover1); } Image outImg = null; if (!float.IsInfinity(key.outTangent)) { Vector2 tangentLocalPos = GetTangentLocalPosition(key, false); GameObject tgo = new GameObject($"TangentOutImage_{i}", typeof(RectTransform), typeof(Image)); tgo.transform.SetParent(curveRawImage.transform, false); RectTransform trt = tgo.GetComponent(); trt.sizeDelta = Vector2.one * 10f; trt.anchoredPosition = tangentLocalPos; outImg = tgo.GetComponent(); outImg.color = tangentColor * 0.8f; outImg.raycastTarget = false; PointMover pointMover2 = tgo.AddComponent(); pointMover2.IO = -1; pointMover2.parent = this; pointMover2.keyIndex = i; pointMover.subpointMover.Add(pointMover2); } keyframeImages.Add(new KeyframeImageInfo { keyImage = img, inTangentImage = inImg, outTangentImage = outImg }); } } // 修复后的切线位置计算方法 private Vector2 GetTangentLocalPosition(Keyframe key, bool isIn) { Vector2 keyLocalPos = GetKeyframeLocalPositionInRawImage(key); float tangent = isIn ? key.inTangent : key.outTangent; // 计算像素/单位转换比例 float pixelsPerTimeUnit = curveTextureSize.x / timeRange; float pixelsPerValueUnit = curveTextureSize.y / valueRange; // 处理无穷斜率的情况(垂直切线) if (float.IsInfinity(tangent)) { return keyLocalPos + new Vector2( 0, isIn ? -tangentLength * 20f : tangentLength * 20f ); } // 计算方向向量 Vector2 direction = new Vector2( isIn ? -1f : 1f, tangent * (pixelsPerValueUnit / pixelsPerTimeUnit) ).normalized; // 动态计算像素长度 float dynamicLength = tangentLength * Mathf.Min(curveTextureSize.x, curveTextureSize.y) * 0.1f; return keyLocalPos + direction * dynamicLength; } [Button("Update Curve From Images")] public void UpdateCurveFromImages() { UpdateCurveRange(); if (curve == null || keyframeImages == null || keyframeImages.Count < curve.length) return; Keyframe[] newKeys = new Keyframe[curve.length]; for (int i = 0; i < curve.length; i++) { RectTransform rt = keyframeImages[i].keyImage.rectTransform; Vector2 localPos = rt.anchoredPosition; float px = localPos.x + curveTextureSize.x * 0.5f; float py = localPos.y + curveTextureSize.y * 0.5f; float normX = px / curveTextureSize.x; float normY = py / curveTextureSize.y; float time = Mathf.Lerp(minTime, maxTime, normX); float value = Mathf.Clamp01(0f + normY * Mathf.Max(0.0001f, 1f - 0f)); float inTangent = curve.keys[i].inTangent; float outTangent = curve.keys[i].outTangent; float timeScale = timeRange / curveTextureSize.x; float valueScale = valueRange / curveTextureSize.y; // 修复切线斜率计算 if (keyframeImages[i].inTangentImage != null) { Vector2 inLocalPos = keyframeImages[i].inTangentImage.rectTransform.anchoredPosition; Vector2 keyLocalPos = keyframeImages[i].keyImage.rectTransform.anchoredPosition; // 使用像素坐标差计算真实斜率 float dx = (keyLocalPos.x - inLocalPos.x) * timeScale; float dy = (keyLocalPos.y - inLocalPos.y) * valueScale; if (Mathf.Abs(dx) > 0.001f) inTangent = dy / dx; } if (keyframeImages[i].outTangentImage != null) { Vector2 outLocalPos = keyframeImages[i].outTangentImage.rectTransform.anchoredPosition; Vector2 keyLocalPos = keyframeImages[i].keyImage.rectTransform.anchoredPosition; // 使用像素坐标差计算真实斜率 float dx = (outLocalPos.x - keyLocalPos.x) * timeScale; float dy = (outLocalPos.y - keyLocalPos.y) * valueScale; if (Mathf.Abs(dx) > 0.001f) outTangent = dy / dx; } Keyframe newKey = new Keyframe(time, value, inTangent, outTangent); newKeys[i] = newKey; } curve.keys = newKeys; } [Button("Refresh Keyframe Images Position")] public void RefreshKeyframeImagesPosition() { UpdateCurveRange(); if (curve == null || keyframeImages == null || keyframeImages.Count < curve.length) return; for (int i = 0; i < curve.length; i++) { Keyframe key = curve.keys[i]; var info = keyframeImages[i]; if (info.keyImage != null) { Vector2 localPos = GetKeyframeLocalPositionInRawImage(key); info.keyImage.rectTransform.anchoredPosition = localPos; } if (info.inTangentImage != null && !float.IsInfinity(key.inTangent)) { Vector2 tangentLocalPos = GetTangentLocalPosition(key, true); info.inTangentImage.rectTransform.anchoredPosition = tangentLocalPos; } if (info.outTangentImage != null && !float.IsInfinity(key.outTangent)) { Vector2 tangentLocalPos = GetTangentLocalPosition(key, false); info.outTangentImage.rectTransform.anchoredPosition = tangentLocalPos; } } } public CompositeParameterWindow compositeParameterWindow; public void UpadteValue() { compositeParameterWindow.connectedBaseElement.GetType().GetField(compositeParameterWindow.parameterName).SetValue(compositeParameterWindow.connectedBaseElement, curve); } // === 新增:绘制水平线辅助方法 === private void DrawHorizontalLineOnTexture(Texture2D tex, int x0, int x1, int y, Color color) { int width = tex.width; int height = tex.height; if (y < 0 || y >= height) return; int minX = Mathf.Clamp(Mathf.Min(x0, x1), 0, width - 1); int maxX = Mathf.Clamp(Mathf.Max(x0, x1), 0, width - 1); for (int x = minX; x <= maxX; x++) tex.SetPixel(x, y, color); } // === 新增:绘制垂直线辅助方法 === private void DrawVerticalLineOnTexture(Texture2D tex, int x, int y0, int y1, Color color) { int width = tex.width; int height = tex.height; if (x < 0 || x >= width) return; int minY = Mathf.Clamp(Mathf.Min(y0, y1), 0, height - 1); int maxY = Mathf.Clamp(Mathf.Max(y0, y1), 0, height - 1); for (int y = minY; y <= maxY; y++) tex.SetPixel(x, y, color); } } public class PointMover : MonoBehaviour { public RectTransform rectTransform; public KeyframeVisualizer parent; public int keyIndex = -1; // 记录所属关键帧索引 public PointMover parentPointMover; public List subpointMover = new(); public bool Pressed; public int IO = 0;//0关键帧 1in -1out private Dictionary initialOffsets; // 存储切线点初始偏移 private Vector2 startPosition; // 拖拽开始位置 private void Start() { rectTransform = gameObject.GetComponent(); } private void Update() { if (RectTransformUtility.RectangleContainsScreenPoint(rectTransform, Mouse.current.position.ReadValue())) { if (Mouse.current.leftButton.wasPressedThisFrame) { StartCoroutine(Moving()); } } } public IEnumerator Moving() { var windowDragger = parent.compositeParameterWindow.GetComponent(); if (windowDragger != null) { windowDragger.Lock = true; } // 获取边界尺寸 float halfWidth = parent.curveTextureSize.x * 0.5f; float halfHeight = parent.curveTextureSize.y * 0.5f; // 如果是切线点,记录初始位置和方向 Vector2 initialPosition = rectTransform.anchoredPosition; Vector2 initialDirection = Vector2.zero; if (IO != 0) { PointMover keyPoint = parentPointMover != null ? parentPointMover : this; Vector2 keyPos = keyPoint.rectTransform.anchoredPosition; initialDirection = (initialPosition - keyPos).normalized; } while (Mouse.current.leftButton.isPressed) { Vector2 mouseDelta = Mouse.current.delta.ReadValue(); Vector2 newPos = rectTransform.anchoredPosition + mouseDelta; // 边界约束 newPos.x = Mathf.Clamp(newPos.x, -halfWidth, halfWidth); newPos.y = Mathf.Clamp(newPos.y, -halfHeight, halfHeight); // 如果是切线点,约束移动方向 if (IO != 0 && initialDirection != Vector2.zero) { Vector2 keyPos = parentPointMover.rectTransform.anchoredPosition; Vector2 toNewPos = newPos - keyPos; // 计算与初始方向的夹角 float angle = Vector2.Angle(initialDirection, toNewPos); // 如果偏离初始方向超过45度,修正方向 if (angle > 45f) { // 投影到初始方向 float dot = Vector2.Dot(toNewPos, initialDirection); newPos = keyPos + initialDirection * Mathf.Sign(dot) * toNewPos.magnitude; } } rectTransform.anchoredPosition = newPos; // 如果是关键帧点,同时移动切线点 if (IO == 0) { foreach (PointMover tangentPoint in subpointMover) { if (tangentPoint != null) { tangentPoint.rectTransform.anchoredPosition += mouseDelta; } } } yield return null; } if (windowDragger != null) { windowDragger.Lock = false; } UpdateParentCurve(); } public void UpdateParentCurve() { parent.UpdateCurveFromImages(); parent.DrawCurveToRawImage(); parent.UpadteValue(); parent.CreateKeyframeImages(); } } }