And there we go, Audio Clips and Audio sources! (and bugfixes lol.)

This commit is contained in:
Anemunt
2025-12-16 19:53:02 -05:00
parent 195eb73a73
commit 6ecf2a5106
14 changed files with 96633 additions and 19 deletions

View File

@@ -31,6 +31,7 @@ uniform float lightRangeArr[MAX_LIGHTS];
uniform float lightInnerCosArr[MAX_LIGHTS]; uniform float lightInnerCosArr[MAX_LIGHTS];
uniform float lightOuterCosArr[MAX_LIGHTS]; uniform float lightOuterCosArr[MAX_LIGHTS];
uniform vec2 lightAreaSizeArr[MAX_LIGHTS]; uniform vec2 lightAreaSizeArr[MAX_LIGHTS];
uniform float lightAreaFadeArr[MAX_LIGHTS];
// Single directional light controlled by hierarchy (fallback if none set) // Single directional light controlled by hierarchy (fallback if none set)
uniform vec3 lightDir = normalize(vec3(0.3, 1.0, 0.5)); uniform vec3 lightDir = normalize(vec3(0.3, 1.0, 0.5));
@@ -92,8 +93,23 @@ void main()
vec2 local; vec2 local;
local.x = dot(onPlane - center, tangent); local.x = dot(onPlane - center, tangent);
local.y = dot(onPlane - center, bitangent); local.y = dot(onPlane - center, bitangent);
vec2 clamped = clamp(local, -halfSize, halfSize);
vec3 closest = center + tangent * clamped.x + bitangent * clamped.y; float fade = clamp(lightAreaFadeArr[i], 0.0, 1.0);
vec2 absLocal = abs(local);
float edgeWeight = 1.0;
if (fade < 0.0001) {
if (absLocal.x > halfSize.x || absLocal.y > halfSize.y) continue;
} else {
vec2 inner = halfSize * (1.0 - fade);
vec2 delta = max(halfSize - inner, vec2(0.0001));
vec2 outside = max(absLocal - inner, vec2(0.0));
float maxOutside = max(outside.x / delta.x, outside.y / delta.y);
edgeWeight = 1.0 - clamp(maxOutside, 0.0, 1.0);
if (edgeWeight <= 0.0) continue;
edgeWeight = smoothstep(0.0, 1.0, edgeWeight);
}
vec3 closest = center + tangent * local.x + bitangent * local.y;
vec3 lvec = closest - FragPos; vec3 lvec = closest - FragPos;
float dist = length(lvec); float dist = length(lvec);
@@ -110,7 +126,7 @@ void main()
// Lambert against area normal for softer look // Lambert against area normal for softer look
float nl = max(dot(norm, lDirN), 0.0); float nl = max(dot(norm, lDirN), 0.0);
float facing = max(dot(n, -lDirN), 0.0); float facing = max(dot(n, -lDirN), 0.0);
attenuation *= facing; attenuation *= facing * edgeWeight;
vec3 diffuse = nl * lightColorArr[i] * intensity; vec3 diffuse = nl * lightColorArr[i] * intensity;
vec3 halfwayDir = normalize(lDirN + viewDir); vec3 halfwayDir = normalize(lDirN + viewDir);

91
Scripts/RigidbodyTest.cpp Normal file
View File

@@ -0,0 +1,91 @@
// Minimal rigidbody smoke-test script for Modularity script compilation.
// Build via the engine's "Compile Script" action (wrapper is generated automatically).
#include "ScriptRuntime.h"
#include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h"
namespace {
bool autoLaunch = true;
glm::vec3 launchVelocity = glm::vec3(0.0f, 6.0f, 8.0f);
glm::vec3 teleportOffset = glm::vec3(0.0f, 1.0f, 0.0f);
bool showVelocity = true;
void Launch(ScriptContext& ctx) {
if (ctx.HasRigidbody()) {
ctx.SetRigidbodyVelocity(launchVelocity);
} else {
ctx.AddConsoleMessage("RigidbodyTest: attach a Rigidbody component", ConsoleMessageType::Warning);
}
}
void Teleport(ScriptContext& ctx) {
if (!ctx.object) return;
const glm::vec3 targetPos = ctx.object->position + teleportOffset;
const glm::vec3 targetRot = ctx.object->rotation;
if (!ctx.TeleportRigidbody(targetPos, targetRot)) {
ctx.AddConsoleMessage("RigidbodyTest: teleport failed (needs Rigidbody)", ConsoleMessageType::Warning);
}
}
} // namespace
extern "C" void Script_OnInspector(ScriptContext& ctx) {
// Persist settings automatically between sessions.
ctx.AutoSetting("autoLaunch", autoLaunch);
ctx.AutoSetting("launchVelocity", launchVelocity);
ctx.AutoSetting("teleportOffset", teleportOffset);
ctx.AutoSetting("showVelocity", showVelocity);
ImGui::TextUnformatted("RigidbodyTest");
ImGui::Separator();
bool changed = false;
changed |= ImGui::Checkbox("Launch on Begin", &autoLaunch);
changed |= ImGui::Checkbox("Show Velocity Readback", &showVelocity);
changed |= ImGui::DragFloat3("Launch Velocity", &launchVelocity.x, 0.25f, -50.0f, 50.0f);
changed |= ImGui::DragFloat3("Teleport Offset", &teleportOffset.x, 0.1f, -10.0f, 10.0f);
if (changed) {
ctx.SaveAutoSettings();
}
if (ImGui::Button("Launch Now")) {
Launch(ctx);
}
ImGui::SameLine();
if (ImGui::Button("Zero Velocity")) {
if (!ctx.SetRigidbodyVelocity(glm::vec3(0.0f))) {
ctx.AddConsoleMessage("RigidbodyTest: zeroing velocity requires a Rigidbody", ConsoleMessageType::Warning);
}
}
if (ImGui::Button("Teleport Offset")) {
Teleport(ctx);
}
glm::vec3 currentVelocity;
if (showVelocity && ctx.GetRigidbodyVelocity(currentVelocity)) {
ImGui::Text("Velocity: (%.2f, %.2f, %.2f)", currentVelocity.x, currentVelocity.y, currentVelocity.z);
} else if (showVelocity) {
ImGui::TextDisabled("Velocity readback requires a Rigidbody");
}
if (ctx.object) {
ImGui::TextDisabled("Attached to: %s (id=%d)", ctx.object->name.c_str(), ctx.object->id);
}
}
void Begin(ScriptContext& ctx, float /*deltaTime*/) {
if (autoLaunch) {
Launch(ctx);
}
}
void Spec(ScriptContext& ctx, float /*deltaTime*/) {
}
void TestEditor(ScriptContext& ctx, float /*deltaTime*/) {
}
void TickUpdate(ScriptContext& ctx, float /*deltaTime*/) {
}

95649
include/ThirdParty/miniaudio.h vendored Normal file

File diff suppressed because it is too large Load Diff

277
src/AudioSystem.cpp Normal file
View File

