Files
ichni_Creator_Studio/Assets/Scripts/Graphical Tools/Hermite Editor/KeyframeVisualizer.cs

587 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<KeyframeImageInfo> keyframeImages = new List<KeyframeImageInfo>();
// 合并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; }
}
}
/// <summary>
/// 获取关键帧在RawImage中的localPosition以RawImage中心为原点
/// </summary>
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<RectTransform>();
rt.sizeDelta = Vector2.one * 16f;
rt.anchoredPosition = localPos;
Image img = go.GetComponent<Image>();
img.color = keyframeColor;
img.raycastTarget = false;
PointMover pointMover = go.AddComponent<PointMover>();
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<RectTransform>();
trt.sizeDelta = Vector2.one * 10f;
trt.anchoredPosition = tangentLocalPos;
inImg = tgo.GetComponent<Image>();
inImg.color = tangentColor;
inImg.raycastTarget = false;
PointMover pointMover1 = tgo.AddComponent<PointMover>();
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<RectTransform>();
trt.sizeDelta = Vector2.one * 10f;
trt.anchoredPosition = tangentLocalPos;
outImg = tgo.GetComponent<Image>();
outImg.color = tangentColor * 0.8f;
outImg.raycastTarget = false;
PointMover pointMover2 = tgo.AddComponent<PointMover>();
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<PointMover> subpointMover = new();
public bool Pressed;
public int IO = 0;//0关键帧 1in -1out
private Dictionary<PointMover, Vector2> initialOffsets; // 存储切线点初始偏移
private Vector2 startPosition; // 拖拽开始位置
private void Start()
{
rectTransform = gameObject.GetComponent<RectTransform>();
}
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<WindowDragger>();
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();
}
}
}