这能叫例行更新吗

Signed-off-by: TRAfoer <lhf190@outlook.com>
This commit is contained in:
2026-02-09 23:10:55 +08:00
parent 77726bcb6c
commit a76f650998
40 changed files with 1323 additions and 866 deletions

View File

@@ -240,63 +240,87 @@ namespace Ichni.Editor
public KeyframeVisualizer keyframeVisualizer;
public CompositeParameterWindow SetAsCustomCurve()
{
//生成一个预览器先
keyframeVisualizer.gameObject.SetActive(true);
//
// 获取字段信息和曲线数据
unitPrefab = EditorManager.instance.basePrefabs.customCurveKeyframeUnit;
FieldInfo fieldInfo = connectedBaseElement.GetType().GetField(parameterName);
AnimationCurve curve = fieldInfo.GetValue(connectedBaseElement) as AnimationCurve;
if (curve == null) curve = AnimationCurve.Linear(0, 0, 1, 1);
// 初始化预览器
keyframeVisualizer.curve = curve;
keyframeVisualizer.DrawCurveToRawImage();
keyframeVisualizer.RebuildInteractablePoints();
// 关键点:松开手柄时的回调
keyframeVisualizer.OnEditFinished = () =>
{
// 同步左侧 Unit 列表的数值显示(假设 unitList[0] 是 WrapMode
for (int i = 0; i < keyframeVisualizer.curve.length; i++)
{
int unitIdx = i + 1;
if (unitIdx < unitList.Count)
{
var unit = unitList[unitIdx] as DynamicUICustomCurveKeyframeUnit;
if (unit != null) unit.SetUnit(this, keyframeVisualizer.curve.keys[i]);
}
}
ApplyParameters?.Invoke();
};
// 生成 WrapMode 单元
WarpModes warpModes = new WarpModes(curve.preWrapMode, curve.postWrapMode);
var wmUnit = Instantiate(EditorManager.instance.basePrefabs.customCurveWrapModeUnit, windowRect)
.GetComponent<DynamicUICustomCurveWrapModeUnit>();
unitList.Add(wmUnit);
wmUnit.SetUnit(this, warpModes);
// 生成 Keyframe 单元
void GenerateUnit(Keyframe content)
{
DynamicUICustomCurveKeyframeUnit unit = Instantiate(unitPrefab, windowRect).GetComponent<DynamicUICustomCurveKeyframeUnit>();
var unit = Instantiate(unitPrefab, windowRect).GetComponent<DynamicUICustomCurveKeyframeUnit>();
unitList.Add(unit);
unit.SetUnit(this, content);
}
unitPrefab = EditorManager.instance.basePrefabs.customCurveKeyframeUnit;
AnimationCurve curve = connectedBaseElement.GetType().GetField(parameterName).GetValue(connectedBaseElement) as AnimationCurve;
List<Keyframe> keyframes = curve.keys.ToList();
WarpModes warpModes = new WarpModes(curve.preWrapMode, curve.postWrapMode);
//生成warpModes的Unit
DynamicUICustomCurveWrapModeUnit warpModesUnit =
Instantiate(EditorManager.instance.basePrefabs.customCurveWrapModeUnit, windowRect).GetComponent<DynamicUICustomCurveWrapModeUnit>();
unitList.Add(warpModesUnit);
warpModesUnit.SetUnit(this, warpModes);
//
keyframeVisualizer.curve = curve;
keyframeVisualizer.DrawCurveToRawImage();
keyframeVisualizer.CreateKeyframeImages();
//
foreach (Keyframe keyframe in keyframes)
{
GenerateUnit(keyframe);
}
foreach (Keyframe k in curve.keys) GenerateUnit(k);
addNewUnitButton.GetComponent<RectTransform>().SetAsLastSibling();
addNewUnitButton.onClick.AddListener(() =>
{
GenerateUnit(new Keyframe(0, 0, 0, 0));
Keyframe newKey = new Keyframe(0.5f, 0.5f, 0, 0);
keyframeVisualizer.curve.AddKey(newKey);
GenerateUnit(newKey);
keyframeVisualizer.RebuildInteractablePoints();
keyframeVisualizer.DrawCurveToRawImage();
addNewUnitButton.GetComponent<RectTransform>().SetAsLastSibling();
});
// 真正的应用/保存逻辑
ApplyParameters = () =>
{
AnimationCurve newCurve = new AnimationCurve();
DynamicUICustomCurveWrapModeUnit warpModesUnit = unitList[0] as DynamicUICustomCurveWrapModeUnit;
newCurve.preWrapMode = warpModesUnit.GetValue().preWrapMode;
newCurve.postWrapMode = warpModesUnit.GetValue().postWrapMode;
// 1. 获取 WrapMode
var wUnit = unitList[0] as DynamicUICustomCurveWrapModeUnit;
newCurve.preWrapMode = wUnit.GetValue().preWrapMode;
newCurve.postWrapMode = wUnit.GetValue().postWrapMode;
// 2. 获取所有 Keyframes
for (int i = 1; i < unitList.Count; i++)
{
DynamicUICustomCurveKeyframeUnit unit = unitList[i] as DynamicUICustomCurveKeyframeUnit;
newCurve.AddKey(unit.GetValue());
var kUnit = unitList[i] as DynamicUICustomCurveKeyframeUnit;
newCurve.AddKey(kUnit.GetValue());
}
FieldInfo fieldInfo = connectedBaseElement.GetType().GetField(parameterName);
fieldInfo.SetValue(connectedBaseElement, newCurve);
keyframeVisualizer.curve = newCurve;
keyframeVisualizer.DrawCurveToRawImage();
};
return this;
}
public CompositeParameterWindow SetAsGradientColorKeys()
{
void GenerateUnit(GradientColorKey content)

View File

@@ -164,37 +164,61 @@ public partial class HsvDrawer
int targetSize = useLowRes ? lowResTextureSize : highResTextureSize;
textureSize = targetSize;
// 1. 优化:仅在尺寸变化时重新分配纹理
if (_texture == null || _texture.width != textureSize || _texture.height != textureSize)
{
_texture = new Texture2D(textureSize, textureSize, TextureFormat.RGBA32, false);
_texture.filterMode = FilterMode.Bilinear; // 确保平滑
_texture.wrapMode = TextureWrapMode.Clamp;
}
_texture.wrapMode = TextureWrapMode.Clamp;
// 2. 核心优化:使用 Color32 数组代替 Color 数组
// Color32 占用的内存更小SetPixels32 的执行效率远高于 SetPixels
Color32[] pixels = new Color32[textureSize * textureSize];
Vector2 center = new Vector2(textureSize * 0.5f, textureSize * 0.5f);
float maxRadius = textureSize * 0.5f;
Color[] pixels = new Color[textureSize * textureSize];
float halfSize = textureSize * 0.5f;
float invMaxRadius = 1.0f / halfSize; // 预计算半径倒数,将除法转为乘法
Color32 clearColor = new Color32(0, 0, 0, 0);
// 3. 循环优化
for (int y = 0; y < textureSize; y++)
{
// 预计算当前行的偏移量 Y 轴部分
float offY = (y + 0.5f) - halfSize;
float offYSq = offY * offY;
int rowOffset = y * textureSize;
for (int x = 0; x < textureSize; x++)
{
Vector2 offset = new Vector2(x + 0.5f, y + 0.5f) - center;
float distance = offset.magnitude;
int idx = y * textureSize + x;
float offX = (x + 0.5f) - halfSize;
float distSq = offX * offX + offYSq; // 使用平方比较,避免在外部判断时使用 magnitude (开方)
if (distance <= maxRadius)
int idx = rowOffset + x;
if (distSq <= halfSize * halfSize)
{
float angle = Mathf.Atan2(offset.y, offset.x) * Mathf.Rad2Deg;
float distance = Mathf.Sqrt(distSq);
// 计算色相 (Hue)
float angle = Mathf.Atan2(offY, offX) * Mathf.Rad2Deg;
if (angle < 0) angle += 360f;
float sat = Mathf.Clamp01(distance / maxRadius) * saturation;
pixels[idx] = Color.HSVToRGB(angle / 360f, sat, value);
// 计算饱和度 (Saturation),受外部变量 saturation 影响
float sat = (distance * invMaxRadius) * saturation;
// 转换 HSV 到 RGB 并存入 Color32
// 注意Color.HSVToRGB 返回 Color赋值给 Color32 会自动隐式转换
pixels[idx] = Color.HSVToRGB(angle / 360f, Mathf.Clamp01(sat), value);
}
else
{
pixels[idx] = Color.clear;
pixels[idx] = clearColor;
}
}
}
_texture.SetPixels(pixels);
// 4. 应用像素数据
_texture.SetPixels32(pixels);
_texture.Apply();
}