@@ -0,0 +1,277 @@
#define MINIAUDIO_IMPLEMENTATION
#include "../include/ThirdParty/miniaudio.h"
#include "AudioSystem.h"
#include <cmath>
namespace {
constexpr size_t kPreviewBuckets = 800;
constexpr ma_uint32 kPreviewChunkFrames = 2048;
}
bool AudioSystem::init() {
if (initialized) return true;
ma_result res = ma_engine_init(nullptr, &engine);
if (res != MA_SUCCESS) {
std::cerr << "AudioSystem: failed to init miniaudio (" << res << ")\n";
return false;
}
initialized = true;
return true;
}
void AudioSystem::shutdown() {
stopPreview();
destroyActiveSounds();
if (initialized) {
ma_engine_uninit(&engine);
initialized = false;
}
}
void AudioSystem::destroyActiveSounds() {
for (auto& kv : activeSounds) {
ma_sound_uninit(&kv.second.sound);
}
activeSounds.clear();
}
void AudioSystem::onPlayStart(const std::vector<SceneObject>& objects) {
if (!initialized && !init()) return;
destroyActiveSounds();
for (const auto& obj : objects) {
if (!obj.enabled || !obj.hasAudioSource || obj.audioSource.clipPath.empty()) continue;
if (!obj.audioSource.enabled) continue;
if (ensureSoundFor(obj) && obj.audioSource.playOnStart) {
ma_sound_start(&activeSounds[obj.id].sound);
}
}
}
void AudioSystem::onPlayStop() {
destroyActiveSounds();
}
bool AudioSystem::ensureSoundFor(const SceneObject& obj) {
auto it = activeSounds.find(obj.id);
if (it != activeSounds.end()) {
if (it->second.clipPath == obj.audioSource.clipPath) {
refreshSoundParams(obj, it->second);
return true;
}
ma_sound_uninit(&it->second.sound);
activeSounds.erase(it);
}
if (!initialized && !init()) return false;
ActiveSound snd{};
ma_result res = ma_sound_init_from_file(
&engine,
obj.audioSource.clipPath.c_str(),
MA_SOUND_FLAG_STREAM,
nullptr,
nullptr,
&snd.sound
);
if (res != MA_SUCCESS) {
std::cerr << "AudioSystem: failed to load " << obj.audioSource.clipPath << " (" << res << ")\n";
return false;
}
snd.clipPath = obj.audioSource.clipPath;
snd.spatial = obj.audioSource.spatial;
refreshSoundParams(obj, snd);
activeSounds[obj.id] = std::move(snd);
return true;
}
void AudioSystem::refreshSoundParams(const SceneObject& obj, ActiveSound& snd) {
ma_sound_set_looping(&snd.sound, obj.audioSource.loop ? MA_TRUE : MA_FALSE);
ma_sound_set_volume(&snd.sound, obj.audioSource.volume);
ma_sound_set_spatialization_enabled(&snd.sound, obj.audioSource.spatial ? MA_TRUE : MA_FALSE);
ma_sound_set_min_distance(&snd.sound, obj.audioSource.minDistance);
ma_sound_set_max_distance(&snd.sound, obj.audioSource.maxDistance);
ma_sound_set_position(&snd.sound, obj.position.x, obj.position.y, obj.position.z);
if (!ma_sound_is_playing(&snd.sound) && obj.audioSource.playOnStart && obj.audioSource.enabled) {
ma_sound_start(&snd.sound);
}
}
void AudioSystem::update(const std::vector<SceneObject>& objects, const Camera& listenerCamera, bool playing) {
if (!initialized) return;
ma_engine_listener_set_position(&engine, 0, listenerCamera.position.x, listenerCamera.position.y, listenerCamera.position.z);
ma_engine_listener_set_direction(&engine, 0, listenerCamera.front.x, listenerCamera.front.y, listenerCamera.front.z);
ma_engine_listener_set_world_up(&engine, 0, listenerCamera.up.x, listenerCamera.up.y, listenerCamera.up.z);
if (!playing) {
destroyActiveSounds();
return;
}
std::unordered_set<int> stillPresent;
for (const auto& obj : objects) {
if (!obj.hasAudioSource) continue;
stillPresent.insert(obj.id);
auto eraseIt = activeSounds.find(obj.id);
if (!obj.enabled || !obj.audioSource.enabled || obj.audioSource.clipPath.empty()) {
if (eraseIt != activeSounds.end()) {
ma_sound_uninit(&eraseIt->second.sound);
activeSounds.erase(eraseIt);
}
continue;
}
if (ensureSoundFor(obj)) {
refreshSoundParams(obj, activeSounds[obj.id]);
}
}
for (auto it = activeSounds.begin(); it != activeSounds.end(); ) {
if (stillPresent.find(it->first) == stillPresent.end()) {
ma_sound_uninit(&it->second.sound);
it = activeSounds.erase(it);
} else {
++it;
}
}
}
bool AudioSystem::playPreview(const std::string& path, float volume) {
if (path.empty()) return false;
if (!initialized && !init()) return false;
stopPreview();
ma_result res = ma_sound_init_from_file(&engine, path.c_str(), MA_SOUND_FLAG_STREAM, nullptr, nullptr, &previewSound);
if (res != MA_SUCCESS) {
std::cerr << "AudioSystem: preview load failed for " << path << " (" << res << ")\n";
return false;
}
ma_sound_set_volume(&previewSound, volume);
ma_sound_set_spatialization_enabled(&previewSound, MA_FALSE);
previewPath = path;
previewActive = ma_sound_start(&previewSound) == MA_SUCCESS;
if (!previewActive) {
ma_sound_uninit(&previewSound);
}
return previewActive;
}
void AudioSystem::stopPreview() {
if (previewActive) {
ma_sound_stop(&previewSound);
ma_sound_uninit(&previewSound);
}
previewActive = false;
previewPath.clear();
}
bool AudioSystem::isPreviewing(const std::string& path) const {
return previewActive && previewPath == path;
}
bool AudioSystem::getPreviewTime(const std::string& path, double& cursorSeconds, double& durationSeconds) const {
if (!previewActive || previewPath != path) return false;
float cur = 0.0f;
float len = 0.0f;
if (ma_sound_get_cursor_in_seconds(&previewSound, &cur) != MA_SUCCESS) return false;
if (ma_sound_get_length_in_seconds(&previewSound, &len) != MA_SUCCESS) return false;
cursorSeconds = static_cast<double>(cur);
durationSeconds = static_cast<double>(len);
return true;
}
bool AudioSystem::seekPreview(const std::string& path, double seconds) {
if (!previewActive || previewPath != path) return false;
ma_uint32 sampleRate = 0;
if (ma_sound_get_data_format(&previewSound, nullptr, nullptr, &sampleRate, nullptr, 0) != MA_SUCCESS) {
return false;
}
float lenSec = 0.0f;
ma_sound_get_length_in_seconds(&previewSound, &lenSec);
seconds = std::clamp(seconds, 0.0, static_cast<double>(lenSec));
ma_uint64 targetFrame = static_cast<ma_uint64>(seconds * static_cast<double>(sampleRate));
ma_result res = ma_sound_seek_to_pcm_frame(&previewSound, targetFrame);
return res == MA_SUCCESS;
}
AudioClipPreview AudioSystem::loadPreview(const std::string& path) {
AudioClipPreview preview;
preview.path = path;
ma_decoder decoder;
if (ma_decoder_init_file(path.c_str(), nullptr, &decoder) != MA_SUCCESS) {
return preview;
}
ma_uint64 totalFrames = 0;
ma_decoder_get_length_in_pcm_frames(&decoder, &totalFrames);
preview.channels = decoder.outputChannels;
preview.sampleRate = decoder.outputSampleRate;
preview.durationSeconds = (preview.sampleRate > 0) ? static_cast<double>(totalFrames) / static_cast<double>(preview.sampleRate) : 0.0;
if (totalFrames == 0 || preview.channels == 0) {
ma_decoder_uninit(&decoder);
return preview;
}
const ma_uint64 framesPerBucket = std::max<ma_uint64>(1, totalFrames / kPreviewBuckets);
preview.waveform.assign(static_cast<size_t>(kPreviewBuckets), 0.0f);
std::vector<float> temp(kPreviewChunkFrames * preview.channels);
ma_uint64 frameCursor = 0;
size_t bucketIndex = 0;
ma_uint64 bucketCursor = 0;
float bucketMax = 0.0f;
while (frameCursor < totalFrames && bucketIndex < preview.waveform.size()) {
ma_uint64 framesToRead = std::min<ma_uint64>(kPreviewChunkFrames, totalFrames - frameCursor);
ma_uint64 framesRead = 0;
ma_result readResult = ma_decoder_read_pcm_frames(&decoder, temp.data(), framesToRead, &framesRead);
if (readResult != MA_SUCCESS && readResult != MA_AT_END) {
break;
}
if (framesRead == 0) break;
for (ma_uint64 f = 0; f < framesRead; ++f) {
for (ma_uint32 c = 0; c < preview.channels; ++c) {
float sample = temp[static_cast<size_t>(f * preview.channels + c)];
bucketMax = std::max(bucketMax, std::fabs(sample));
}
bucketCursor++;
frameCursor++;
if (bucketCursor >= framesPerBucket) {
if (bucketIndex < preview.waveform.size()) {
preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f);
bucketIndex++;
}
bucketCursor = 0;
bucketMax = 0.0f;
}
}
}
if (bucketIndex < preview.waveform.size() && bucketMax > 0.0f) {
preview.waveform[bucketIndex] = std::clamp(bucketMax, 0.0f, 1.0f);
}
ma_decoder_uninit(&decoder);
preview.loaded = true;
return preview;
}
const AudioClipPreview* AudioSystem::getPreview(const std::string& path) {
if (path.empty()) return nullptr;
auto it = previewCache.find(path);
if (it == previewCache.end()) {
previewCache[path] = loadPreview(path);
it = previewCache.find(path);
}
if (it != previewCache.end() && it->second.loaded) {
return &it->second;
}
return nullptr;
}

56
src/AudioSystem.h Normal file
View File

@@ -0,0 +1,56 @@
#pragma once
#include "Common.h"
#include "SceneObject.h"
#include "Camera.h"
#include "../include/ThirdParty/miniaudio.h"
#include <unordered_map>
#include <unordered_set>
struct AudioClipPreview {
bool loaded = false;
std::string path;
uint32_t channels = 0;
uint32_t sampleRate = 0;
double durationSeconds = 0.0;
std::vector<float> waveform; // Normalized 0..1 amplitude envelope for drawing
};
class AudioSystem {
public:
bool init();
void shutdown();
bool isReady() const { return initialized; }
void onPlayStart(const std::vector<SceneObject>& objects);
void onPlayStop();
void update(const std::vector<SceneObject>& objects, const Camera& listenerCamera, bool playing);
bool playPreview(const std::string& path, float volume = 1.0f);
void stopPreview();
bool isPreviewing(const std::string& path) const;
const AudioClipPreview* getPreview(const std::string& path);
bool getPreviewTime(const std::string& path, double& cursorSeconds, double& durationSeconds) const;
bool seekPreview(const std::string& path, double seconds);
private:
struct ActiveSound {
ma_sound sound;
std::string clipPath;
bool spatial = true;
};
ma_engine engine{};
bool initialized = false;
std::unordered_map<int, ActiveSound> activeSounds;
std::unordered_map<std::string, AudioClipPreview> previewCache;
ma_sound previewSound{};
bool previewActive = false;
std::string previewPath;
void destroyActiveSounds();
bool ensureSoundFor(const SceneObject& obj);
void refreshSoundParams(const SceneObject& obj, ActiveSound& snd);
AudioClipPreview loadPreview(const std::string& path);
};

View File

