And there we go, Audio Clips and Audio sources! (and bugfixes lol.)
This commit is contained in:
@@ -31,6 +31,7 @@ uniform float lightRangeArr[MAX_LIGHTS];
|
||||
uniform float lightInnerCosArr[MAX_LIGHTS];
|
||||
uniform float lightOuterCosArr[MAX_LIGHTS];
|
||||
uniform vec2 lightAreaSizeArr[MAX_LIGHTS];
|
||||
uniform float lightAreaFadeArr[MAX_LIGHTS];
|
||||
|
||||
// Single directional light controlled by hierarchy (fallback if none set)
|
||||
uniform vec3 lightDir = normalize(vec3(0.3, 1.0, 0.5));
|
||||
@@ -92,8 +93,23 @@ void main()
|
||||
vec2 local;
|
||||
local.x = dot(onPlane - center, tangent);
|
||||
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;
|
||||
float dist = length(lvec);
|
||||
@@ -110,7 +126,7 @@ void main()
|
||||
// Lambert against area normal for softer look
|
||||
float nl = max(dot(norm, lDirN), 0.0);
|
||||
float facing = max(dot(n, -lDirN), 0.0);
|
||||
attenuation *= facing;
|
||||
attenuation *= facing * edgeWeight;
|
||||
|
||||
vec3 diffuse = nl * lightColorArr[i] * intensity;
|
||||
vec3 halfwayDir = normalize(lDirN + viewDir);
|
||||
|
||||
91
Scripts/RigidbodyTest.cpp
Normal file
91
Scripts/RigidbodyTest.cpp
Normal 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
95649
include/ThirdParty/miniaudio.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
277
src/AudioSystem.cpp
Normal file
277
src/AudioSystem.cpp
Normal 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
56
src/AudioSystem.h
Normal 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);
|
||||
};
|
||||
@@ -248,6 +248,11 @@ bool Engine::init() {
|
||||
setupImGui();
|
||||
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");
|
||||
return true;
|
||||
}
|
||||
@@ -325,6 +330,17 @@ void Engine::run() {
|
||||
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) {
|
||||
glm::mat4 view = camera.getViewMatrix();
|
||||
float aspect = static_cast<float>(viewportWidth) / static_cast<float>(viewportHeight);
|
||||
@@ -433,6 +449,8 @@ void Engine::shutdown() {
|
||||
}
|
||||
|
||||
physics.onPlayStop();
|
||||
audio.onPlayStop();
|
||||
audio.shutdown();
|
||||
physics.shutdown();
|
||||
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
@@ -756,6 +774,7 @@ void Engine::updateScripts(float delta) {
|
||||
if (sceneObjects.empty()) return;
|
||||
|
||||
for (auto& obj : sceneObjects) {
|
||||
if (!obj.enabled) continue;
|
||||
for (auto& sc : obj.scripts) {
|
||||
if (sc.path.empty()) continue;
|
||||
fs::path binary = resolveScriptBinary(sc.path);
|
||||
@@ -775,7 +794,7 @@ void Engine::updatePlayerController(float delta) {
|
||||
|
||||
SceneObject* player = nullptr;
|
||||
for (auto& obj : sceneObjects) {
|
||||
if (obj.hasPlayerController && obj.playerController.enabled) {
|
||||
if (obj.enabled && obj.hasPlayerController && obj.playerController.enabled) {
|
||||
player = &obj;
|
||||
activePlayerId = obj.id;
|
||||
break;
|
||||
@@ -1112,6 +1131,8 @@ void Engine::duplicateSelected() {
|
||||
newObj.collider = it->collider;
|
||||
newObj.hasPlayerController = it->hasPlayerController;
|
||||
newObj.playerController = it->playerController;
|
||||
newObj.hasAudioSource = it->hasAudioSource;
|
||||
newObj.audioSource = it->audioSource;
|
||||
|
||||
sceneObjects.push_back(newObj);
|
||||
setPrimarySelection(id);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "ScriptCompiler.h"
|
||||
#include "ScriptRuntime.h"
|
||||
#include "PhysicsSystem.h"
|
||||
#include "AudioSystem.h"
|
||||
#include "../include/Window/Window.h"
|
||||
|
||||
void window_size_callback(GLFWwindow* window, int width, int height);
|
||||
@@ -111,6 +112,7 @@ private:
|
||||
ScriptCompiler scriptCompiler;
|
||||
ScriptRuntime scriptRuntime;
|
||||
PhysicsSystem physics;
|
||||
AudioSystem audio;
|
||||
bool showCompilePopup = false;
|
||||
bool lastCompileSuccess = false;
|
||||
std::string lastCompileStatus;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
@@ -1435,8 +1436,10 @@ void Engine::renderMainMenuBar() {
|
||||
} else {
|
||||
addConsoleMessage("PhysX failed to initialize; physics disabled for play mode", ConsoleMessageType::Warning);
|
||||
}
|
||||
audio.onPlayStart(sceneObjects);
|
||||
} else {
|
||||
physics.onPlayStop();
|
||||
audio.onPlayStop();
|
||||
isPaused = false;
|
||||
if (specMode && (physics.isReady() || physics.init())) {
|
||||
physics.onPlayStart(sceneObjects);
|
||||
@@ -1456,8 +1459,13 @@ void Engine::renderMainMenuBar() {
|
||||
}
|
||||
specMode = enable;
|
||||
if (!isPlaying) {
|
||||
if (specMode) physics.onPlayStart(sceneObjects);
|
||||
else physics.onPlayStop();
|
||||
if (specMode) {
|
||||
physics.onPlayStart(sceneObjects);
|
||||
audio.onPlayStart(sceneObjects);
|
||||
} else {
|
||||
physics.onPlayStop();
|
||||
audio.onPlayStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1654,9 +1662,13 @@ void Engine::renderInspectorPanel() {
|
||||
|
||||
fs::path selectedMaterialPath;
|
||||
bool browserHasMaterial = false;
|
||||
fs::path selectedAudioPath;
|
||||
bool browserHasAudio = false;
|
||||
const AudioClipPreview* selectedAudioPreview = nullptr;
|
||||
if (!fileBrowser.selectedFile.empty() && fs::exists(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();
|
||||
browserHasMaterial = true;
|
||||
if (inspectedMaterialPath != selectedMaterialPath.string()) {
|
||||
@@ -1676,11 +1688,51 @@ void Engine::renderInspectorPanel() {
|
||||
inspectedMaterialPath.clear();
|
||||
inspectedMaterialValid = false;
|
||||
}
|
||||
if (cat == FileCategory::Audio) {
|
||||
selectedAudioPath = entry.path();
|
||||
browserHasAudio = true;
|
||||
selectedAudioPreview = audio.getPreview(selectedAudioPath.string());
|
||||
}
|
||||
} else {
|
||||
inspectedMaterialPath.clear();
|
||||
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) {
|
||||
if (!browserHasMaterial) return;
|
||||
|
||||
@@ -1858,9 +1910,70 @@ void Engine::renderInspectorPanel() {
|
||||
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 (browserHasMaterial) {
|
||||
renderMaterialAssetPanel("Material Asset", true);
|
||||
} else if (browserHasAudio) {
|
||||
renderAudioAssetPanel("Audio Clip", nullptr);
|
||||
} else {
|
||||
ImGui::TextDisabled("No object selected");
|
||||
}
|
||||
@@ -1879,6 +1992,7 @@ void Engine::renderInspectorPanel() {
|
||||
}
|
||||
|
||||
SceneObject& obj = *it;
|
||||
ImGui::PushID(obj.id); // Scope per-object widgets to avoid ID collisions
|
||||
bool addComponentButtonShown = false;
|
||||
|
||||
if (selectedObjectIds.size() > 1) {
|
||||
@@ -1922,6 +2036,31 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::Text("ID:");
|
||||
ImGui::SameLine();
|
||||
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();
|
||||
@@ -1931,6 +2070,7 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.4f, 0.5f, 0.3f, 1.0f));
|
||||
|
||||
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::PushID("Transform");
|
||||
ImGui::Indent(10.0f);
|
||||
|
||||
if (obj.type == ObjectType::PostFXNode) {
|
||||
@@ -1973,6 +2113,7 @@ void Engine::renderInspectorPanel() {
|
||||
}
|
||||
|
||||
ImGui::Unindent(10.0f);
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::PopStyleColor();
|
||||
@@ -1981,10 +2122,11 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.5f, 0.35f, 1.0f));
|
||||
if (ImGui::CollapsingHeader("Collider", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::PushID("Collider");
|
||||
ImGui::Indent(10.0f);
|
||||
bool changed = false;
|
||||
|
||||
if (ImGui::Checkbox("Enabled", &obj.collider.enabled)) {
|
||||
if (ImGui::Checkbox("Enabled##Collider", &obj.collider.enabled)) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
@@ -2035,6 +2177,7 @@ void Engine::renderInspectorPanel() {
|
||||
projectManager.currentProject.hasUnsavedChanges = true;
|
||||
}
|
||||
ImGui::Unindent(10.0f);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
@@ -2043,10 +2186,11 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.45f, 0.7f, 1.0f));
|
||||
if (ImGui::CollapsingHeader("Player Controller", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::PushID("PlayerController");
|
||||
ImGui::Indent(10.0f);
|
||||
bool changed = false;
|
||||
|
||||
if (ImGui::Checkbox("Enabled", &obj.playerController.enabled)) {
|
||||
if (ImGui::Checkbox("Enabled##PlayerController", &obj.playerController.enabled)) {
|
||||
changed = true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
ImGui::Unindent(10.0f);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
@@ -2097,10 +2242,11 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.45f, 0.45f, 0.25f, 1.0f));
|
||||
if (ImGui::CollapsingHeader("Rigidbody", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::PushID("Rigidbody");
|
||||
ImGui::Indent(10.0f);
|
||||
bool changed = false;
|
||||
|
||||
if (ImGui::Checkbox("Enabled", &obj.rigidbody.enabled)) {
|
||||
if (ImGui::Checkbox("Enabled##Rigidbody", &obj.rigidbody.enabled)) {
|
||||
changed = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
@@ -2137,6 +2283,124 @@ void Engine::renderInspectorPanel() {
|
||||
projectManager.currentProject.hasUnsavedChanges = true;
|
||||
}
|
||||
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();
|
||||
}
|
||||
@@ -2145,6 +2409,7 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.45f, 0.35f, 0.65f, 1.0f));
|
||||
if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::PushID("Camera");
|
||||
ImGui::Indent(10.0f);
|
||||
const char* cameraTypes[] = { "Scene", "Player" };
|
||||
int camType = static_cast<int>(obj.camera.type);
|
||||
@@ -2165,6 +2430,7 @@ void Engine::renderInspectorPanel() {
|
||||
projectManager.currentProject.hasUnsavedChanges = true;
|
||||
}
|
||||
ImGui::Unindent(10.0f);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
@@ -2173,6 +2439,7 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.25f, 0.55f, 0.6f, 1.0f));
|
||||
if (ImGui::CollapsingHeader("Post Processing", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::PushID("PostFX");
|
||||
ImGui::Indent(10.0f);
|
||||
bool changed = false;
|
||||
|
||||
@@ -2273,6 +2540,7 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::TextDisabled("Nodes stack in hierarchy order; latest node overrides previous settings.");
|
||||
ImGui::TextDisabled("Wireframe/line mode auto-disables post effects.");
|
||||
ImGui::Unindent(10.0f);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
@@ -2283,6 +2551,7 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.35f, 0.55f, 1.0f));
|
||||
|
||||
if (ImGui::CollapsingHeader("Material", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::PushID("Material");
|
||||
ImGui::Indent(10.0f);
|
||||
|
||||
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);
|
||||
materialChanged = true;
|
||||
}
|
||||
if (!obj.hasAudioSource && ImGui::MenuItem("Audio Source")) {
|
||||
obj.hasAudioSource = true;
|
||||
obj.audioSource = AudioSourceComponent{};
|
||||
materialChanged = true;
|
||||
}
|
||||
if (!obj.hasCollider && ImGui::BeginMenu("Collider")) {
|
||||
if (ImGui::MenuItem("Box Collider")) {
|
||||
obj.hasCollider = true;
|
||||
@@ -2548,6 +2822,7 @@ void Engine::renderInspectorPanel() {
|
||||
}
|
||||
|
||||
ImGui::Unindent(10.0f);
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::PopStyleColor();
|
||||
@@ -2557,6 +2832,7 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.5f, 0.45f, 0.2f, 1.0f));
|
||||
if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::PushID("Light");
|
||||
ImGui::Indent(10.0f);
|
||||
|
||||
int currentType = (obj.type == ObjectType::DirectionalLight) ? 0 :
|
||||
@@ -2586,6 +2862,7 @@ void Engine::renderInspectorPanel() {
|
||||
obj.light.range = 10.0f;
|
||||
obj.light.intensity = 3.0f;
|
||||
obj.light.size = glm::vec2(2.0f, 2.0f);
|
||||
obj.light.edgeFade = 0.2f;
|
||||
}
|
||||
projectManager.currentProject.hasUnsavedChanges = true;
|
||||
}
|
||||
@@ -2601,7 +2878,7 @@ void Engine::renderInspectorPanel() {
|
||||
projectManager.currentProject.hasUnsavedChanges = true;
|
||||
}
|
||||
}
|
||||
if (ImGui::Checkbox("Enabled", &obj.light.enabled)) {
|
||||
if (ImGui::Checkbox("Enabled##Light", &obj.light.enabled)) {
|
||||
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)) {
|
||||
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::PopID();
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
@@ -2630,6 +2911,7 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.3f, 0.5f, 0.4f, 1.0f));
|
||||
|
||||
if (ImGui::CollapsingHeader("Mesh Info", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::PushID("MeshInfo");
|
||||
ImGui::Indent(10.0f);
|
||||
|
||||
const auto* meshInfo = g_objLoader.getMeshInfo(obj.meshId);
|
||||
@@ -2673,6 +2955,7 @@ void Engine::renderInspectorPanel() {
|
||||
}
|
||||
|
||||
ImGui::Unindent(10.0f);
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::PopStyleColor();
|
||||
@@ -2683,6 +2966,7 @@ void Engine::renderInspectorPanel() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.35f, 0.45f, 0.65f, 1.0f));
|
||||
|
||||
if (ImGui::CollapsingHeader("Model Info", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
ImGui::PushID("ModelInfo");
|
||||
ImGui::Indent(10.0f);
|
||||
|
||||
const auto* meshInfo = getModelLoader().getMeshInfo(obj.meshId);
|
||||
@@ -2724,6 +3008,7 @@ void Engine::renderInspectorPanel() {
|
||||
}
|
||||
|
||||
ImGui::Unindent(10.0f);
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::PopStyleColor();
|
||||
@@ -2759,6 +3044,12 @@ void Engine::renderInspectorPanel() {
|
||||
projectManager.currentProject.hasUnsavedChanges = 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 (ImGui::MenuItem("Box Collider")) {
|
||||
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 headerId = headerLabel + "##ScriptHeader" + std::to_string(i);
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_DefaultOpen;
|
||||
ImGui::SetNextItemAllowOverlap(); // allow following button to overlap header hit box
|
||||
bool open = ImGui::CollapsingHeader(headerId.c_str(), flags);
|
||||
|
||||
ImVec2 headerMin = ImGui::GetItemRectMin();
|
||||
@@ -2871,7 +3163,11 @@ void Engine::renderInspectorPanel() {
|
||||
ScriptContext ctx;
|
||||
ctx.engine = this;
|
||||
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);
|
||||
ImGui::PopID();
|
||||
} else if (!scriptRuntime.getLastError().empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.6f, 1.0f), "Inspector load failed");
|
||||
ImGui::TextWrapped("%s", scriptRuntime.getLastError().c_str());
|
||||
@@ -2887,17 +3183,60 @@ void Engine::renderInspectorPanel() {
|
||||
char valBuf[256] = {};
|
||||
std::snprintf(keyBuf, sizeof(keyBuf), "%s", sc.settings[s].key.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);
|
||||
if (ImGui::InputText("##Key", keyBuf, sizeof(keyBuf))) {
|
||||
sc.settings[s].key = keyBuf;
|
||||
scriptsChanged = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-100);
|
||||
if (ImGui::InputText("##Value", valBuf, sizeof(valBuf))) {
|
||||
sc.settings[s].value = valBuf;
|
||||
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))) {
|
||||
sc.settings[s].value = valBuf;
|
||||
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();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
sc.settings.erase(sc.settings.begin() + static_cast<long>(s));
|
||||
@@ -2926,11 +3265,16 @@ void Engine::renderInspectorPanel() {
|
||||
projectManager.currentProject.hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
if (browserHasAudio) {
|
||||
ImGui::Spacing();
|
||||
renderAudioAssetPanel("Audio Clip (File Browser)", &obj);
|
||||
}
|
||||
if (browserHasMaterial) {
|
||||
ImGui::Spacing();
|
||||
renderMaterialAssetPanel("Material Asset (File Browser)", true);
|
||||
}
|
||||
|
||||
ImGui::PopID(); // object scope
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
|
||||
@@ -368,6 +368,7 @@ void PhysicsSystem::onPlayStart(const std::vector<SceneObject>& objects) {
|
||||
createGroundPlane();
|
||||
|
||||
for (const auto& obj : objects) {
|
||||
if (!obj.enabled) continue;
|
||||
ActorRecord rec = createActorFor(obj);
|
||||
if (!rec.actor) continue;
|
||||
mScene->addActor(*rec.actor);
|
||||
@@ -480,6 +481,12 @@ void PhysicsSystem::simulate(float deltaTime, std::vector<SceneObject>& objects)
|
||||
if (!rec.actor) continue;
|
||||
auto it = std::find_if(objects.begin(), objects.end(), [id](const SceneObject& o) { return o.id == id; });
|
||||
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 (dyn->getRigidBodyFlags().isSet(PxRigidBodyFlag::eKINEMATIC)) {
|
||||
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;
|
||||
PxTransform pose = rec.actor->getGlobalPose();
|
||||
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->rotation.y = ToGlmEulerDeg(pose.q).y;
|
||||
|
||||
@@ -258,7 +258,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
|
||||
if (!file.is_open()) return false;
|
||||
|
||||
file << "# Scene File\n";
|
||||
file << "version=7\n";
|
||||
file << "version=8\n";
|
||||
file << "nextId=" << nextId << "\n";
|
||||
file << "objectCount=" << objects.size() << "\n";
|
||||
file << "\n";
|
||||
@@ -268,6 +268,9 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
|
||||
file << "id=" << obj.id << "\n";
|
||||
file << "name=" << obj.name << "\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 << "position=" << obj.position.x << "," << obj.position.y << "," << obj.position.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 << "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 << "materialAmbient=" << obj.material.ambientStrength << "\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 << "lightIntensity=" << obj.light.intensity << "\n";
|
||||
file << "lightRange=" << obj.light.range << "\n";
|
||||
file << "lightEdgeFade=" << obj.light.edgeFade << "\n";
|
||||
file << "lightInner=" << obj.light.innerAngle << "\n";
|
||||
file << "lightOuter=" << obj.light.outerAngle << "\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) {
|
||||
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") {
|
||||
currentObj->parentId = std::stoi(value);
|
||||
} else if (key == "position") {
|
||||
@@ -499,6 +520,24 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
|
||||
currentObj->playerController.radius = std::stof(value);
|
||||
} else if (key == "pcJumpStrength") {
|
||||
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") {
|
||||
sscanf(value.c_str(), "%f,%f,%f",
|
||||
¤tObj->material.color.r,
|
||||
@@ -575,6 +614,8 @@ bool SceneSerializer::loadScene(const fs::path& filePath,
|
||||
currentObj->light.intensity = std::stof(value);
|
||||
} else if (key == "lightRange") {
|
||||
currentObj->light.range = std::stof(value);
|
||||
} else if (key == "lightEdgeFade") {
|
||||
currentObj->light.edgeFade = std::stof(value);
|
||||
} else if (key == "lightInner") {
|
||||
currentObj->light.innerAngle = std::stof(value);
|
||||
} else if (key == "lightOuter") {
|
||||
|
||||
@@ -873,6 +873,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
float inner = glm::cos(glm::radians(15.0f));
|
||||
float outer = glm::cos(glm::radians(25.0f));
|
||||
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) {
|
||||
glm::vec3 f = glm::normalize(glm::vec3(
|
||||
@@ -891,6 +892,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
lights.reserve(10);
|
||||
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (!obj.enabled) continue;
|
||||
if (obj.light.enabled && obj.type == ObjectType::DirectionalLight) {
|
||||
LightUniform l;
|
||||
l.type = 0;
|
||||
@@ -903,6 +905,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
}
|
||||
if (lights.size() < 10) {
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (!obj.enabled) continue;
|
||||
if (obj.light.enabled && obj.type == ObjectType::SpotLight) {
|
||||
LightUniform l;
|
||||
l.type = 2;
|
||||
@@ -920,6 +923,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
}
|
||||
if (lights.size() < 10) {
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (!obj.enabled) continue;
|
||||
if (obj.light.enabled && obj.type == ObjectType::PointLight) {
|
||||
LightUniform l;
|
||||
l.type = 1;
|
||||
@@ -934,6 +938,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
}
|
||||
if (lights.size() < 10) {
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (!obj.enabled) continue;
|
||||
if (obj.light.enabled && obj.type == ObjectType::AreaLight) {
|
||||
LightUniform l;
|
||||
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);
|
||||
l.range = (obj.light.range > 0.0f) ? obj.light.range : glm::max(sizeHint * 2.0f, 1.0f);
|
||||
l.areaSize = obj.light.size;
|
||||
l.areaFade = glm::clamp(obj.light.edgeFade, 0.0f, 1.0f);
|
||||
lights.push_back(l);
|
||||
if (lights.size() >= 10) break;
|
||||
}
|
||||
@@ -951,6 +957,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
}
|
||||
|
||||
for (const auto& obj : sceneObjects) {
|
||||
if (!obj.enabled) continue;
|
||||
// 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) {
|
||||
continue;
|
||||
@@ -980,6 +987,7 @@ void Renderer::renderSceneInternal(const Camera& camera, const std::vector<Scene
|
||||
shader->setFloat("lightInnerCosArr" + idx, l.inner);
|
||||
shader->setFloat("lightOuterCosArr" + idx, l.outer);
|
||||
shader->setVec2("lightAreaSizeArr" + idx, l.areaSize);
|
||||
shader->setFloat("lightAreaFadeArr" + idx, l.areaFade);
|
||||
}
|
||||
|
||||
glm::mat4 model = glm::mat4(1.0f);
|
||||
|
||||
@@ -36,6 +36,7 @@ struct LightComponent {
|
||||
glm::vec3 color = glm::vec3(1.0f);
|
||||
float intensity = 1.0f;
|
||||
float range = 10.0f;
|
||||
float edgeFade = 0.2f; // 0 = sharp cutoff, 1 = fully softened edges (area lights)
|
||||
// Spot
|
||||
float innerAngle = 15.0f;
|
||||
float outerAngle = 25.0f;
|
||||
@@ -133,10 +134,24 @@ struct PlayerControllerComponent {
|
||||
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 {
|
||||
public:
|
||||
std::string name;
|
||||
ObjectType type;
|
||||
bool enabled = true;
|
||||
int layer = 0;
|
||||
std::string tag = "Untagged";
|
||||
glm::vec3 position;
|
||||
glm::vec3 rotation;
|
||||
glm::vec3 scale;
|
||||
@@ -165,6 +180,8 @@ public:
|
||||
ColliderComponent collider;
|
||||
bool hasPlayerController = false;
|
||||
PlayerControllerComponent playerController;
|
||||
bool hasAudioSource = false;
|
||||
AudioSourceComponent audioSource;
|
||||
|
||||
SceneObject(const std::string& name, ObjectType type, int id)
|
||||
: name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "ScriptRuntime.h"
|
||||
#include "Engine.h"
|
||||
#include "SceneObject.h"
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <Windows.h>
|
||||
@@ -18,6 +20,51 @@ SceneObject* ScriptContext::FindObjectById(int 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) {
|
||||
if (object) {
|
||||
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(),
|
||||
[&](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;
|
||||
entry.type = AutoSettingType::Bool;
|
||||
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(),
|
||||
[&](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;
|
||||
entry.type = AutoSettingType::Vec3;
|
||||
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(),
|
||||
[&](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()) {
|
||||
std::snprintf(buffer, bufferSize, "%s", existing.c_str());
|
||||
} else if (!defaultVal.empty()) {
|
||||
std::snprintf(buffer, bufferSize, "%s", defaultVal.c_str());
|
||||
}
|
||||
AutoSettingEntry entry;
|
||||
entry.type = AutoSettingType::StringBuf;
|
||||
|
||||
@@ -25,6 +25,14 @@ struct ScriptContext {
|
||||
// Convenience helpers for scripts
|
||||
SceneObject* FindObjectByName(const std::string& name);
|
||||
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 SetRotation(const glm::vec3& rot);
|
||||
void SetScale(const glm::vec3& scl);
|
||||
|
||||
Reference in New Issue
Block a user