Files
SpaghettiKart/src/port/ui/TrackProperties.cpp
T
quarrel07 8629bec320 Fix the warning-flagged real bugs across the codebase (#691, 1 of 3) (#725)
* menu_items: fix the real-bug-tier compiler warnings (#691 batch 1)

Six fixes, all in menu_items.c, all verified by menu playtest on macOS:

- BSWAP16(*color0++) advanced the pointer twice per pixel on little-endian
  builds (the macro evaluates its argument twice) and mixed bytes from two
  different pixels. Read first, increment separately. Note: this function
  (func_8009A9FC) currently has no callers, so the corruption was latent,
  not live.
- func_8009B9D0 fell off the end on a lookup miss, returning garbage (the
  existing code comment already suspected this). The miss path now returns
  the display list head unchanged, i.e. draws nothing.
- update_ok_menu_item read an uninitialized stack slot for unknown menu
  item types; now selects an explicit no-animation value, matching what
  the garbage read did in practice.
- pause_menu_item_box_cursor: removed the three empty 'if (x2);' matching
  artifacts. The x/y/z spin state itself is untouched.
- func_800A1FB0: initialized var_s4/var_s5 against the guarded-but-warned
  switch default.
- func_800A54EC: initialized the pause cursor position pointer against its
  unreachable mode-switch default (would have been a null-deref class bug
  if mode values ever grew).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* collision: define the G_ENDDL opcode shifts, return 0 when no tyre surface found

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* replays: cast staff-ghost pointer comparisons, return 0 for empty ghost buffer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* update_objects: return 0 from conditional step helpers, fix TLUT pointer comparison, init train draw distances

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* render_player: remove the impossible lamp range (vanilla bug, never glowed on N64 either)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* math_util_2: return the vector, not the address of the parameter slot

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Crab: remove inner declaration shadowing the initialized objectIndex

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* editor: honor InverseMatrix failure (bool was compared against 2, always true)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Track.h: drop dead null checks on array members

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* shells: drop always-true angle and surface gates (behavior unchanged, s16 made them tautological)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* particles: make the no-return particle setters void

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* effects/stubs/skybox/main: align signatures with functions that return nothing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* menus/save: type the pak status variable as s32, return BAD_READ on the fall-off path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* audio: remove uninitialized-read matching artifacts, init isSound

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* code_80005FD0/code_80086E70: init dead bomb kart pointer and the no_init variable

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* port/engine UI: fix printf-style format types and non-literal format string

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* render_objects: feed the unused texture param to the uninitialized img walker

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Format the changed lines per .clang-format

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address review: strip explanatory comments, drop vec3f pointer returns

Comments moved to the PR record; the one flagged worth keeping stays.
vec3f_set_xyz/normalize/cross_product return void now since the out
argument is the interface and nothing used the pointer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Update actors_extended.c

* Add checkbox for 'Shells Shoot Straight' option

* Update render_player.c

* Update PortMenu.cpp

* Update render_player.c

* Update actors_extended.c

* Update render_player.c

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: MegaMech <MegaMech@users.noreply.github.com>
2026-08-01 22:29:56 -06:00

550 lines
22 KiB
C++

#include "TrackProperties.h"
#include "port/ui/PortMenu.h"
#include "UIWidgets.h"
#include "ship/Context.h"
#include <imgui.h>
#include <map>
#include <libultraship/libultraship.h>
#include <spdlog/fmt/fmt.h>
#include "spdlog/formatter.h"
#include <common_structs.h>
#include <defines.h>
#include "port/Game.h"
#include "engine/cameras/TourCamera.h"
#include "engine/TrackBrowser.h"
#include "engine/editor/SceneManager.h"
#include "engine/registry/Registry.h"
extern "C" {
#include "code_800029B0.h"
#include "sounds.h"
#include "audio/external.h"
#include "racing/render_courses.h"
#include "render_objects.h"
}
namespace TrackEditor {
TrackPropertiesWindow::~TrackPropertiesWindow() {
SPDLOG_TRACE("destruct track properties window");
}
void TrackPropertiesWindow::DrawElement() {
if (nullptr == GetWorld()->GetTrack()) {
return;
}
if (ImGui::Button("Edit TrackInfo")) {
ImGui::OpenPopup("Edit TrackInfo");
}
DrawResourceNameEdit();
ImGui::InputFloat("Water Level", &GetWorld()->GetTrack()->Props.WaterLevel);
if (ImGui::CollapsingHeader("Camera")) {
ImGui::InputFloat("Near Perspective", &GetWorld()->GetTrack()->Props.NearPersp);
ImGui::InputFloat("Far Perspective", &GetWorld()->GetTrack()->Props.FarPersp);
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::Text("Controls the far clipping distance for perspective rendering.");
ImGui::Text("Disable culling enhancement needs to be off to see a difference.");
ImGui::EndTooltip();
}
}
if (ImGui::CollapsingHeader("Environment")) {
TrackPropertiesWindow::DrawFog();
TrackPropertiesWindow::DrawLight();
}
if (ImGui::CollapsingHeader("AI")) {
ImGui::InputFloat("AI Max Separation", &GetWorld()->GetTrack()->Props.AIMaximumSeparation);
ImGui::InputFloat("AI Min Separation", &GetWorld()->GetTrack()->Props.AIMinimumSeparation);
ImGui::InputInt("AI Steering Sensitivity", (int*)&GetWorld()->GetTrack()->Props.AISteeringSensitivity);
ImGui::Separator();
for (size_t i = 0; i < 32; i++) {
ImGui::InputScalar(("Element " + std::to_string(i)).c_str(), ImGuiDataType_S16, &GetWorld()->GetTrack()->Props.AIDistance[i]);
}
}
if (ImGui::CollapsingHeader("Random Junk")) {
for (size_t i = 0; i < 4; i++) {
ImGui::InputFloat(fmt::format("CurveTargetSpeed[{}]", i).c_str(), &GetWorld()->GetTrack()->Props.CurveTargetSpeed[i]);
}
ImGui::Separator();
for (size_t i = 0; i < 4; i++) {
ImGui::InputFloat(fmt::format("NormalTargetSpeed[{}]", i).c_str(), &GetWorld()->GetTrack()->Props.NormalTargetSpeed[i]);
}
ImGui::Separator();
for (size_t i = 0; i < 4; i++) {
ImGui::InputFloat(fmt::format("D_0D0096B8[{}]", i).c_str(), &GetWorld()->GetTrack()->Props.D_0D0096B8[i]);
}
ImGui::Separator();
for (size_t i = 0; i < 4; i++) {
ImGui::InputFloat(fmt::format("OffTrackTargetSpeed[{}]", i).c_str(), &GetWorld()->GetTrack()->Props.OffTrackTargetSpeed[i]);
}
}
float minimapColour[3];
RGB8ToFloat((u8*)&GetWorld()->GetTrack()->Props.Minimap.Colour, minimapColour);
if (ImGui::CollapsingHeader("Minimap")) {
ImGui::Text("Position");
ImGui::SameLine();
if (ImGui::DragInt2("##MinimapPosition", &GetWorld()->GetTrack()->Props.Minimap.Pos[0].X, 1.0f)) {
}
ImGui::Text("P2 Position");
ImGui::SameLine();
if (ImGui::DragInt2("##MinimapPosition2p", &GetWorld()->GetTrack()->Props.Minimap.Pos[1].X, 1.0f)) {
}
ImGui::Text("Player Markers");
ImGui::SameLine();
if (ImGui::DragInt2("##MinimapPlayers", &GetWorld()->GetTrack()->Props.Minimap.PlayerX, 1.0f)) {
}
ImGui::Text("Player Scale Factor");
ImGui::SameLine();
if (ImGui::DragFloat("##MinimapScaleFactor", &GetWorld()->GetTrack()->Props.Minimap.PlayerScaleFactor, 0.0001f)) {
}
ImGui::Text("Finishline");
ImGui::SameLine();
ImGui::DragFloat2("##MinimapFinishlineX", &GetWorld()->GetTrack()->Props.Minimap.FinishlineX, 1.0f);
ImGui::Text("Colour");
ImGui::SameLine();
ImGui::ColorEdit3("##MinimapColour", minimapColour, 1.0f);
}
FloatToRGB8(minimapColour, (u8*)&GetWorld()->GetTrack()->Props.Minimap.Colour);
// Convert and pass to ImGui ColorEdit3
float topRight[3], bottomRight[3], bottomLeft[3], topLeft[3];
float floorTopRight[3], floorBottomRight[3], floorBottomLeft[3], floorTopLeft[3];
// Convert RGB8 (0-255) to float (0.0f to 1.0f)
RGB8ToFloat((u8*)&GetWorld()->GetTrack()->Props.Skybox.TopRight, topRight);
RGB8ToFloat((u8*)&GetWorld()->GetTrack()->Props.Skybox.BottomRight, bottomRight);
RGB8ToFloat((u8*)&GetWorld()->GetTrack()->Props.Skybox.BottomLeft, bottomLeft);
RGB8ToFloat((u8*)&GetWorld()->GetTrack()->Props.Skybox.TopLeft, topLeft);
RGB8ToFloat((u8*)&GetWorld()->GetTrack()->Props.Skybox.FloorTopRight, floorTopRight);
RGB8ToFloat((u8*)&GetWorld()->GetTrack()->Props.Skybox.FloorBottomRight, floorBottomRight);
RGB8ToFloat((u8*)&GetWorld()->GetTrack()->Props.Skybox.FloorBottomLeft, floorBottomLeft);
RGB8ToFloat((u8*)&GetWorld()->GetTrack()->Props.Skybox.FloorTopLeft, floorTopLeft);
if (ImGui::CollapsingHeader("Skybox")) {
ImGui::ColorEdit3("Skybox Top Right", topRight);
ImGui::ColorEdit3("Skybox Bottom Right", bottomRight);
ImGui::ColorEdit3("Skybox Bottom Left", bottomLeft);
ImGui::ColorEdit3("Skybox Top Left", topLeft);
ImGui::ColorEdit3("Skybox Floor Top Right", floorTopRight);
ImGui::ColorEdit3("Skybox Floor Bottom Right", floorBottomRight);
ImGui::ColorEdit3("Skybox Floor Bottom Left", floorBottomLeft);
ImGui::ColorEdit3("Skybox Floor Top Left", floorTopLeft);
}
// Convert the modified float values back to RGB8 (0-255)
FloatToRGB8(topRight, (u8*)&GetWorld()->GetTrack()->Props.Skybox.TopRight);
FloatToRGB8(bottomRight, (u8*)&GetWorld()->GetTrack()->Props.Skybox.BottomRight);
FloatToRGB8(bottomLeft, (u8*)&GetWorld()->GetTrack()->Props.Skybox.BottomLeft);
FloatToRGB8(topLeft, (u8*)&GetWorld()->GetTrack()->Props.Skybox.TopLeft);
FloatToRGB8(floorTopRight, (u8*)&GetWorld()->GetTrack()->Props.Skybox.FloorTopRight);
FloatToRGB8(floorBottomRight, (u8*)&GetWorld()->GetTrack()->Props.Skybox.FloorBottomRight);
FloatToRGB8(floorBottomLeft, (u8*)&GetWorld()->GetTrack()->Props.Skybox.FloorBottomLeft);
FloatToRGB8(floorTopLeft, (u8*)&GetWorld()->GetTrack()->Props.Skybox.FloorTopLeft);
TrackPropertiesWindow::DrawMusic();
TrackPropertiesWindow::DrawTourCamera();
}
void TrackPropertiesWindow::DrawResourceNameEdit() {
Track* track = GetWorld()->GetTrack();
if (!track) {
return;
}
static char resourceNameBuffer[128] = {};
static char nameBuffer[128] = "blank_track";
static char debugNameBuffer[128] = "blanktrack";
static char lengthBuffer[128] = "100m";
static bool initialized = false;
static std::string oldResourceName = track->ResourceName;
// Auto-sizing fills the height of the screen for a single frame.
// Because there's no content in the window in the first frame.
// This forces the window size to prevent that
ImGui::SetNextWindowSize(ImVec2(400, 275), ImGuiCond_Always);
if (ImGui::BeginPopupModal("Edit TrackInfo", nullptr,
ImGuiWindowFlags_AlwaysAutoResize)) {
// Initialize once per popup open
if (!initialized) {
strncpy(resourceNameBuffer, track->ResourceName.c_str(), sizeof(resourceNameBuffer));
resourceNameBuffer[sizeof(resourceNameBuffer) - 1] = '\0';
oldResourceName = track->ResourceName;
initialized = true;
}
ImGui::TextWrapped(
"Changing these fields will:\n"
"- Save the current track\n"
"- Reload the track\n"
"- Update the registry\n\n"
);
gEditor.Pause();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::InputText("ResourceName", resourceNameBuffer, IM_ARRAYSIZE(resourceNameBuffer));
ImGui::InputText("Name", GetWorld()->GetTrack()->Props.Name, IM_ARRAYSIZE(nameBuffer));
ImGui::InputText("Debug Name", GetWorld()->GetTrack()->Props.DebugName, IM_ARRAYSIZE(debugNameBuffer));
ImGui::InputText("Track Length", GetWorld()->GetTrack()->Props.TrackLength, IM_ARRAYSIZE(lengthBuffer));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
bool cancel = ImGui::Button("Cancel", ImVec2(120, 0));
ImGui::SameLine();
bool confirm = ImGui::Button("Confirm", ImVec2(120, 0));
if (confirm) {
if (oldResourceName != resourceNameBuffer) {
track->ResourceName = resourceNameBuffer;
const TrackInfo* oldInfo = gTrackRegistry.GetInfo(oldResourceName);
TrackInfo info;
info.ResourceName = track->ResourceName;
info.Name = track->Props.Name;
info.DebugName = track->Props.DebugName;
info.Path = oldInfo->Path;
TrackEditor::SaveLevel(track, static_cast<const TrackInfo*>(&info));
auto archive = track->Archive;
gTrackRegistry.Remove(oldResourceName);
gTrackRegistry.Add(info, [info, archive]() {
auto track = std::make_unique<Track>();
track->Archive = archive;
track->ResourceName = info.ResourceName;
GetWorld()->SetCurrentTrack(std::move(track));
});
TrackBrowser::Instance->Refresh(gTrackRegistry);
gGotoMode = RACING;
}
initialized = false;
ImGui::CloseCurrentPopup();
}
if (cancel) {
initialized = false;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
void TrackPropertiesWindow::DrawMusic() {
const char* items[] = {
"None", "Title Screen", "Main Menu", "Wario Stadium", "Moo Moo Farm",
"Choco Mountain", "Koopa Troopa Beach", "Banshee Boardwalk", "Frappe Snowland", "Bowser's Castle",
"Kalimari Desert", "Start Grid GP/VS", "Final Lap Fanfare", "Finish 1st Place", "Finish 2nd-4th Place",
"Finish 5th-8th Place", "16", "Star Jingle", "Rainbow Road", "DK Jungle", "Game Over", "Toad's Turnpike",
"Start Grid Time Attack", "VS Battle Results", "Losing Results", "Battle Arenas", "Award Ceremony Buildup",
"Award Ceremony 1st-3rd", "Staff Roll", "Award Ceremony 4th-8th", "Luigi Raceway", "Mario Raceway",
"Royal Raceway", "Yoshi Valley", "Block Fort", "Double Deck"
};
const char* currentItem = MusicSeqToString(GetWorld()->GetTrack()->Props.Sequence); // Get the current selected value's string
if (ImGui::BeginCombo("Music Sequence", currentItem)) {
for (size_t i = 0; i < IM_ARRAYSIZE(items); ++i) {
bool isSelected = (currentItem == items[i]);
if (ImGui::Selectable(items[i], isSelected)) {
// Update the sequence when an option is selected
GetWorld()->GetTrack()->Props.Sequence = static_cast<MusicSeq>(i);
play_sequence(GetWorld()->GetTrack()->Props.Sequence); // Call play_sequence with the updated sequence
// Update currentItem after selection is made
currentItem = items[i];
}
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
}
// Enum to string conversion
const char* TrackPropertiesWindow::MusicSeqToString(MusicSeq seq) {
switch (seq) {
case MUSIC_SEQ_SOUND_PLAYER: return "Sound FX Player (Don't use)";
case MUSIC_SEQ_TITLE_SCREEN: return "Title Screen";
case MUSIC_SEQ_MAIN_MENU: return "Main Menu";
case MUSIC_SEQ_WARIO_STADIUM: return "Wario Stadium";
case MUSIC_SEQ_MOO_MOO_FARM: return "Moo Moo Farm";
case MUSIC_SEQ_CHOCO_MOUNTAIN: return "Choco Mountain";
case MUSIC_SEQ_KOOPA_TROOPA_BEACH: return "Koopa Troopa Beach";
case MUSIC_SEQ_BANSHEE_BOARDWALK: return "Banshee Boardwalk";
case MUSIC_SEQ_FRAPPE_SNOWLAND: return "Frappe Snowland";
case MUSIC_SEQ_BOWSERS_CASTLE: return "Bowser's Castle";
case MUSIC_SEQ_KALIMARI_DESERT: return "Kalimari Desert";
case MUSIC_SEQ_START_GRID_GP_VS: return "Start Grid GP/VS";
case MUSIC_SEQ_FINAL_LAP_FANFARE: return "Final Lap Fanfare";
case MUSIC_SEQ_FINISH_1ST_PLACE: return "Finish 1st Place";
case MUSIC_SEQ_FINISH_2ND_4TH_PLACE: return "Finish 2nd-4th Place";
case MUSIC_SEQ_FINISH_5TH_8TH_PLACE: return "Finish 5th-8th Place";
case MUSIC_SEQ_WINNING_RESULTS: return "Winning Results";
case MUSIC_SEQ_STAR_JINGLE: return "Star Jingle";
case MUSIC_SEQ_RAINBOW_ROAD: return "Rainbow Road";
case MUSIC_SEQ_DK_JUNGLE: return "DK Jungle";
case MUSIC_SEQ_GAME_OVER: return "Game Over";
case MUSIC_SEQ_TOADS_TURNPIKE: return "Toad's Turnpike";
case MUSIC_SEQ_START_GRID_TIME_ATTACK: return "Start Grid Time Attack";
case MUSIC_SEQ_VS_BATTLE_RESULTS: return "VS Battle Results";
case MUSIC_SEQ_LOSING_RESULTS: return "Losing Results";
case MUSIC_SEQ_BATTLE_ARENAS: return "Battle Arenas";
case MUSIC_SEQ_AWARD_CEREMONY_BUILDUP: return "Award Ceremony Buildup";
case MUSIC_SEQ_AWARD_CEREMONY_1ST_3RD: return "Award Ceremony 1st-3rd";
case MUSIC_SEQ_STAFF_ROLL: return "Staff Roll";
case MUSIC_SEQ_AWARD_CEREMONY_4TH_8TH: return "Award Ceremony 4th-8th";
case MUSIC_SEQ_LUIGI_RACEWAY: return "Luigi Raceway";
case MUSIC_SEQ_MARIO_RACEWAY: return "Mario Raceway";
case MUSIC_SEQ_ROYAL_RACEWAY: return "Royal Raceway";
case MUSIC_SEQ_YOSHI_VALLEY: return "Yoshi Valley";
case MUSIC_SEQ_BLOCK_FORT: return "Block Fort";
case MUSIC_SEQ_DOUBLE_DECK: return "Double Deck";
default: return "None";
}
}
void TrackPropertiesWindow::DrawFog() {
if (ImGui::CollapsingHeader("Fog")) {
ImGui::Checkbox("Enable Fog", &bFog);
float colours[4];
// Convert rgba to floats
RGB8ToFloat((u8*)&gFogColour, colours);
colours[3] = gFogColour.a / 255.0f;
// Edit the ambient RGB colour
ImGui::ColorEdit4("Fog Colour", colours);
// Convert floats to rgba
FloatToRGB8(colours, (u8*)&gFogColour);
gFogColour.a = static_cast<u8>(colours[3] * 255.0f);
// Fog near and far planes
int val[2] = {static_cast<int>(gFogMin), static_cast<int>(gFogMax)};
ImGui::DragInt2("##MinimapPosition", &val[0], 1.0f, 0, 1000);
if (val[0] >= val[1]) {
val[0] = val[1] - 1;
}
// Clamp to allowed range just in case
val[0] = std::clamp(val[0], 0, 999);
val[1] = std::clamp(val[1], 1, 1000);
gFogMin = static_cast<int16_t>(val[0]);
gFogMax = static_cast<int16_t>(val[1]);
}
}
void TrackPropertiesWindow::DrawLight() {
// Convert and pass to ImGui ColorEdit3
for (size_t i = 0; i < 2; ++i) {
float ambient[3], diffuse[3], direction[3];
RGB8ToFloat((u8*)&D_800DC610[i].a.l.col, ambient);
RGB8ToFloat((u8*)&D_800DC610[i].l->l.col, diffuse);
RGB8ToFloat((u8*)&D_800DC610[i].l->l.dir, direction);
// Edit the ambient RGB colour
ImGui::Text("Light %zu - Ambient Colour", i + 1);
ImGui::ColorEdit3(("Ambient Colour " + std::to_string(i)).c_str(), ambient); // Modify ambient colour
// Edit the diffuse RGB colour
ImGui::Text("Light %zu - Diffuse Colour", i + 1);
ImGui::ColorEdit3(("Diffuse Colour " + std::to_string(i)).c_str(), diffuse); // Modify diffuse colour
// Edit the direction RGB colour (this could be represented as a direction vector)
ImGui::Text("Light %zu - Direction", i + 1);
ImGui::ColorEdit3(("Direction Colour " + std::to_string(i)).c_str(), direction); // Modify direction vector colour
FloatToRGB8(ambient, (u8*)&D_800DC610[i].a.l.col);
FloatToRGB8(ambient, (u8*)&D_800DC610[i].a.l.colc);
FloatToRGB8(diffuse, (u8*)&D_800DC610[i].l->l.col);
FloatToRGB8(diffuse, (u8*)&D_800DC610[i].l->l.colc);
FloatToRGB8(direction, (u8*)&D_800DC610[i].l->l.dir);
}
}
// Convert s16 colour values to float (normalized to [0, 1] range)
void TrackPropertiesWindow::RGB8ToFloat(const u8* src, float* dst) {
for (size_t i = 0; i < 3; ++i) {
dst[i] = src[i] / 255.0f; // Normalize to the range [0.0f, 1.0f]
}
}
void TrackPropertiesWindow::FloatToRGB8(const float* src, u8* dst) {
for (size_t i = 0; i < 3; ++i) {
dst[i] = static_cast<u8>(src[i] * 255.0f); // Scale to [0, 255] range
}
}
int32_t TrackPropertiesWindow::SelectedShot = -1;
int32_t TrackPropertiesWindow::SelectedKeyframe = -1;
void TrackPropertiesWindow::DrawTourCamera() {
Track* track = GetWorld()->GetTrack();
if (nullptr == track) {
return;
}
Camera* camera = gScreenOneCtx->camera;
if (nullptr == camera) {
return;
}
FVector camPos = FVector(camera->pos[0], camera->pos[1], camera->pos[2]);
FVector camLookAt = FVector(camera->lookAt[0], camera->lookAt[1], camera->lookAt[2]);
// Enable / disable
ImGui::Checkbox("Enable Tour Camera", &track->bTourEnabled);
if (!track->bTourEnabled) {
return;
}
ImGui::SeparatorText("Tour Camera Controls");
// Button to add a new empty shot
if (ImGui::Button("Add CameraShot")) {
TourCamera::CameraShot shot;
shot.Pos = camPos; // start where the camera currently is
shot.LookAt = camLookAt; // or however your camera exposes these
track->TourShots.push_back(shot);
}
ImGui::Spacing();
// ============================
// Display Camera Shots
// ============================
for (int32_t i = 0; i < track->TourShots.size(); i++)
{
TourCamera::CameraShot& shot = track->TourShots[i];
ImGui::PushID(i);
bool open = ImGui::CollapsingHeader(
("CameraShot " + std::to_string(i)).c_str(),
ImGuiTreeNodeFlags_DefaultOpen
);
// Select the shot
if (ImGui::Selectable("Select Shot", SelectedShot == i)) {
SelectedShot = i;
}
// Delete shot
ImGui::SameLine();
if (ImGui::Button("Delete Shot")) {
track->TourShots.erase(track->TourShots.begin() + i);
if (SelectedShot == i) SelectedShot = -1;
ImGui::PopID();
break;
}
if (open) {
ImGui::Indent();
// Start pos / lookAt editing
ImGui::InputFloat3("Start Pos", &shot.Pos.x);
ImGui::InputFloat3("Start LookAt", &shot.LookAt.x);
ImGui::Spacing();
// Record keyframe from current camera
if (ImGui::Button("Record KeyFrame From Camera")) {
TourCamera::KeyFrame kf;
kf.Pos = camPos;
kf.LookAt = camLookAt;
kf.Duration = 60.0f;
shot.Frames.push_back(kf);
}
ImGui::SeparatorText("KeyFrames");
// ============================
// KeyFrame List
// ============================
for (int32_t k = 0; k < shot.Frames.size(); k++)
{
TourCamera::KeyFrame& kf = shot.Frames[k];
ImGui::PushID(k);
bool kOpen = ImGui::TreeNode(("KeyFrame " + std::to_string(k)).c_str());
// Select keyframe
if (ImGui::Selectable("Select KeyFrame", SelectedKeyframe == k))
SelectedKeyframe = k;
ImGui::SameLine();
if (ImGui::Button("Delete")) {
shot.Frames.erase(shot.Frames.begin() + k);
if (SelectedKeyframe == k) SelectedKeyframe = -1;
ImGui::PopID();
break;
}
if (kOpen) {
ImGui::InputFloat3("Position", &kf.Pos.x);
ImGui::InputFloat3("LookAt", &kf.LookAt.x);
ImGui::DragFloat("Duration", &kf.Duration, 1.0f, 1.0f, 5000.0f);
ImGui::TreePop();
}
ImGui::PopID();
}
ImGui::Unindent();
}
ImGui::PopID();
}
}
}