@@ -248,6 +248,11 @@ bool Engine::init() {
setupImGui(); setupImGui();
std::cerr << "[DEBUG] ImGui setup complete" << std::endl; std::cerr << "[DEBUG] ImGui setup complete" << std::endl;
if (!audio.init()) {
std::cerr << "[DEBUG] Audio init failed\n";
addConsoleMessage("Audio initialization failed. Audio playback will be disabled.", ConsoleMessageType::Warning);
}
logToConsole("Engine initialized - Waiting for project selection"); logToConsole("Engine initialized - Waiting for project selection");
return true; return true;
} }
@@ -325,6 +330,17 @@ void Engine::run() {
updatePlayerController(deltaTime); updatePlayerController(deltaTime);
} }
bool audioShouldPlay = isPlaying || specMode || testMode;
Camera listenerCamera = camera;
for (const auto& obj : sceneObjects) {
if (obj.type == ObjectType::Camera && obj.camera.type == SceneCameraType::Player) {
listenerCamera = makeCameraFromObject(obj);
listenerCamera.position = obj.position;
break;
}
}
audio.update(sceneObjects, listenerCamera, audioShouldPlay);
if (!showLauncher && projectManager.currentProject.isLoaded && rendererInitialized) { if (!showLauncher && projectManager.currentProject.isLoaded && rendererInitialized) {
glm::mat4 view = camera.getViewMatrix(); glm::mat4 view = camera.getViewMatrix();
float aspect = static_cast<float>(viewportWidth) / static_cast<float>(viewportHeight); float aspect = static_cast<float>(viewportWidth) / static_cast<float>(viewportHeight);
@@ -433,6 +449,8 @@ void Engine::shutdown() {
} }
physics.onPlayStop(); physics.onPlayStop();
audio.onPlayStop();
audio.shutdown();
physics.shutdown(); physics.shutdown();
ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplOpenGL3_Shutdown();
@@ -756,6 +774,7 @@ void Engine::updateScripts(float delta) {
if (sceneObjects.empty()) return; if (sceneObjects.empty()) return;
for (auto& obj : sceneObjects) { for (auto& obj : sceneObjects) {
if (!obj.enabled) continue;
for (auto& sc : obj.scripts) { for (auto& sc : obj.scripts) {
if (sc.path.empty()) continue; if (sc.path.empty()) continue;
fs::path binary = resolveScriptBinary(sc.path); fs::path binary = resolveScriptBinary(sc.path);
@@ -775,7 +794,7 @@ void Engine::updatePlayerController(float delta) {
SceneObject* player = nullptr; SceneObject* player = nullptr;
for (auto& obj : sceneObjects) { for (auto& obj : sceneObjects) {
if (obj.hasPlayerController && obj.playerController.enabled) { if (obj.enabled && obj.hasPlayerController && obj.playerController.enabled) {
player = &obj; player = &obj;
activePlayerId = obj.id; activePlayerId = obj.id;
break; break;
@@ -1112,6 +1131,8 @@ void Engine::duplicateSelected() {
newObj.collider = it->collider; newObj.collider = it->collider;
newObj.hasPlayerController = it->hasPlayerController; newObj.hasPlayerController = it->hasPlayerController;
newObj.playerController = it->playerController; newObj.playerController = it->playerController;
newObj.hasAudioSource = it->hasAudioSource;
newObj.audioSource = it->audioSource;
sceneObjects.push_back(newObj); sceneObjects.push_back(newObj);
setPrimarySelection(id); setPrimarySelection(id);

View File

@@ -10,6 +10,7 @@
#include "ScriptCompiler.h" #include "ScriptCompiler.h"
#include "ScriptRuntime.h" #include "ScriptRuntime.h"
#include "PhysicsSystem.h" #include "PhysicsSystem.h"
#include "AudioSystem.h"
#include "../include/Window/Window.h" #include "../include/Window/Window.h"
void window_size_callback(GLFWwindow* window, int width, int height); void window_size_callback(GLFWwindow* window, int width, int height);
@@ -111,6 +112,7 @@ private:
ScriptCompiler scriptCompiler; ScriptCompiler scriptCompiler;
ScriptRuntime scriptRuntime; ScriptRuntime scriptRuntime;
PhysicsSystem physics; PhysicsSystem physics;
AudioSystem audio;
bool showCompilePopup = false; bool showCompilePopup = false;
bool lastCompileSuccess = false; bool lastCompileSuccess = false;
std::string lastCompileStatus; std::string lastCompileStatus;

View File

@@ -3,6 +3,7 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <cstring> #include <cstring>
#include <cstdlib>
#include <cfloat> #include <cfloat>
#include <cmath> #include <cmath>
#include <sstream> #include <sstream>
@@ -1435,8 +1436,10 @@ void Engine::renderMainMenuBar() {
} else { } else {
addConsoleMessage("PhysX failed to initialize; physics disabled for play mode", ConsoleMessageType::Warning); addConsoleMessage("PhysX failed to initialize; physics disabled for play mode", ConsoleMessageType::Warning);
} }
audio.onPlayStart(sceneObjects);
} else { } else {
physics.onPlayStop(); physics.onPlayStop();
audio.onPlayStop();
isPaused = false; isPaused = false;
if (specMode && (physics.isReady() || physics.init())) { if (specMode && (physics.isReady() || physics.init())) {
physics.onPlayStart(sceneObjects); physics.onPlayStart(sceneObjects);
@@ -1456,8 +1459,13 @@ void Engine::renderMainMenuBar() {
} }
specMode = enable; specMode = enable;
if (!isPlaying) { if (!isPlaying) {
if (specMode) physics.onPlayStart(sceneObjects); if (specMode) {
else physics.onPlayStop(); physics.onPlayStart(sceneObjects);
audio.onPlayStart(sceneObjects);
} else {
physics.onPlayStop();
audio.onPlayStop();
}
} }
} }
@@ -1654,9 +1662,13 @@ void Engine::renderInspectorPanel() {
fs::path selectedMaterialPath; fs::path selectedMaterialPath;
bool browserHasMaterial = false; bool browserHasMaterial = false;
fs::path selectedAudioPath;
bool browserHasAudio = false;
const AudioClipPreview* selectedAudioPreview = nullptr;
if (!fileBrowser.selectedFile.empty() && fs::exists(fileBrowser.selectedFile)) { if (!fileBrowser.selectedFile.empty() && fs::exists(fileBrowser.selectedFile)) {
fs::directory_entry entry(fileBrowser.selectedFile); fs::directory_entry entry(fileBrowser.selectedFile);
if (fileBrowser.getFileCategory(entry) == FileCategory::Material) { FileCategory cat = fileBrowser.getFileCategory(entry);
if (cat == FileCategory::Material) {
selectedMaterialPath = entry.path(); selectedMaterialPath = entry.path();
browserHasMaterial = true; browserHasMaterial = true;
if (inspectedMaterialPath != selectedMaterialPath.string()) { if (inspectedMaterialPath != selectedMaterialPath.string()) {
@@ -1676,11 +1688,51 @@ void Engine::renderInspectorPanel() {
inspectedMaterialPath.clear(); inspectedMaterialPath.clear();
inspectedMaterialValid = false; inspectedMaterialValid = false;
} }
if (cat == FileCategory::Audio) {
selectedAudioPath = entry.path();
browserHasAudio = true;
selectedAudioPreview = audio.getPreview(selectedAudioPath.string());
}
} else { } else {
inspectedMaterialPath.clear(); inspectedMaterialPath.clear();
inspectedMaterialValid = false; inspectedMaterialValid = false;
} }
auto drawWaveform = [&](const char* id, const AudioClipPreview* preview, const ImVec2& size, float progressRatio, float* seekRatioOut) {
if (!preview || preview->waveform.empty()) {
ImGui::Dummy(size);
return;
}
ImVec2 start = ImGui::GetCursorScreenPos();
ImVec2 end = ImVec2(start.x + size.x, start.y + size.y);
ImGui::InvisibleButton(id, size);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(start, end, IM_COL32(30, 35, 45, 180), 4.0f);
float midY = (start.y + end.y) * 0.5f;
float usableHeight = size.y * 0.45f;
size_t count = preview->waveform.size();
float step = count > 1 ? size.x / static_cast<float>(count - 1) : size.x;
ImU32 color = IM_COL32(255, 180, 100, 200);
for (size_t i = 0; i < count; ++i) {
float amp = std::clamp(preview->waveform[i], 0.0f, 1.0f);
float x = start.x + step * static_cast<float>(i);
float yOff = amp * usableHeight;
dl->AddLine(ImVec2(x, midY - yOff), ImVec2(x, midY + yOff), color, 1.2f);
}
if (progressRatio >= 0.0f && progressRatio <= 1.0f) {
float px = start.x + progressRatio * size.x;
dl->AddLine(ImVec2(px, start.y), ImVec2(px, end.y), IM_COL32(120, 210, 255, 230), 2.0f);
}
if (seekRatioOut && ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
float mouseX = ImGui::GetIO().MousePos.x;
float ratio = (mouseX - start.x) / size.x;
ratio = std::clamp(ratio, 0.0f, 1.0f);
*seekRatioOut = ratio;
}
};
auto renderMaterialAssetPanel = [&](const char* headerTitle, bool allowApply) { auto renderMaterialAssetPanel = [&](const char* headerTitle, bool allowApply) {
if (!browserHasMaterial) return; if (!browserHasMaterial) return;
@@ -1858,9 +1910,70 @@ void Engine::renderInspectorPanel() {
ImGui::PopStyleColor(); ImGui::PopStyleColor();
}; };
auto renderAudioAssetPanel = [&](const char* headerTitle, SceneObject* target) {
if (!browserHasAudio) return;
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.5f, 0.4f, 0.25f, 1.0f));
if (ImGui::CollapsingHeader(headerTitle, ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent(8.0f);
ImGui::TextDisabled("%s", selectedAudioPath.filename().string().c_str());
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "%s", selectedAudioPath.string().c_str());
ImGui::Spacing();
if (selectedAudioPreview) {
double cur = 0.0;
double dur = 0.0;
float progress = -1.0f;
if (audio.getPreviewTime(selectedAudioPath.string(), cur, dur) && dur > 0.0001) {
progress = static_cast<float>(cur / dur);
}
ImGui::Text("Format: %u ch @ %u Hz", selectedAudioPreview->channels, selectedAudioPreview->sampleRate);
ImGui::Text("Length: %.2f s", selectedAudioPreview->durationSeconds);
ImVec2 waveSize(ImGui::GetContentRegionAvail().x, 96.0f);
float seekRatio = -1.0f;
drawWaveform("##AudioWaveAsset", selectedAudioPreview, waveSize, progress, &seekRatio);
if (seekRatio >= 0.0f && dur > 0.0) {
audio.seekPreview(selectedAudioPath.string(), seekRatio * dur);
}
if (dur > 0.0) {
ImGui::TextDisabled("Time: %0.2f / %0.2f", cur, dur);
}
} else {
ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.55f, 1.0f), "Unable to decode audio preview.");
}
ImGui::Spacing();
bool isPlayingPreview = audio.isPreviewing(selectedAudioPath.string());
if (ImGui::Button(isPlayingPreview ? "Stop" : "Play", ImVec2(72, 0))) {
if (isPlayingPreview) {
audio.stopPreview();
} else {
audio.playPreview(selectedAudioPath.string());
}
}
if (target) {
ImGui::SameLine();
if (ImGui::SmallButton("Assign to Selection")) {
if (!target->hasAudioSource) {
target->hasAudioSource = true;
target->audioSource = AudioSourceComponent{};
}
target->audioSource.clipPath = selectedAudioPath.string();
projectManager.currentProject.hasUnsavedChanges = true;
}
}
ImGui::Unindent(8.0f);
}
ImGui::PopStyleColor();
};
if (selectedObjectIds.empty()) { if (selectedObjectIds.empty()) {
if (browserHasMaterial) { if (browserHasMaterial) {
renderMaterialAssetPanel("Material Asset", true); renderMaterialAssetPanel("Material Asset", true);
} else if (browserHasAudio) {
renderAudioAssetPanel("Audio Clip", nullptr);
} else { } else {
ImGui::TextDisabled("No object selected"); ImGui::TextDisabled("No object selected");
} }
@@ -1879,6 +1992,7 @@ void Engine::renderInspectorPanel() {
} }
SceneObject& obj = *it; SceneObject& obj = *it;
ImGui::PushID(obj.id); // Scope per-object widgets to avoid ID collisions
bool addComponentButtonShown = false; bool addComponentButtonShown = false;
if (selectedObjectIds.size() > 1) { if (selectedObjectIds.size() > 1) {
@@ -1922,6 +2036,31 @@ void Engine::renderInspectorPanel() {
ImGui::Text("ID:"); ImGui::Text("ID:");
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextDisabled("%d", obj.id); ImGui::TextDisabled("%d", obj.id);
if (ImGui::Checkbox("Enabled##ObjEnabled", &obj.enabled)) {
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::Text("Layer:");
ImGui::SameLine();
int layer = obj.layer;
ImGui::SetNextItemWidth(120);
if (ImGui::SliderInt("##Layer", &layer, 0, 31)) {
obj.layer = layer;
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::SameLine();
ImGui::TextDisabled("(0-31)");
ImGui::Text("Tag:");
ImGui::SameLine();
char tagBuf[64] = {};
std::snprintf(tagBuf, sizeof(tagBuf), "%s", obj.tag.c_str());
ImGui::SetNextItemWidth(-1);
if (ImGui::InputText("##Tag", tagBuf, sizeof(tagBuf))) {
obj.tag = tagBuf;
projectManager.currentProject.hasUnsavedChanges = true;
}
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -1931,6 +2070,7 @@ void Engine::renderInspectorPanel() {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.4f, 0.5f, 0.3f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.4f, 0.5f, 0.3f, 1.0f));
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushID("Transform");
ImGui::Indent(10.0f); ImGui::Indent(10.0f);
if (obj.type == ObjectType::PostFXNode) { if (obj.type == ObjectType::PostFXNode) {
@@ -1973,6 +2113,7 @@ void Engine::renderInspectorPanel() {
} }
ImGui::Unindent(10.0f); ImGui::Unindent(10.0f);
ImGui::PopID();
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -1981,10 +2122,11 @@ void Engine::renderInspectorPanel() {
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.5f, 0.35f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.5f, 0.35f, 1.0f));
if (ImGui::CollapsingHeader("Collider", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::CollapsingHeader("Collider", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushID("Collider");
ImGui::Indent(10.0f); ImGui::Indent(10.0f);
bool changed = false; bool changed = false;
if (ImGui::Checkbox("Enabled", &obj.collider.enabled)) { if (ImGui::Checkbox("Enabled##Collider", &obj.collider.enabled)) {
changed = true; changed = true;
} }
@@ -2035,6 +2177,7 @@ void Engine::renderInspectorPanel() {
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
} }
ImGui::Unindent(10.0f); ImGui::Unindent(10.0f);
ImGui::PopID();
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
@@ -2043,10 +2186,11 @@ void Engine::renderInspectorPanel() {
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.45f, 0.7f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.45f, 0.7f, 1.0f));
if (ImGui::CollapsingHeader("Player Controller", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::CollapsingHeader("Player Controller", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushID("PlayerController");
ImGui::Indent(10.0f); ImGui::Indent(10.0f);
bool changed = false; bool changed = false;
if (ImGui::Checkbox("Enabled", &obj.playerController.enabled)) { if (ImGui::Checkbox("Enabled##PlayerController", &obj.playerController.enabled)) {
changed = true; changed = true;
} }
if (ImGui::DragFloat("Move Speed", &obj.playerController.moveSpeed, 0.1f, 0.1f, 100.0f, "%.2f")) { if (ImGui::DragFloat("Move Speed", &obj.playerController.moveSpeed, 0.1f, 0.1f, 100.0f, "%.2f")) {
@@ -2089,6 +2233,7 @@ void Engine::renderInspectorPanel() {
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
} }
ImGui::Unindent(10.0f); ImGui::Unindent(10.0f);
ImGui::PopID();
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
@@ -2097,10 +2242,11 @@ void Engine::renderInspectorPanel() {
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.45f, 0.45f, 0.25f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.45f, 0.45f, 0.25f, 1.0f));
if (ImGui::CollapsingHeader("Rigidbody", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::CollapsingHeader("Rigidbody", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushID("Rigidbody");
ImGui::Indent(10.0f); ImGui::Indent(10.0f);
bool changed = false; bool changed = false;
if (ImGui::Checkbox("Enabled", &obj.rigidbody.enabled)) { if (ImGui::Checkbox("Enabled##Rigidbody", &obj.rigidbody.enabled)) {
changed = true; changed = true;
} }
ImGui::SameLine(); ImGui::SameLine();
@@ -2137,6 +2283,124 @@ void Engine::renderInspectorPanel() {
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
} }
ImGui::Unindent(10.0f); ImGui::Unindent(10.0f);
ImGui::PopID();
}
ImGui::PopStyleColor();
}
if (obj.hasAudioSource) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.55f, 0.4f, 0.3f, 1.0f));
if (ImGui::CollapsingHeader("Audio Source", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushID("AudioSource");
ImGui::Indent(10.0f);
bool changed = false;
auto& src = obj.audioSource;
if (ImGui::Checkbox("Enabled##AudioSource", &src.enabled)) {
changed = true;
}
char clipBuf[512] = {};
std::snprintf(clipBuf, sizeof(clipBuf), "%s", src.clipPath.c_str());
ImGui::TextDisabled("Clip");
ImGui::SetNextItemWidth(-170);
if (ImGui::InputText("##ClipPath", clipBuf, sizeof(clipBuf))) {
src.clipPath = clipBuf;
changed = true;
}
ImGui::SameLine();
if (ImGui::SmallButton("Clear##AudioClip")) {
src.clipPath.clear();
changed = true;
}
ImGui::SameLine();
bool selectionIsAudio = false;
if (!fileBrowser.selectedFile.empty() && fs::exists(fileBrowser.selectedFile)) {
selectionIsAudio = fileBrowser.getFileCategory(fs::directory_entry(fileBrowser.selectedFile)) == FileCategory::Audio;
}
ImGui::BeginDisabled(!selectionIsAudio);
if (ImGui::SmallButton("Use Selection##AudioClip")) {
src.clipPath = fileBrowser.selectedFile.string();
changed = true;
}
ImGui::EndDisabled();
ImGui::Spacing();
bool previewPlaying = !src.clipPath.empty() && audio.isPreviewing(src.clipPath);
if (ImGui::Button(previewPlaying ? "Stop Preview" : "Play Preview")) {
if (previewPlaying) {
audio.stopPreview();
} else if (!src.clipPath.empty()) {
audio.playPreview(src.clipPath, src.volume);
}
}
ImGui::SameLine();
ImGui::TextDisabled("%s", src.clipPath.empty() ? "No clip selected" : fs::path(src.clipPath).filename().string().c_str());
if (ImGui::SliderFloat("Volume", &src.volume, 0.0f, 1.5f, "%.2f")) {
changed = true;
}
if (ImGui::Checkbox("Loop", &src.loop)) {
changed = true;
}
if (ImGui::Checkbox("Play On Start", &src.playOnStart)) {
changed = true;
}
if (ImGui::Checkbox("3D Spatialization", &src.spatial)) {
changed = true;
}
ImGui::BeginDisabled(!src.spatial);
if (ImGui::DragFloat("Min Distance", &src.minDistance, 0.1f, 0.1f, 200.0f, "%.2f")) {
src.minDistance = std::max(0.1f, src.minDistance);
changed = true;
}
if (ImGui::DragFloat("Max Distance", &src.maxDistance, 0.1f, src.minDistance + 0.5f, 500.0f, "%.2f")) {
src.maxDistance = std::max(src.maxDistance, src.minDistance + 0.5f);
changed = true;
}
ImGui::EndDisabled();
const AudioClipPreview* clipPreview = audio.getPreview(src.clipPath);
ImGui::Separator();
ImGui::TextDisabled("Waveform");
ImVec2 waveSize(ImGui::GetContentRegionAvail().x, 80.0f);
double cur = 0.0;
double dur = clipPreview ? clipPreview->durationSeconds : 0.0;
float progress = -1.0f;
if (audio.getPreviewTime(src.clipPath, cur, dur) && dur > 0.0001) {
progress = static_cast<float>(cur / dur);
}
float seekRatio = -1.0f;
drawWaveform("##AudioWaveComponent", clipPreview, waveSize, progress, &seekRatio);
if (seekRatio >= 0.0f && dur > 0.0) {
audio.seekPreview(src.clipPath, seekRatio * dur);
}
if (dur > 0.0) {
ImGui::TextDisabled("Time: %0.2f / %0.2f", cur, dur);
}
if (clipPreview) {
ImGui::TextDisabled("Length: %.2fs | %u channels @ %u Hz",
clipPreview->durationSeconds,
clipPreview->channels,
clipPreview->sampleRate);
}
ImGui::Spacing();
if (ImGui::Button("Remove Audio Source", ImVec2(-1, 0))) {
if (audio.isPreviewing(src.clipPath)) {
audio.stopPreview();
}
obj.hasAudioSource = false;
changed = true;
}
if (changed) {
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::Unindent(10.0f);
ImGui::PopID();
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
@@ -2145,6 +2409,7 @@ void Engine::renderInspectorPanel() {
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.45f, 0.35f, 0.65f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.45f, 0.35f, 0.65f, 1.0f));
if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushID("Camera");
ImGui::Indent(10.0f); ImGui::Indent(10.0f);
const char* cameraTypes[] = { "Scene", "Player" }; const char* cameraTypes[] = { "Scene", "Player" };
int camType = static_cast<int>(obj.camera.type); int camType = static_cast<int>(obj.camera.type);
@@ -2165,6 +2430,7 @@ void Engine::renderInspectorPanel() {
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
} }
ImGui::Unindent(10.0f); ImGui::Unindent(10.0f);
ImGui::PopID();
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
@@ -2173,6 +2439,7 @@ void Engine::renderInspectorPanel() {
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.25f, 0.55f, 0.6f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.25f, 0.55f, 0.6f, 1.0f));
if (ImGui::CollapsingHeader("Post Processing", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::CollapsingHeader("Post Processing", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushID("PostFX");
ImGui::Indent(10.0f); ImGui::Indent(10.0f);
bool changed = false; bool changed = false;
@@ -2273,6 +2540,7 @@ void Engine::renderInspectorPanel() {
ImGui::TextDisabled("Nodes stack in hierarchy order; latest node overrides previous settings."); ImGui::TextDisabled("Nodes stack in hierarchy order; latest node overrides previous settings.");
ImGui::TextDisabled("Wireframe/line mode auto-disables post effects."); ImGui::TextDisabled("Wireframe/line mode auto-disables post effects.");
ImGui::Unindent(10.0f); ImGui::Unindent(10.0f);
ImGui::PopID();
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
@@ -2283,6 +2551,7 @@ void Engine::renderInspectorPanel() {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.35f, 0.55f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.35f, 0.55f, 1.0f));
if (ImGui::CollapsingHeader("Material", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::CollapsingHeader("Material", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushID("Material");
ImGui::Indent(10.0f); ImGui::Indent(10.0f);
auto textureField = [&](const char* label, const char* idSuffix, std::string& path) { auto textureField = [&](const char* label, const char* idSuffix, std::string& path) {
@@ -2508,6 +2777,11 @@ void Engine::renderInspectorPanel() {
obj.scale = glm::vec3(obj.playerController.radius * 2.0f, obj.playerController.height, obj.playerController.radius * 2.0f); obj.scale = glm::vec3(obj.playerController.radius * 2.0f, obj.playerController.height, obj.playerController.radius * 2.0f);
materialChanged = true; materialChanged = true;
} }
if (!obj.hasAudioSource && ImGui::MenuItem("Audio Source")) {
obj.hasAudioSource = true;
obj.audioSource = AudioSourceComponent{};
materialChanged = true;
}
if (!obj.hasCollider && ImGui::BeginMenu("Collider")) { if (!obj.hasCollider && ImGui::BeginMenu("Collider")) {
if (ImGui::MenuItem("Box Collider")) { if (ImGui::MenuItem("Box Collider")) {
obj.hasCollider = true; obj.hasCollider = true;
@@ -2548,6 +2822,7 @@ void Engine::renderInspectorPanel() {
} }
ImGui::Unindent(10.0f); ImGui::Unindent(10.0f);
ImGui::PopID();
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -2557,6 +2832,7 @@ void Engine::renderInspectorPanel() {
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.5f, 0.45f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.5f, 0.45f, 0.2f, 1.0f));
if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushID("Light");
ImGui::Indent(10.0f); ImGui::Indent(10.0f);
int currentType = (obj.type == ObjectType::DirectionalLight) ? 0 : int currentType = (obj.type == ObjectType::DirectionalLight) ? 0 :
@@ -2586,6 +2862,7 @@ void Engine::renderInspectorPanel() {
obj.light.range = 10.0f; obj.light.range = 10.0f;
obj.light.intensity = 3.0f; obj.light.intensity = 3.0f;
obj.light.size = glm::vec2(2.0f, 2.0f); obj.light.size = glm::vec2(2.0f, 2.0f);
obj.light.edgeFade = 0.2f;
} }
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
} }
@@ -2601,7 +2878,7 @@ void Engine::renderInspectorPanel() {
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
} }
} }
if (ImGui::Checkbox("Enabled", &obj.light.enabled)) { if (ImGui::Checkbox("Enabled##Light", &obj.light.enabled)) {
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
} }
@@ -2618,9 +2895,13 @@ void Engine::renderInspectorPanel() {
if (ImGui::DragFloat2("Size", &obj.light.size.x, 0.05f, 0.1f, 10.0f)) { if (ImGui::DragFloat2("Size", &obj.light.size.x, 0.05f, 0.1f, 10.0f)) {
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
} }
if (ImGui::SliderFloat("Edge Softness", &obj.light.edgeFade, 0.0f, 1.0f, "%.2f")) {
projectManager.currentProject.hasUnsavedChanges = true;
}
} }
ImGui::Unindent(10.0f); ImGui::Unindent(10.0f);
ImGui::PopID();
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
@@ -2630,6 +2911,7 @@ void Engine::renderInspectorPanel() {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.3f, 0.5f, 0.4f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.3f, 0.5f, 0.4f, 1.0f));
if (ImGui::CollapsingHeader("Mesh Info", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::CollapsingHeader("Mesh Info", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushID("MeshInfo");
ImGui::Indent(10.0f); ImGui::Indent(10.0f);
const auto* meshInfo = g_objLoader.getMeshInfo(obj.meshId); const auto* meshInfo = g_objLoader.getMeshInfo(obj.meshId);
@@ -2673,6 +2955,7 @@ void Engine::renderInspectorPanel() {
} }
ImGui::Unindent(10.0f); ImGui::Unindent(10.0f);
ImGui::PopID();
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -2683,6 +2966,7 @@ void Engine::renderInspectorPanel() {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.45f, 0.65f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.45f, 0.65f, 1.0f));
if (ImGui::CollapsingHeader("Model Info", ImGuiTreeNodeFlags_DefaultOpen)) { if (ImGui::CollapsingHeader("Model Info", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::PushID("ModelInfo");
ImGui::Indent(10.0f); ImGui::Indent(10.0f);
const auto* meshInfo = getModelLoader().getMeshInfo(obj.meshId); const auto* meshInfo = getModelLoader().getMeshInfo(obj.meshId);
@@ -2724,6 +3008,7 @@ void Engine::renderInspectorPanel() {
} }
ImGui::Unindent(10.0f); ImGui::Unindent(10.0f);
ImGui::PopID();
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -2759,6 +3044,12 @@ void Engine::renderInspectorPanel() {
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
addComponentButtonShown = true; addComponentButtonShown = true;
} }
if (!obj.hasAudioSource && ImGui::MenuItem("Audio Source")) {
obj.hasAudioSource = true;
obj.audioSource = AudioSourceComponent{};
projectManager.currentProject.hasUnsavedChanges = true;
addComponentButtonShown = true;
}
if (!obj.hasCollider && ImGui::BeginMenu("Collider")) { if (!obj.hasCollider && ImGui::BeginMenu("Collider")) {
if (ImGui::MenuItem("Box Collider")) { if (ImGui::MenuItem("Box Collider")) {
obj.hasCollider = true; obj.hasCollider = true;
@@ -2808,6 +3099,7 @@ void Engine::renderInspectorPanel() {
std::string headerLabel = sc.path.empty() ? "Script" : fs::path(sc.path).filename().string(); std::string headerLabel = sc.path.empty() ? "Script" : fs::path(sc.path).filename().string();
std::string headerId = headerLabel + "##ScriptHeader" + std::to_string(i); std::string headerId = headerLabel + "##ScriptHeader" + std::to_string(i);
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_DefaultOpen; ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_DefaultOpen;
ImGui::SetNextItemAllowOverlap(); // allow following button to overlap header hit box
bool open = ImGui::CollapsingHeader(headerId.c_str(), flags); bool open = ImGui::CollapsingHeader(headerId.c_str(), flags);
ImVec2 headerMin = ImGui::GetItemRectMin(); ImVec2 headerMin = ImGui::GetItemRectMin();
@@ -2871,7 +3163,11 @@ void Engine::renderInspectorPanel() {
ScriptContext ctx; ScriptContext ctx;
ctx.engine = this; ctx.engine = this;
ctx.object = &obj; ctx.object = &obj;
// Scope script inspector to avoid shared ImGui IDs across objects or multiple instances
std::string inspectorId = "ScriptInspector##" + std::to_string(obj.id) + sc.path;
ImGui::PushID(inspectorId.c_str());
inspector(ctx); inspector(ctx);
ImGui::PopID();
} else if (!scriptRuntime.getLastError().empty()) { } else if (!scriptRuntime.getLastError().empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.6f, 1.0f), "Inspector load failed"); ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.6f, 1.0f), "Inspector load failed");
ImGui::TextWrapped("%s", scriptRuntime.getLastError().c_str()); ImGui::TextWrapped("%s", scriptRuntime.getLastError().c_str());
@@ -2887,17 +3183,60 @@ void Engine::renderInspectorPanel() {
char valBuf[256] = {}; char valBuf[256] = {};
std::snprintf(keyBuf, sizeof(keyBuf), "%s", sc.settings[s].key.c_str()); std::snprintf(keyBuf, sizeof(keyBuf), "%s", sc.settings[s].key.c_str());
std::snprintf(valBuf, sizeof(valBuf), "%s", sc.settings[s].value.c_str()); std::snprintf(valBuf, sizeof(valBuf), "%s", sc.settings[s].value.c_str());
auto isBoolString = [](const std::string& v, bool& out) {
if (v == "1" || v == "true" || v == "True") { out = true; return true; }
if (v == "0" || v == "false" || v == "False") { out = false; return true; }
return false;
};
auto isNumberString = [](const std::string& v, float& out) {
if (v.empty()) return false;
char* end = nullptr;
out = std::strtof(v.c_str(), &end);
return end && *end == '\0';
};
bool boolVal = false;
bool hasBool = isBoolString(sc.settings[s].value, boolVal);
float numVal = 0.0f;
bool hasNumber = isNumberString(sc.settings[s].value, numVal);
ImGui::SetNextItemWidth(140); ImGui::SetNextItemWidth(140);
if (ImGui::InputText("##Key", keyBuf, sizeof(keyBuf))) { if (ImGui::InputText("##Key", keyBuf, sizeof(keyBuf))) {
sc.settings[s].key = keyBuf; sc.settings[s].key = keyBuf;
scriptsChanged = true; scriptsChanged = true;
} }
ImGui::SameLine(); ImGui::SameLine();
ImGui::SetNextItemWidth(-100); ImGui::SetNextItemWidth(-200);
if (hasBool) {
if (ImGui::Checkbox("##BoolVal", &boolVal)) {
sc.settings[s].value = boolVal ? "1" : "0";
scriptsChanged = true;
}
} else if (hasNumber) {
if (ImGui::InputFloat("##NumVal", &numVal, 0.0f, 0.0f, "%.4f")) {
sc.settings[s].value = std::to_string(numVal);
scriptsChanged = true;
}
} else {
if (ImGui::InputText("##Value", valBuf, sizeof(valBuf))) { if (ImGui::InputText("##Value", valBuf, sizeof(valBuf))) {
sc.settings[s].value = valBuf; sc.settings[s].value = valBuf;
scriptsChanged = true; scriptsChanged = true;
} }
}
ImGui::SameLine();
ImGui::BeginDisabled(hasBool);
if (ImGui::SmallButton("As Bool")) {
sc.settings[s].value = (!sc.settings[s].value.empty() && sc.settings[s].value != "0" && sc.settings[s].value != "false") ? "1" : "0";
scriptsChanged = true;
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(hasNumber);
if (ImGui::SmallButton("As Number")) {
float parsed = 0.0f;
if (!isNumberString(sc.settings[s].value, parsed)) parsed = 0.0f;
sc.settings[s].value = std::to_string(parsed);
scriptsChanged = true;
}
ImGui::EndDisabled();
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton("X")) { if (ImGui::SmallButton("X")) {
sc.settings.erase(sc.settings.begin() + static_cast<long>(s)); sc.settings.erase(sc.settings.begin() + static_cast<long>(s));
@@ -2926,11 +3265,16 @@ void Engine::renderInspectorPanel() {
projectManager.currentProject.hasUnsavedChanges = true; projectManager.currentProject.hasUnsavedChanges = true;
} }
if (browserHasAudio) {
ImGui::Spacing();
renderAudioAssetPanel("Audio Clip (File Browser)", &obj);
}
if (browserHasMaterial) { if (browserHasMaterial) {
ImGui::Spacing(); ImGui::Spacing();
renderMaterialAssetPanel("Material Asset (File Browser)", true); renderMaterialAssetPanel("Material Asset (File Browser)", true);
} }
ImGui::PopID(); // object scope
ImGui::End(); ImGui::End();
} }

View File

@@ -368,6 +368,7 @@ void PhysicsSystem::onPlayStart(const std::vector<SceneObject>& objects) {
createGroundPlane(); createGroundPlane();
for (const auto& obj : objects) { for (const auto& obj : objects) {
if (!obj.enabled) continue;
ActorRecord rec = createActorFor(obj); ActorRecord rec = createActorFor(obj);
if (!rec.actor) continue; if (!rec.actor) continue;
mScene->addActor(*rec.actor); mScene->addActor(*rec.actor);
@@ -480,6 +481,12 @@ void PhysicsSystem::simulate(float deltaTime, std::vector<SceneObject>& objects)
if (!rec.actor) continue; if (!rec.actor) continue;
auto it = std::find_if(objects.begin(), objects.end(), [id](const SceneObject& o) { return o.id == id; }); auto it = std::find_if(objects.begin(), objects.end(), [id](const SceneObject& o) { return o.id == id; });
if (it == objects.end()) continue; if (it == objects.end()) continue;
if (!it->enabled) {
rec.actor->setActorFlag(PxActorFlag::eDISABLE_SIMULATION, true);
continue;
} else {
rec.actor->setActorFlag(PxActorFlag::eDISABLE_SIMULATION, false);
}
if (PxRigidDynamic* dyn = rec.actor->is<PxRigidDynamic>()) { if (PxRigidDynamic* dyn = rec.actor->is<PxRigidDynamic>()) {
if (dyn->getRigidBodyFlags().isSet(PxRigidBodyFlag::eKINEMATIC)) { if (dyn->getRigidBodyFlags().isSet(PxRigidBodyFlag::eKINEMATIC)) {
dyn->setKinematicTarget(PxTransform(ToPxVec3(it->position), ToPxQuat(it->rotation))); dyn->setKinematicTarget(PxTransform(ToPxVec3(it->position), ToPxQuat(it->rotation)));
@@ -497,7 +504,7 @@ void PhysicsSystem::simulate(float deltaTime, std::vector<SceneObject>& objects)
if (!rec.actor || !rec.isDynamic || rec.isKinematic) continue; if (!rec.actor || !rec.isDynamic || rec.isKinematic) continue;
PxTransform pose = rec.actor->getGlobalPose(); PxTransform pose = rec.actor->getGlobalPose();
auto it = std::find_if(objects.begin(), objects.end(), [id](const SceneObject& o) { return o.id == id; }); auto it = std::find_if(objects.begin(), objects.end(), [id](const SceneObject& o) { return o.id == id; });
if (it == objects.end()) continue; if (it == objects.end() || !it->enabled) continue;
it->position = ToGlmVec3(pose.p); it->position = ToGlmVec3(pose.p);
it->rotation.y = ToGlmEulerDeg(pose.q).y; it->rotation.y = ToGlmEulerDeg(pose.q).y;

View File

@@ -258,7 +258,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
if (!file.is_open()) return false; if (!file.is_open()) return false;
file << "# Scene File\n"; file << "# Scene File\n";
file << "version=7\n"; file << "version=8\n";
file << "nextId=" << nextId << "\n"; file << "nextId=" << nextId << "\n";
file << "objectCount=" << objects.size() << "\n"; file << "objectCount=" << objects.size() << "\n";
file << "\n"; file << "\n";
@@ -268,6 +268,9 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "id=" << obj.id << "\n"; file << "id=" << obj.id << "\n";
file << "name=" << obj.name << "\n"; file << "name=" << obj.name << "\n";
file << "type=" << static_cast<int>(obj.type) << "\n"; file << "type=" << static_cast<int>(obj.type) << "\n";
file << "enabled=" << (obj.enabled ? 1 : 0) << "\n";
file << "layer=" << obj.layer << "\n";
file << "tag=" << obj.tag << "\n";
file << "parentId=" << obj.parentId << "\n"; file << "parentId=" << obj.parentId << "\n";
file << "position=" << obj.position.x << "," << obj.position.y << "," << obj.position.z << "\n"; file << "position=" << obj.position.x << "," << obj.position.y << "," << obj.position.z << "\n";
file << "rotation=" << obj.rotation.x << "," << obj.rotation.y << "," << obj.rotation.z << "\n"; file << "rotation=" << obj.rotation.x << "," << obj.rotation.y << "," << obj.rotation.z << "\n";
@@ -297,6 +300,17 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "pcRadius=" << obj.playerController.radius << "\n"; file << "pcRadius=" << obj.playerController.radius << "\n";
file << "pcJumpStrength=" << obj.playerController.jumpStrength << "\n"; file << "pcJumpStrength=" << obj.playerController.jumpStrength << "\n";
} }
file << "hasAudioSource=" << (obj.hasAudioSource ? 1 : 0) << "\n";
if (obj.hasAudioSource) {
file << "audioEnabled=" << (obj.audioSource.enabled ? 1 : 0) << "\n";
file << "audioClip=" << obj.audioSource.clipPath << "\n";
file << "audioVolume=" << obj.audioSource.volume << "\n";
file << "audioLoop=" << (obj.audioSource.loop ? 1 : 0) << "\n";
file << "audioPlayOnStart=" << (obj.audioSource.playOnStart ? 1 : 0) << "\n";
file << "audioSpatial=" << (obj.audioSource.spatial ? 1 : 0) << "\n";
file << "audioMinDistance=" << obj.audioSource.minDistance << "\n";
file << "audioMaxDistance=" << obj.audioSource.maxDistance << "\n";
}
file << "materialColor=" << obj.material.color.r << "," << obj.material.color.g << "," << obj.material.color.b << "\n"; file << "materialColor=" << obj.material.color.r << "," << obj.material.color.g << "," << obj.material.color.b << "\n";
file << "materialAmbient=" << obj.material.ambientStrength << "\n"; file << "materialAmbient=" << obj.material.ambientStrength << "\n";
file << "materialSpecular=" << obj.material.specularStrength << "\n"; file << "materialSpecular=" << obj.material.specularStrength << "\n";
@@ -325,6 +339,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "lightColor=" << obj.light.color.r << "," << obj.light.color.g << "," << obj.light.color.b << "\n"; file << "lightColor=" << obj.light.color.r << "," << obj.light.color.g << "," << obj.light.color.b << "\n";
file << "lightIntensity=" << obj.light.intensity << "\n"; file << "lightIntensity=" << obj.light.intensity << "\n";
file << "lightRange=" << obj.light.range << "\n"; file << "lightRange=" << obj.light.range << "\n";
file << "lightEdgeFade=" << obj.light.edgeFade << "\n";
file << "lightInner=" << obj.light.innerAngle << "\n"; file << "lightInner=" << obj.light.innerAngle << "\n";
file << "lightOuter=" << obj.light.outerAngle << "\n"; file << "lightOuter=" << obj.light.outerAngle << "\n";
file << "lightSize=" << obj.light.size.x << "," << obj.light.size.y << "\n"; file << "lightSize=" << obj.light.size.x << "," << obj.light.size.y << "\n";
@@ -440,6 +455,12 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
else if (currentObj->type == ObjectType::Camera) { else if (currentObj->type == ObjectType::Camera) {
currentObj->camera.type = SceneCameraType::Scene; currentObj->camera.type = SceneCameraType::Scene;
} }
} else if (key == "enabled") {
currentObj->enabled = (std::stoi(value) != 0);
} else if (key == "layer") {
currentObj->layer = std::stoi(value);
} else if (key == "tag") {
currentObj->tag = value;
} else if (key == "parentId") { } else if (key == "parentId") {
currentObj->parentId = std::stoi(value); currentObj->parentId = std::stoi(value);
} else if (key == "position") { } else if (key == "position") {
@@ -499,6 +520,24 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
currentObj->playerController.radius = std::stof(value); currentObj->playerController.radius = std::stof(value);
} else if (key == "pcJumpStrength") { } else if (key == "pcJumpStrength") {
currentObj->playerController.jumpStrength = std::stof(value); currentObj->playerController.jumpStrength = std::stof(value);
} else if (key == "hasAudioSource") {
currentObj->hasAudioSource = std::stoi(value) != 0;
} else if (key == "audioEnabled") {
currentObj->audioSource.enabled = std::stoi(value) != 0;
} else if (key == "audioClip") {
currentObj->audioSource.clipPath = value;
} else if (key == "audioVolume") {
currentObj->audioSource.volume = std::stof(value);
} else if (key == "audioLoop") {
currentObj->audioSource.loop = std::stoi(value) != 0;
} else if (key == "audioPlayOnStart") {
currentObj->audioSource.playOnStart = std::stoi(value) != 0;
} else if (key == "audioSpatial") {
currentObj->audioSource.spatial = std::stoi(value) != 0;
} else if (key == "audioMinDistance") {
currentObj->audioSource.minDistance = std::stof(value);
} else if (key == "audioMaxDistance") {
currentObj->audioSource.maxDistance = std::stof(value);
} else if (key == "materialColor") { } else if (key == "materialColor") {
sscanf(value.c_str(), "%f,%f,%f", sscanf(value.c_str(), "%f,%f,%f",
&currentObj->material.color.r, &currentObj->material.color.r,
@@ -575,6 +614,8 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
currentObj->light.intensity = std::stof(value); currentObj->light.intensity = std::stof(value);
} else if (key == "lightRange") { } else if (key == "lightRange") {
currentObj->light.range = std::stof(value); currentObj->light.range = std::stof(value);
} else if (key == "lightEdgeFade") {
currentObj->light.edgeFade = std::stof(value);
} else if (key == "lightInner") { } else if (key == "lightInner") {
currentObj->light.innerAngle = std::stof(value); currentObj->light.innerAngle = std::stof(value);
} else if (key == "lightOuter") { } else if (key == "lightOuter") {

View File

@@ -873,6 +873,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
float inner = glm::cos(glm::radians(15.0f)); float inner = glm::cos(glm::radians(15.0f));
float outer = glm::cos(glm::radians(25.0f)); float outer = glm::cos(glm::radians(25.0f));
glm::vec2 areaSize = glm::vec2(1.0f); // width/height for area lights glm::vec2 areaSize = glm::vec2(1.0f); // width/height for area lights
float areaFade = 0.0f; // 0 sharp, 1 fully softened
}; };
auto forwardFromRotation = [](const SceneObject& obj) { auto forwardFromRotation = [](const SceneObject& obj) {
glm::vec3 f = glm::normalize(glm::vec3( glm::vec3 f = glm::normalize(glm::vec3(
@@ -891,6 +892,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
lights.reserve(10); lights.reserve(10);
for (const auto& obj : sceneObjects) { for (const auto& obj : sceneObjects) {
if (!obj.enabled) continue;
if (obj.light.enabled && obj.type == ObjectType::DirectionalLight) { if (obj.light.enabled && obj.type == ObjectType::DirectionalLight) {
LightUniform l; LightUniform l;
l.type = 0; l.type = 0;
@@ -903,6 +905,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
} }
if (lights.size() < 10) { if (lights.size() < 10) {
for (const auto& obj : sceneObjects) { for (const auto& obj : sceneObjects) {
if (!obj.enabled) continue;
if (obj.light.enabled && obj.type == ObjectType::SpotLight) { if (obj.light.enabled && obj.type == ObjectType::SpotLight) {
LightUniform l; LightUniform l;
l.type = 2; l.type = 2;
@@ -920,6 +923,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
} }
if (lights.size() < 10) { if (lights.size() < 10) {
for (const auto& obj : sceneObjects) { for (const auto& obj : sceneObjects) {
if (!obj.enabled) continue;
if (obj.light.enabled && obj.type == ObjectType::PointLight) { if (obj.light.enabled && obj.type == ObjectType::PointLight) {
LightUniform l; LightUniform l;
l.type = 1; l.type = 1;
@@ -934,6 +938,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
} }
if (lights.size() < 10) { if (lights.size() < 10) {
for (const auto& obj : sceneObjects) { for (const auto& obj : sceneObjects) {
if (!obj.enabled) continue;
if (obj.light.enabled && obj.type == ObjectType::AreaLight) { if (obj.light.enabled && obj.type == ObjectType::AreaLight) {
LightUniform l; LightUniform l;
l.type = 3; // area l.type = 3; // area
@@ -944,6 +949,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
float sizeHint = glm::max(obj.light.size.x, obj.light.size.y); float sizeHint = glm::max(obj.light.size.x, obj.light.size.y);
l.range = (obj.light.range > 0.0f) ? obj.light.range : glm::max(sizeHint * 2.0f, 1.0f); l.range = (obj.light.range > 0.0f) ? obj.light.range : glm::max(sizeHint * 2.0f, 1.0f);
l.areaSize = obj.light.size; l.areaSize = obj.light.size;
l.areaFade = glm::clamp(obj.light.edgeFade, 0.0f, 1.0f);
lights.push_back(l); lights.push_back(l);
if (lights.size() >= 10) break; if (lights.size() >= 10) break;
} }
@@ -951,6 +957,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
} }
for (const auto& obj : sceneObjects) { for (const auto& obj : sceneObjects) {
if (!obj.enabled) continue;
// Skip light gizmo-only types and camera helpers // Skip light gizmo-only types and camera helpers
if (obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight || obj.type == ObjectType::Camera || obj.type == ObjectType::PostFXNode) { if (obj.type == ObjectType::PointLight || obj.type == ObjectType::SpotLight || obj.type == ObjectType::AreaLight || obj.type == ObjectType::Camera || obj.type == ObjectType::PostFXNode) {
continue; continue;
@@ -980,6 +987,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
shader->setFloat("lightInnerCosArr" + idx, l.inner); shader->setFloat("lightInnerCosArr" + idx, l.inner);
shader->setFloat("lightOuterCosArr" + idx, l.outer); shader->setFloat("lightOuterCosArr" + idx, l.outer);
shader->setVec2("lightAreaSizeArr" + idx, l.areaSize); shader->setVec2("lightAreaSizeArr" + idx, l.areaSize);
shader->setFloat("lightAreaFadeArr" + idx, l.areaFade);
} }
glm::mat4 model = glm::mat4(1.0f); glm::mat4 model = glm::mat4(1.0f);

View File

@@ -36,6 +36,7 @@ struct LightComponent {
glm::vec3 color = glm::vec3(1.0f); glm::vec3 color = glm::vec3(1.0f);
float intensity = 1.0f; float intensity = 1.0f;
float range = 10.0f; float range = 10.0f;
float edgeFade = 0.2f; // 0 = sharp cutoff, 1 = fully softened edges (area lights)
// Spot // Spot
float innerAngle = 15.0f; float innerAngle = 15.0f;
float outerAngle = 25.0f; float outerAngle = 25.0f;
@@ -133,10 +134,24 @@ struct PlayerControllerComponent {
float yaw = 0.0f; float yaw = 0.0f;
}; };
struct AudioSourceComponent {
bool enabled = true;
std::string clipPath;
float volume = 1.0f;
bool loop = true;
bool playOnStart = true;
bool spatial = true;
float minDistance = 1.0f;
float maxDistance = 25.0f;
};
class SceneObject { class SceneObject {
public: public:
std::string name; std::string name;
ObjectType type; ObjectType type;
bool enabled = true;
int layer = 0;
std::string tag = "Untagged";
glm::vec3 position; glm::vec3 position;
glm::vec3 rotation; glm::vec3 rotation;
glm::vec3 scale; glm::vec3 scale;
@@ -165,6 +180,8 @@ public:
ColliderComponent collider; ColliderComponent collider;
bool hasPlayerController = false; bool hasPlayerController = false;
PlayerControllerComponent playerController; PlayerControllerComponent playerController;
bool hasAudioSource = false;
AudioSourceComponent audioSource;
SceneObject(const std::string& name, ObjectType type, int id) SceneObject(const std::string& name, ObjectType type, int id)
: name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {} : name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {}

View File

@@ -1,6 +1,8 @@
#include "ScriptRuntime.h" #include "ScriptRuntime.h"
#include "Engine.h" #include "Engine.h"
#include "SceneObject.h" #include "SceneObject.h"
#include <algorithm>
#include <unordered_map>
#if defined(_WIN32) #if defined(_WIN32)
#include <Windows.h> #include <Windows.h>
@@ -18,6 +20,51 @@ SceneObject* ScriptContext::FindObjectById(int id) {
return engine->findObjectById(id); return engine->findObjectById(id);
} }
bool ScriptContext::IsObjectEnabled() const {
return object ? object->enabled : false;
}
void ScriptContext::SetObjectEnabled(bool enabled) {
if (!object) return;
if (object->enabled != enabled) {
object->enabled = enabled;
MarkDirty();
}
}
int ScriptContext::GetLayer() const {
return object ? object->layer : 0;
}
void ScriptContext::SetLayer(int layer) {
if (!object) return;
int clamped = std::clamp(layer, 0, 31);
if (object->layer != clamped) {
object->layer = clamped;
MarkDirty();
}
}
std::string ScriptContext::GetTag() const {
return object ? object->tag : std::string();
}
void ScriptContext::SetTag(const std::string& tag) {
if (!object) return;
if (object->tag != tag) {
object->tag = tag;
MarkDirty();
}
}
bool ScriptContext::HasTag(const std::string& tag) const {
return object && object->tag == tag;
}
bool ScriptContext::IsInLayer(int layer) const {
return object && object->layer == layer;
}
void ScriptContext::SetPosition(const glm::vec3& pos) { void ScriptContext::SetPosition(const glm::vec3& pos) {
if (object) { if (object) {
object->position = pos; object->position = pos;
@@ -121,7 +168,18 @@ void ScriptContext::AutoSetting(const std::string& key, bool& value) {
if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(), if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(),
[&](const AutoSettingEntry& e){ return e.key == key; })) return; [&](const AutoSettingEntry& e){ return e.key == key; })) return;
value = GetSettingBool(key, value); static std::unordered_map<std::string, bool> defaults;
std::string scriptId = (!script->path.empty()) ? script->path : std::to_string(reinterpret_cast<uintptr_t>(script));
std::string id = scriptId + "|" + key;
bool defaultVal = value;
auto itDef = defaults.find(id);
if (itDef != defaults.end()) {
defaultVal = itDef->second;
} else {
defaults[id] = defaultVal; // capture first-seen initializer for this module/key
}
value = GetSettingBool(key, defaultVal);
AutoSettingEntry entry; AutoSettingEntry entry;
entry.type = AutoSettingType::Bool; entry.type = AutoSettingType::Bool;
entry.key = key; entry.key = key;
@@ -135,7 +193,18 @@ void ScriptContext::AutoSetting(const std::string& key, glm::vec3& value) {
if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(), if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(),
[&](const AutoSettingEntry& e){ return e.key == key; })) return; [&](const AutoSettingEntry& e){ return e.key == key; })) return;
value = GetSettingVec3(key, value); static std::unordered_map<std::string, glm::vec3> defaults;
std::string scriptId = (!script->path.empty()) ? script->path : std::to_string(reinterpret_cast<uintptr_t>(script));
std::string id = scriptId + "|" + key;
glm::vec3 defaultVal = value;
auto itDef = defaults.find(id);
if (itDef != defaults.end()) {
defaultVal = itDef->second;
} else {
defaults[id] = defaultVal;
}
value = GetSettingVec3(key, defaultVal);
AutoSettingEntry entry; AutoSettingEntry entry;
entry.type = AutoSettingType::Vec3; entry.type = AutoSettingType::Vec3;
entry.key = key; entry.key = key;
@@ -149,9 +218,17 @@ void ScriptContext::AutoSetting(const std::string& key, char* buffer, size_t buf
if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(), if (autoSettings.end() != std::find_if(autoSettings.begin(), autoSettings.end(),
[&](const AutoSettingEntry& e){ return e.key == key; })) return; [&](const AutoSettingEntry& e){ return e.key == key; })) return;
std::string existing = GetSetting(key, std::string(buffer)); static std::unordered_map<std::string, std::string> defaults;
std::string scriptId = (!script->path.empty()) ? script->path : std::to_string(reinterpret_cast<uintptr_t>(script));
std::string id = scriptId + "|" + key;
std::string defaultVal = defaults.count(id) ? defaults[id] : std::string(buffer);
defaults.try_emplace(id, defaultVal);
std::string existing = GetSetting(key, defaultVal);
if (!existing.empty()) { if (!existing.empty()) {
std::snprintf(buffer, bufferSize, "%s", existing.c_str()); std::snprintf(buffer, bufferSize, "%s", existing.c_str());
} else if (!defaultVal.empty()) {
std::snprintf(buffer, bufferSize, "%s", defaultVal.c_str());
} }
AutoSettingEntry entry; AutoSettingEntry entry;
entry.type = AutoSettingType::StringBuf; entry.type = AutoSettingType::StringBuf;

View File

@@ -25,6 +25,14 @@ struct ScriptContext {
// Convenience helpers for scripts // Convenience helpers for scripts
SceneObject* FindObjectByName(const std::string& name); SceneObject* FindObjectByName(const std::string& name);
SceneObject* FindObjectById(int id); SceneObject* FindObjectById(int id);
bool IsObjectEnabled() const;
void SetObjectEnabled(bool enabled);
int GetLayer() const;
void SetLayer(int layer);
std::string GetTag() const;
void SetTag(const std::string& tag);
bool HasTag(const std::string& tag) const;
bool IsInLayer(int layer) const;
void SetPosition(const glm::vec3& pos); void SetPosition(const glm::vec3& pos);
void SetRotation(const glm::vec3& rot); void SetRotation(const glm::vec3& rot);
void SetScale(const glm::vec3& scl); void SetScale(const glm::vec3& scl);