曲线视觉编辑器,初步

This commit is contained in:
2025-07-01 19:10:31 +08:00
parent 7fee6d651a
commit ffb97c6d28
11 changed files with 3475 additions and 94 deletions

View File

@@ -0,0 +1,587 @@
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();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e4743327f32eb24e86090ec474ac91a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: