283 lines
9.4 KiB
C++
283 lines
9.4 KiB
C++
#include "ScriptRuntime.h"
|
|
#include "SceneObject.h"
|
|
#include "ThirdParty/imgui/imgui.h"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
struct Keyframe {
|
|
float time = 0.0f;
|
|
glm::vec3 position = glm::vec3(0.0f);
|
|
glm::vec3 rotation = glm::vec3(0.0f);
|
|
glm::vec3 scale = glm::vec3(1.0f);
|
|
};
|
|
|
|
int targetId = -1;
|
|
char targetName[128] = "";
|
|
std::vector<Keyframe> keyframes;
|
|
int selectedKey = -1;
|
|
|
|
float clipLength = 2.0f;
|
|
float currentTime = 0.0f;
|
|
float playSpeed = 1.0f;
|
|
bool isPlaying = false;
|
|
bool loop = true;
|
|
bool applyOnScrub = true;
|
|
|
|
glm::vec3 lerpVec3(const glm::vec3& a, const glm::vec3& b, float t) {
|
|
return a + (b - a) * t;
|
|
}
|
|
|
|
float clampFloat(float value, float minValue, float maxValue) {
|
|
return std::max(minValue, std::min(value, maxValue));
|
|
}
|
|
|
|
SceneObject* resolveTarget(ScriptContext& ctx) {
|
|
if (targetId >= 0) {
|
|
if (auto* obj = ctx.FindObjectById(targetId)) {
|
|
return obj;
|
|
}
|
|
}
|
|
if (targetName[0] != '\0') {
|
|
if (auto* obj = ctx.FindObjectByName(targetName)) {
|
|
targetId = obj->id;
|
|
return obj;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void syncTargetLabel(SceneObject* obj) {
|
|
if (!obj) return;
|
|
strncpy(targetName, obj->name.c_str(), sizeof(targetName) - 1);
|
|
targetName[sizeof(targetName) - 1] = '\0';
|
|
}
|
|
|
|
void captureKeyframe(SceneObject& obj, float time) {
|
|
float clamped = clampFloat(time, 0.0f, clipLength);
|
|
auto it = std::find_if(keyframes.begin(), keyframes.end(),
|
|
[&](const Keyframe& k) { return std::abs(k.time - clamped) < 0.0001f; });
|
|
if (it == keyframes.end()) {
|
|
keyframes.push_back(Keyframe{clamped, obj.position, obj.rotation, obj.scale});
|
|
} else {
|
|
it->position = obj.position;
|
|
it->rotation = obj.rotation;
|
|
it->scale = obj.scale;
|
|
}
|
|
std::sort(keyframes.begin(), keyframes.end(),
|
|
[](const Keyframe& a, const Keyframe& b) { return a.time < b.time; });
|
|
}
|
|
|
|
void deleteKeyframe(int index) {
|
|
if (index < 0 || index >= static_cast<int>(keyframes.size())) return;
|
|
keyframes.erase(keyframes.begin() + index);
|
|
if (selectedKey == index) selectedKey = -1;
|
|
if (selectedKey > index) selectedKey--;
|
|
}
|
|
|
|
void applyPoseAtTime(ScriptContext& ctx, SceneObject& obj, float time) {
|
|
if (keyframes.empty()) return;
|
|
|
|
if (time <= keyframes.front().time) {
|
|
ctx.SetPosition(keyframes.front().position);
|
|
ctx.SetRotation(keyframes.front().rotation);
|
|
ctx.SetScale(keyframes.front().scale);
|
|
ctx.MarkDirty();
|
|
return;
|
|
}
|
|
if (time >= keyframes.back().time) {
|
|
ctx.SetPosition(keyframes.back().position);
|
|
ctx.SetRotation(keyframes.back().rotation);
|
|
ctx.SetScale(keyframes.back().scale);
|
|
ctx.MarkDirty();
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0; i + 1 < keyframes.size(); ++i) {
|
|
const Keyframe& a = keyframes[i];
|
|
const Keyframe& b = keyframes[i + 1];
|
|
if (time >= a.time && time <= b.time) {
|
|
float span = b.time - a.time;
|
|
float t = (span > 0.0f) ? (time - a.time) / span : 0.0f;
|
|
ctx.SetPosition(lerpVec3(a.position, b.position, t));
|
|
ctx.SetRotation(lerpVec3(a.rotation, b.rotation, t));
|
|
ctx.SetScale(lerpVec3(a.scale, b.scale, t));
|
|
ctx.MarkDirty();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void drawTimeline(float& time, float length, int& selection) {
|
|
ImVec2 size = ImVec2(ImGui::GetContentRegionAvail().x, 70.0f);
|
|
ImVec2 start = ImGui::GetCursorScreenPos();
|
|
ImGui::InvisibleButton("Timeline", size);
|
|
|
|
ImDrawList* draw = ImGui::GetWindowDrawList();
|
|
ImU32 bg = ImGui::GetColorU32(ImGuiCol_FrameBg);
|
|
ImU32 border = ImGui::GetColorU32(ImGuiCol_Border);
|
|
ImU32 accent = ImGui::GetColorU32(ImGuiCol_CheckMark);
|
|
ImU32 keyColor = ImGui::GetColorU32(ImGuiCol_SliderGrab);
|
|
|
|
draw->AddRectFilled(start, ImVec2(start.x + size.x, start.y + size.y), bg, 6.0f);
|
|
draw->AddRect(start, ImVec2(start.x + size.x, start.y + size.y), border, 6.0f);
|
|
|
|
float clamped = clampFloat(time, 0.0f, length);
|
|
float playheadX = start.x + (length > 0.0f ? (clamped / length) * size.x : 0.0f);
|
|
draw->AddLine(ImVec2(playheadX, start.y), ImVec2(playheadX, start.y + size.y), accent, 2.0f);
|
|
|
|
for (size_t i = 0; i < keyframes.size(); ++i) {
|
|
float keyX = start.x + (length > 0.0f ? (keyframes[i].time / length) * size.x : 0.0f);
|
|
ImVec2 center(keyX, start.y + size.y * 0.5f);
|
|
float radius = (selection == static_cast<int>(i)) ? 6.0f : 4.5f;
|
|
draw->AddCircleFilled(center, radius, keyColor);
|
|
ImRect hit(ImVec2(center.x - 7.0f, center.y - 7.0f), ImVec2(center.x + 7.0f, center.y + 7.0f));
|
|
if (ImGui::IsMouseHoveringRect(hit.Min, hit.Max) && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
|
selection = static_cast<int>(i);
|
|
time = keyframes[i].time;
|
|
}
|
|
}
|
|
|
|
if (ImGui::IsItemActive() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
|
float mouseX = ImGui::GetIO().MousePos.x;
|
|
float t = (mouseX - start.x) / size.x;
|
|
time = clampFloat(t * length, 0.0f, length);
|
|
}
|
|
}
|
|
|
|
void drawKeyframeTable() {
|
|
if (keyframes.empty()) {
|
|
ImGui::TextDisabled("No keyframes yet.");
|
|
return;
|
|
}
|
|
|
|
if (ImGui::BeginTable("KeyframeTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) {
|
|
ImGui::TableSetupColumn("Time");
|
|
ImGui::TableSetupColumn("Position");
|
|
ImGui::TableSetupColumn("Rotation");
|
|
ImGui::TableSetupColumn("Scale");
|
|
ImGui::TableHeadersRow();
|
|
|
|
for (size_t i = 0; i < keyframes.size(); ++i) {
|
|
const auto& key = keyframes[i];
|
|
ImGui::TableNextRow();
|
|
ImGui::TableNextColumn();
|
|
bool selected = selectedKey == static_cast<int>(i);
|
|
std::string label = std::to_string(key.time);
|
|
if (ImGui::Selectable(label.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) {
|
|
selectedKey = static_cast<int>(i);
|
|
currentTime = key.time;
|
|
}
|
|
ImGui::TableNextColumn();
|
|
ImGui::Text("%.2f, %.2f, %.2f", key.position.x, key.position.y, key.position.z);
|
|
ImGui::TableNextColumn();
|
|
ImGui::Text("%.2f, %.2f, %.2f", key.rotation.x, key.rotation.y, key.rotation.z);
|
|
ImGui::TableNextColumn();
|
|
ImGui::Text("%.2f, %.2f, %.2f", key.scale.x, key.scale.y, key.scale.z);
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
extern "C" void RenderEditorWindow(ScriptContext& ctx) {
|
|
ImGui::TextUnformatted("Simple Animation");
|
|
ImGui::Separator();
|
|
|
|
SceneObject* selectedObj = ctx.object;
|
|
SceneObject* targetObj = resolveTarget(ctx);
|
|
|
|
ImGui::TextDisabled("Select a GameObject to animate:");
|
|
ImGui::BeginDisabled();
|
|
ImGui::InputText("##TargetName", targetName, sizeof(targetName));
|
|
ImGui::EndDisabled();
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Use Selected") && selectedObj) {
|
|
targetId = selectedObj->id;
|
|
syncTargetLabel(selectedObj);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Clear")) {
|
|
targetId = -1;
|
|
targetName[0] = '\0';
|
|
targetObj = nullptr;
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
if (ImGui::BeginTabBar("AnimModeTabs")) {
|
|
if (ImGui::BeginTabItem("Pose Mode")) {
|
|
ImGui::TextDisabled("Pose Editor");
|
|
ImGui::Separator();
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f);
|
|
if (ImGui::Button("Key")) {
|
|
if (targetObj) captureKeyframe(*targetObj, currentTime);
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Delete") && selectedKey >= 0) {
|
|
deleteKeyframe(selectedKey);
|
|
}
|
|
ImGui::PopStyleVar();
|
|
|
|
ImGui::Spacing();
|
|
drawTimeline(currentTime, clipLength, selectedKey);
|
|
ImGui::SliderFloat("Time", ¤tTime, 0.0f, clipLength, "%.2fs");
|
|
|
|
ImGui::Spacing();
|
|
drawKeyframeTable();
|
|
|
|
ImGui::EndTabItem();
|
|
}
|
|
if (ImGui::BeginTabItem("Config Mode")) {
|
|
ImGui::TextDisabled("Playback");
|
|
ImGui::Separator();
|
|
ImGui::Checkbox("Loop", &loop);
|
|
ImGui::Checkbox("Apply On Scrub", &applyOnScrub);
|
|
ImGui::SliderFloat("Length", &clipLength, 0.1f, 20.0f, "%.2fs");
|
|
ImGui::SliderFloat("Speed", &playSpeed, 0.1f, 4.0f, "%.2fx");
|
|
ImGui::EndTabItem();
|
|
}
|
|
ImGui::EndTabBar();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::TextDisabled("Transport");
|
|
if (ImGui::Button(isPlaying ? "Pause" : "Play")) {
|
|
isPlaying = !isPlaying;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Stop")) {
|
|
isPlaying = false;
|
|
currentTime = 0.0f;
|
|
}
|
|
|
|
if (targetObj) {
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("Target: %s", targetObj->name.c_str());
|
|
} else {
|
|
ImGui::TextDisabled("No target selected.");
|
|
}
|
|
|
|
if (isPlaying && clipLength > 0.0f) {
|
|
currentTime += ImGui::GetIO().DeltaTime * playSpeed;
|
|
if (currentTime > clipLength) {
|
|
if (loop) currentTime = std::fmod(currentTime, clipLength);
|
|
else {
|
|
currentTime = clipLength;
|
|
isPlaying = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (targetObj && (isPlaying || applyOnScrub)) {
|
|
applyPoseAtTime(ctx, *targetObj, currentTime);
|
|
}
|
|
}
|
|
|
|
extern "C" void ExitRenderEditorWindow(ScriptContext& ctx) {
|
|
(void)ctx;
|
|
}
|