View File

@@ -0,0 +1,312 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
namespace Ichni.Editor
{
[RequireComponent(typeof(RawImage))]
public class KeyframeVisualizer : MonoBehaviour
{
[Header("Settings")]
public Vector2Int resolution = new Vector2Int(512, 256);
public Color curveColor = Color.green;
public Color gridColor = new Color(0.3f, 0.3f, 0.3f, 0.5f);
public float pointSize = 15f;
public float tangentHandleLength = 40f;
[Header("References")]
public AnimationCurve curve;
public RawImage rawImage;
// 当用户松开鼠标编辑结束时触发用于同步外部UI
public Action OnEditFinished;
private Texture2D _texture;
private Color32[] _buffer;
private List<CurvePoint> _activePoints = new List<CurvePoint>();
// 缓存特定颜色以提高性能
private Color32 _cClear = new Color32(0, 0, 0, 0);
private Color32 _cCurve;
private Color32 _cGrid;
private void Awake()
{
if (rawImage == null) rawImage = GetComponent<RawImage>();
_cCurve = curveColor;
_cGrid = gridColor;
}
private void OnEnable()
{
if (curve == null) curve = AnimationCurve.Linear(0, 0, 1, 1);
InitTexture();
RebuildInteractablePoints();
DrawCurveToRawImage();
}
// === 核心绘制逻辑 (高性能) ===
public void DrawCurveToRawImage()
{
if (curve == null || _texture == null) return;
// 1. 清屏
int len = _buffer.Length;
for (int i = 0; i < len; i++) _buffer[i] = _cClear;
int w = resolution.x;
int h = resolution.y;
// 2. 绘制网格 (0.25, 0.5, 0.75)
DrawGridLine(w, h, 0.25f);
DrawGridLine(w, h, 0.5f);
DrawGridLine(w, h, 0.75f);
// 绘制边框
DrawRect(0, 0, w - 1, h - 1, Color.white);
// 3. 绘制曲线
// 限制范围在 0-1
int prevY = -1;
for (int x = 0; x < w; x++)
{
float t = (float)x / (w - 1);
float val = curve.Evaluate(t);
// 映射到像素高度并Clap防止数组越界
int y = Mathf.FloorToInt(Mathf.Clamp01(val) * (h - 1));
// 绘制点
int idx = y * w + x;
if (idx >= 0 && idx < len) _buffer[idx] = _cCurve;
// 垂直补间(防止曲线断裂)
if (prevY != -1 && Mathf.Abs(y - prevY) > 1)
{
int step = y > prevY ? 1 : -1;
for (int k = prevY + step; k != y; k += step)
{
int fillIdx = k * w + x;
if (fillIdx >= 0 && fillIdx < len) _buffer[fillIdx] = _cCurve;
}
}
prevY = y;
}
_texture.SetPixels32(_buffer);
_texture.Apply();
rawImage.texture = _texture;
}
private void InitTexture()
{
if (_texture == null || _texture.width != resolution.x || _texture.height != resolution.y)
{
_texture = new Texture2D(resolution.x, resolution.y, TextureFormat.ARGB32, false);
_texture.filterMode = FilterMode.Bilinear;
_buffer = new Color32[resolution.x * resolution.y];
}
}
private void DrawGridLine(int w, int h, float percent)
{
int x = (int)(w * percent);
int y = (int)(h * percent);
for (int i = 0; i < h; i++) _buffer[i * w + x] = _cGrid; // 竖线
for (int i = 0; i < w; i++) _buffer[y * w + i] = _cGrid; // 横线
}
private void DrawRect(int x1, int y1, int x2, int y2, Color32 c)
{
int w = resolution.x;
for (int x = x1; x <= x2; x++) { _buffer[y1 * w + x] = c; _buffer[y2 * w + x] = c; }
for (int y = y1; y <= y2; y++) { _buffer[y * w + x1] = c; _buffer[y * w + x2] = c; }
}
// === 交互点生成 ===
public void RebuildInteractablePoints() // 原名 CreateKeyframeImages
{
if (curve == null) return;
// 清理旧点
foreach (Transform child in transform) Destroy(child.gameObject);
_activePoints.Clear();
for (int i = 0; i < curve.length; i++)
{
// 关键帧点 (Key)
var keyPoint = CreatePoint(i, PointType.Key, Color.red, pointSize);
// 入切线 (In Tangent) - 第一个点通常不需要
if (i > 0)
{
var inPoint = CreatePoint(i, PointType.InTangent, Color.cyan, pointSize * 0.6f);
inPoint.relatedKeyPoint = keyPoint;
keyPoint.inHandle = inPoint;
}
// 出切线 (Out Tangent) - 最后一个点通常不需要
if (i < curve.length - 1)
{
var outPoint = CreatePoint(i, PointType.OutTangent, Color.cyan, pointSize * 0.6f);
outPoint.relatedKeyPoint = keyPoint;
keyPoint.outHandle = outPoint;
}
}
RefreshPointsPosition();
}
private CurvePoint CreatePoint(int index, PointType type, Color color, float size)
{
GameObject go = new GameObject($"{type}_{index}");
go.transform.SetParent(transform, false);
Image img = go.AddComponent<Image>();
img.color = color;
RectTransform rt = go.GetComponent<RectTransform>();
rt.sizeDelta = Vector2.one * size;
rt.anchorMin = rt.anchorMax = Vector2.zero; // 使用绝对坐标定位
CurvePoint cp = go.AddComponent<CurvePoint>();
cp.Init(this, index, type);
_activePoints.Add(cp);
return cp;
}
// === 坐标同步逻辑 ===
public void RefreshPointsPosition()
{
Vector2 size = rawImage.rectTransform.rect.size;
float canvasAspect = size.x / size.y;
foreach (var point in _activePoints)
{
Keyframe key = curve.keys[point.keyIndex];
Vector2 keyNormPos = new Vector2(key.time, key.value);
Vector2 keyPixelPos = new Vector2(keyNormPos.x * size.x, keyNormPos.y * size.y);
if (point.type == PointType.Key)
{
point.rectTransform.anchoredPosition = keyPixelPos;
}
else
{
float tangent = (point.type == PointType.InTangent) ? key.inTangent : key.outTangent;
Vector2 visualDir;
if (float.IsInfinity(tangent))
{
visualDir = new Vector2(0, tangent > 0 ? 1 : -1);
}
else
{
// 斜率 = (y / x) * aspect => y = (tangent / aspect) * x
// 令视觉上的 x 为 1 或 -1
float xDir = (point.type == PointType.InTangent) ? -1f : 1f;
float yDir = (tangent / canvasAspect) * xDir;
visualDir = new Vector2(xDir, yDir).normalized;
}
// 顺着计算出的向量方向,放置手柄
point.rectTransform.anchoredPosition = keyPixelPos + visualDir * tangentHandleLength;
}
}
}
// 处理点被拖拽
// 在 KeyframeVisualizer 类中修改 OnPointDragged 方法
public void OnPointDragged(CurvePoint point, Vector2 screenDelta)
{
Vector2 size = rawImage.rectTransform.rect.size;
int index = point.keyIndex;
Keyframe key = curve.keys[index];
if (point.type == PointType.Key)
{
// 关键帧移动逻辑不变
Vector2 deltaNorm = new Vector2(screenDelta.x / size.x, screenDelta.y / size.y);
key.time = Mathf.Clamp01(key.time + deltaNorm.x);
key.value = Mathf.Clamp01(key.value + deltaNorm.y);
}
else
{
// === 切线处理优化:基于向量构建 ===
// 1. 获取关键帧当前的屏幕坐标
Vector2 keyPos = point.relatedKeyPoint.rectTransform.anchoredPosition;
// 2. 获取鼠标当前的目标坐标(当前手柄位置 + 增量)
Vector2 mouseTargetPos = point.rectTransform.anchoredPosition + screenDelta;
// 3. 计算从关键帧指向鼠标的向量
Vector2 dirVec = mouseTargetPos - keyPos;
// 4. 强制方向约束 (InTangent必须在左, OutTangent必须在右)
if (point.type == PointType.InTangent)
{
if (dirVec.x > -1f) dirVec.x = -1f; // 至少向左偏 1 像素,防止除零或方向翻转
}
else if (point.type == PointType.OutTangent)
{
if (dirVec.x < 1f) dirVec.x = 1f; // 至少向右偏 1 像素
}
// 5. 计算斜率 (Tangent)
// 物理斜率 = (DeltaValue / DeltaTime)
// 对应 UI = (dirVec.y / size.y) / (dirVec.x / size.x)
float canvasAspect = size.x / size.y;
float tangent = (dirVec.y / dirVec.x) * canvasAspect;
if (point.type == PointType.InTangent) key.inTangent = tangent;
else key.outTangent = tangent;
// 6. 更新手柄位置:让手柄视觉上严格对齐鼠标方向,但保持固定长度(可选)
// 如果你不希望手柄被拉长,可以将 dirVec 归一化再乘上固定长度
// point.rectTransform.anchoredPosition = keyPos + dirVec.normalized * tangentHandleLength;
}
curve.MoveKey(index, key);
DrawCurveToRawImage();
RefreshPointsPosition(); // 统一刷新位置,确保手柄视觉表现一致
}
}
public enum PointType { Key, InTangent, OutTangent }
// 辅助交互类
public class CurvePoint : MonoBehaviour, IDragHandler, IEndDragHandler
{
public KeyframeVisualizer visualizer;
public int keyIndex;
public PointType type;
public RectTransform rectTransform;
// 引用关联点,方便计算
public CurvePoint relatedKeyPoint;
public CurvePoint inHandle;
public CurvePoint outHandle;
public void Init(KeyframeVisualizer v, int index, PointType t)
{
visualizer = v;
keyIndex = index;
type = t;
rectTransform = GetComponent<RectTransform>();
}
public void OnDrag(PointerEventData eventData)
{
// 实时更新曲线和画面
visualizer.OnPointDragged(this, eventData.delta);
}
public void OnEndDrag(PointerEventData eventData)
{
// 拖拽结束后,通知外部同步 (关键一步)
visualizer.OnEditFinished?.Invoke();
}
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 665007fcbd599ff458ca12322de17e90
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,62 @@
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using Ichni;
using Ichni.Editor;
using Ichni.RhythmGame;
using Unity.VisualScripting;
using UnityEngine;
public class PanelDrawer//暂时支持xz
{
public bool isEditing = false;
public CameraManager cameraManager => EditorManager.instance.cameraManager;
public SceneCamera sceneCamera => cameraManager.sceneCamera;
public float height
{
get
{
return _height;
}
set
{
_height = value;
}
}
private float _height = 10f;
public float baseHeight
{
get
{
return _baseHeight;
}
set
{
_baseHeight = value;
}
}
private float _baseHeight = 0f;
public void startEdit()
{
isEditing = true;
if (!cameraManager.isSceneCameraActive)
{
cameraManager.sceneCamera.transform.position = cameraManager.gameCamera.transform.position;
cameraManager.sceneCamera.transform.rotation = cameraManager.gameCamera.transform.rotation;
cameraManager.SwitchCamera();
cameraManager.sceneCamera.transform.DOMove(cameraManager.gameCamera.transform.position + new Vector3(0, 0, _baseHeight + _height), 0.5f).SetEase(Ease.InOutQuad);
cameraManager.sceneCamera.transform.DORotate(new Vector3(90, 0, 0), 0.5f).SetEase(Ease.InOutQuad);
}
else
{
cameraManager.sceneCamera.transform.DOMove(cameraManager.gameCamera.transform.position + new Vector3(0, 0, _baseHeight + _height), 0.5f).SetEase(Ease.InOutQuad);
}
}
public void endEdit()
{
isEditing = false;
}
}

View File

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

View File

@@ -1,587 +0,0 @@
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

@@ -11,26 +11,28 @@ namespace Ichni.Editor
public class CameraManager : MonoBehaviour, IBaseElement
{
public Camera backgroundCamera;
public bool isSceneCameraActive;
public SceneCamera sceneCamera;
public float sceneCameraMoveSpeed;
public float sceneCameraRotateSpeed;
public PanelDrawer panelDrawer = new();
public GameCamera gameCamera;
public bool haveGameCamera => gameCamera != null;
public Camera currentCamera => haveGameCamera && !isSceneCameraActive ? gameCamera.gameCamera : sceneCamera.sceneCamera;
public BaseElement_BM matchedBM { get; set; }
public void SwitchCamera()
{
if (!haveGameCamera)
{
{
LogWindow.Log("GameCamera is not assigned", Color.red);
return;
}
isSceneCameraActive = !isSceneCameraActive;
sceneCamera.sceneCamera.enabled = isSceneCameraActive;
gameCamera.gameCamera.enabled = !isSceneCameraActive;
@@ -41,17 +43,33 @@ namespace Ichni.Editor
public void SetUpInspector()
{
string ShowCameraType() => isSceneCameraActive ? "Scene Camera" : "Game Camera";
IHaveInspection inspector = EditorManager.instance.uiManager.inspector;
var container = inspector.GenerateContainer("Camera Manager");
//切换摄像机,设置场景相机
var cameraSettings = container.GenerateSubcontainer(3);
var cameraTypeText = inspector.GenerateHintText(this, cameraSettings, ShowCameraType);
var switchCameraButton = inspector.GenerateButton(this, cameraSettings, "Switch Camera", SwitchCamera);
var cameraMoveSpeedField = inspector.GenerateInputField(this, cameraSettings, "Scene Camera Move Speed", nameof(sceneCameraMoveSpeed));
var camMethods = container.GenerateSubcontainer(1);
if (!panelDrawer.isEditing)
{
var butt1 = inspector.GenerateButton(this, camMethods, "startEdit", () =>
{
panelDrawer.startEdit();
});
}
else
{
var butt2 = inspector.GenerateButton(this, camMethods, "endEdit", () =>
{
panelDrawer.endEdit();
});
}
sceneCamera.SetUpInspector();
}
}
}

View File

@@ -68,66 +68,69 @@ namespace Ichni.Editor
private bool isRotatingSceneCamera;
private Transform sceneCameraTransform;
private Vector3 _currentRotation; // 在类成员中缓存旋转角度
private void SceneCameraOperation()
{
if (Keyboard.current.ctrlKey.isPressed || Keyboard.current.altKey.isPressed || Keyboard.current.shiftKey.isPressed)
var kb = Keyboard.current;
var mouse = Mouse.current;
var camManager = EditorManager.instance.cameraManager;
// 1. 组合按键拦截(保持原有逻辑,但缓存引用)
if (kb.ctrlKey.isPressed || kb.altKey.isPressed || kb.shiftKey.isPressed) return;
bool isActive = camManager.isSceneCameraActive;
// 2. 位移逻辑优化
if (isActive && !isPointerOverUI)
{
return;
}
float moveSpeed = camManager.sceneCameraMoveSpeed * Time.deltaTime;
Vector3 moveDir = Vector3.zero;
if (EditorManager.instance.cameraManager.isSceneCameraActive && !isPointerOverUI) // 场景相机的移动和旋转
{
float cameraMoveSpeed = EditorManager.instance.cameraManager.sceneCameraMoveSpeed * Time.deltaTime;
// 计算合成位移向量
if (kb.wKey.isPressed) moveDir += sceneCameraTransform.forward;
if (kb.sKey.isPressed) moveDir -= sceneCameraTransform.forward;
if (kb.dKey.isPressed) moveDir += sceneCameraTransform.right;
if (kb.aKey.isPressed) moveDir -= sceneCameraTransform.right;
if (kb.eKey.isPressed) moveDir += sceneCameraTransform.up;
if (kb.qKey.isPressed) moveDir -= sceneCameraTransform.up;
if (Keyboard.current.wKey.isPressed)
// 一次性应用位移(如果存在移动)
if (moveDir != Vector3.zero)
{
sceneCameraTransform.position += sceneCameraTransform.forward * cameraMoveSpeed;
}
if (Keyboard.current.sKey.isPressed)
{
sceneCameraTransform.position -= sceneCameraTransform.forward * cameraMoveSpeed;
}
if (Keyboard.current.dKey.isPressed)
{
sceneCameraTransform.position += sceneCameraTransform.right * cameraMoveSpeed;
}
if (Keyboard.current.aKey.isPressed)
{
sceneCameraTransform.position -= sceneCameraTransform.right * cameraMoveSpeed;
}
if (Keyboard.current.eKey.isPressed)
{
sceneCameraTransform.position += sceneCameraTransform.up * cameraMoveSpeed;
}
if (Keyboard.current.qKey.isPressed)
{
sceneCameraTransform.position -= sceneCameraTransform.up * cameraMoveSpeed;
sceneCameraTransform.position += moveDir.normalized * moveSpeed;
}
}
if (EditorManager.instance.cameraManager.isSceneCameraActive)
// 3. 旋转状态切换
if (isActive)
{
if (Mouse.current.rightButton.wasPressedThisFrame && !isPointerOverUI)
if (mouse.rightButton.wasPressedThisFrame && !isPointerOverUI)
{
isRotatingSceneCamera = true;
// 初始化当前旋转角度,防止旋转跳变
_currentRotation = sceneCameraTransform.eulerAngles;
}
else if (Mouse.current.rightButton.wasReleasedThisFrame)
else if (mouse.rightButton.wasReleasedThisFrame)
{
isRotatingSceneCamera = false;
}
}
if (isRotatingSceneCamera)
// 4. 旋转逻辑优化:使用欧拉角避免万向节锁
if (isRotatingSceneCamera && EditorManager.instance.cameraManager.sceneCamera.CanBeFreeRotate)
{
float cameraRotateSpeed = EditorManager.instance.cameraManager.sceneCameraRotateSpeed * Time.deltaTime;
Vector2 mouseDelta = Mouse.current.delta.ReadValue();
sceneCameraTransform.Rotate(Vector3.up, -mouseDelta.x * cameraRotateSpeed, Space.World);
sceneCameraTransform.Rotate(sceneCameraTransform.right, mouseDelta.y * cameraRotateSpeed, Space.World);
float rotateSpeed = camManager.sceneCameraRotateSpeed * Time.deltaTime;
Vector2 mouseDelta = mouse.delta.ReadValue();
// 计算新的旋转角度Y轴左右转X轴上下抬头
_currentRotation.y += mouseDelta.x * rotateSpeed;
_currentRotation.x -= mouseDelta.y * rotateSpeed;
// 限制抬头角度(防止翻转)
_currentRotation.x = Mathf.Clamp(_currentRotation.x, -89f, 89f);
sceneCameraTransform.rotation = Quaternion.Euler(_currentRotation.x, _currentRotation.y, 0f);
}
}

View File

@@ -19,24 +19,40 @@ namespace Ichni.Editor
public BaseElement_BM matchedBM { get; set; }
[HideInInspector]
public Vector3 cameraPosition; //注意这里的Position和EulerAngles是transform的中介变量仅能用于Inspector显示
public Vector3 cameraPosition
{
get => sceneCamera.transform.position;
set
{
sceneCamera.transform.position = value;
}
}
[HideInInspector]
public Vector3 cameraEulerAngles;
public Vector3 cameraEulerAngles
{
get => sceneCamera.transform.eulerAngles;
set
{
sceneCamera.transform.eulerAngles = value;
}
}
public bool CanBeFreeRotate => EditorManager.instance.cameraManager.panelDrawer.isEditing;
public void SetUpInspector()
{
IHaveInspection inspector = EditorManager.instance.uiManager.inspector;
var container = inspector.GenerateContainer("Scene Camera");
//摄像机参数设置
var cameraSettings = container.GenerateSubcontainer(3);
var viewTypeDropdown =
var viewTypeDropdown =
inspector.GenerateDropdown(this, cameraSettings, "View Type", typeof(GameCamera.CameraViewType), nameof(viewType))
.AddListenerFunction(() => sceneCamera.orthographic = viewType == GameCamera.CameraViewType.Orthographic);
var perspectiveAngleField =
var perspectiveAngleField =
inspector.GenerateInputField(this, cameraSettings, "Perspective Angle", nameof(perspectiveAngle))
.AddListenerFunction(() => sceneCamera.fieldOfView = perspectiveAngle);
var orthographicSizeField =
var orthographicSizeField =
inspector.GenerateInputField(this, cameraSettings, "Orthographic Size", nameof(orthographicSize))
.AddListenerFunction(() => sceneCamera.orthographicSize = orthographicSize);
@@ -45,7 +61,7 @@ namespace Ichni.Editor
var positionInputFields =
inspector.GenerateVector3InputField(this, transformSettings, "Position", nameof(cameraPosition), true)
.AddListenerFunction(() => sceneCamera.transform.position = cameraPosition);
var eulerAnglesInputFields =
var eulerAnglesInputFields =
inspector.GenerateVector3InputField(this, transformSettings, "Euler Angles", nameof(cameraEulerAngles), true)
.AddListenerFunction(() => sceneCamera.transform.eulerAngles = cameraEulerAngles);
}