Updated license and added a new terms of use screen, Optimized the engine heavily and finalized some Sprite2D functions and objects, alongside some small bug fixes and crashes, so it should be much more stable,

This commit is contained in:
2026-03-04 16:15:38 -05:00
parent f7b1f00322
commit 96976d4ffb
30 changed files with 3607 additions and 952 deletions

94
LICENSE
View File

@@ -1,31 +1,77 @@
Copyright (c) 2025 Shock Interactive LLC & Tareno Labs LLC.
# Modularity Engine License
Permission is hereby granted, free of charge, to any person obtaining a copy of this engine and associated documentation files (the "Engine"), to use, copy, modify, merge, publish, and distribute software built using the Engine (including but not limited to video games, applications, services, and Marketplace content such as ModuPaks), under the following conditions:
Copyright © [2025-2026]
Shock Interactive LLC & Tareno Labs™ LLC
1. Use in Commercial Software:
- You may use Modularity to develop commercial or closed-source software.
- You may sell games, applications, services, or Marketplace content built using the Engine.
- Software built using the Engine is not required to be open source.
## 1. Definitions
For the purposes of this license:
- **Engine** refers to the Modularity engine source code, core runtime, editor, tools, and official components distributed as part of the Modularity project.
- **Software Built Using the Engine** refers to any video game, application, service, tool, or other software created using the Engine.
- **Marketplace Content** refers to assets, plugins, ModuPaks, extensions, templates, or other packages intended for use with the Engine.
2. Marketplace Content:
- You may create and sell assets, plugins, ModuPaks, or extensions for use with the Engine.
- Marketplace content may be licensed under any terms you choose, including closed-source.
- Marketplace content does not automatically become part of the Engine.
## 2. Permission
Permission is hereby granted, free of charge, to any person obtaining a copy of the Engine and associated documentation files to use, copy, modify, merge, publish, and distribute software built using the Engine, subject to the conditions listed in this license.
3. Modifications to the Engine:
- You may modify the Engine for personal or internal use without publishing those changes.
- If you distribute the Engine or a modified version of the Engine, you must release the full corresponding source code of those Engine modifications under this same license.
- Distributed modifications must include a clear notice describing the changes made.
- Original copyright notices must be retained.
## 3. Use in Commercial Software
You may use the Engine to develop commercial or closed-source software.
4. Distribution of the Engine:
- You may distribute the Engine in original or modified form only under this license.
- You may not sell, sublicense, or distribute the Engine itself as a standalone commercial engine product.
- You may not rebrand the Engine and present it as a different engine.
You are permitted to:
- Sell video games, applications, or services built using the Engine
- Distribute commercial software built using the Engine
- Keep the source code of software built using the Engine private
5. Attribution:
- You must include visible attribution to Modularity in the documentation, credits, or about section of any distributed software built using the Engine.
Software built using the Engine is **not required to be open source**.
6. Disclaimer:
- The Engine is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement.
- In no event shall the authors or copyright holders be liable for any claim, damages, or other liability arising from the use of the Engine
## 4. Marketplace Content
You may create and distribute Marketplace Content for use with the Engine.
Marketplace Content:
- May be distributed commercially or free of charge
- May be licensed under any license chosen by its creator, including closed-source licenses
- Does **not automatically become part of the Engine**
Creators retain ownership and licensing control over their Marketplace Content.
## 5. Modifications to the Engine
You may modify the Engine for personal, research, or internal use without publishing those modifications.
However, if you **distribute the Engine or a modified version of the Engine**, the following conditions apply:
- The **full corresponding source code** of the modified Engine must be released under this same license
- The source code must be made available in a **publicly accessible location** without unreasonable access restrictions
- Distributed versions must include a **clear notice describing the modifications made**
- All original copyright notices must be retained
This requirement applies **only to the Engine itself**, not to software built using the Engine.
## 6. Distribution of the Engine
You may distribute the Engine in original or modified form **only under this license**.
You may **not**:
- Sell the Engine itself as a standalone commercial engine product
- Sublicense the Engine under a different license
- Rebrand the Engine and present it as a different engine
Forks or modified versions must clearly acknowledge that they are based on the Modularity Engine.
## 7. Attribution
Software distributed using the Engine must include visible attribution to **Modularity** in at least one of the following locations:
- Software credits
- Documentation
- An "About" section
- A similar visible acknowledgment
Example attribution:
- **Powered by the Modularity Engine**
## 8. Trademarks
The names **"Modularity"** and **"ModuEngine"** are trademarks of Shock Interactive LLC and Tareno Labs™ LLC.
These trademarks may **not** be used to imply endorsement, official status, or affiliation without explicit permission from the trademark holders.
## 9. Disclaimer
The Engine is provided **"as is"**, without warranty of any kind, express or implied, including but not limited to:
- merchantability
- fitness for a particular purpose
- noninfringement
In no event shall the authors or copyright holders be liable for any claim, damages, or other liability arising from the use of the Engine.

View File

@@ -15,6 +15,7 @@ uniform bool unlit = false;
uniform vec3 viewPos;
uniform vec3 materialColor = vec3(1.0);
uniform float materialAlpha = 1.0;
uniform float ambientStrength = 0.2;
uniform vec3 ambientColor = vec3(1.0);
@@ -168,9 +169,13 @@ void main()
texColor = mix(texColor, overlay, mixAmount);
}
vec3 baseColor = texColor * materialColor;
float alpha = tex1.a * materialAlpha;
if (alpha <= 0.001) {
discard;
}
if (unlit) {
FragColor = vec4(baseColor, tex1.a);
FragColor = vec4(baseColor, alpha);
return;
}
@@ -346,7 +351,6 @@ void main()
lighting += (1.0 - shadow) * (attenuation * diffuse + specular);
}
float alpha = tex1.a;
vec3 finalColor = pow(max(lighting, vec3(0.0)), vec3(1.0 / 2.2));
FragColor = vec4(finalColor, alpha);
}

View File

@@ -5,13 +5,16 @@ in vec2 TexCoord;
uniform sampler2D sceneTex;
uniform float threshold = 1.0;
uniform float softKnee = 0.25;
void main() {
vec3 c = texture(sceneTex, TexCoord).rgb;
float luma = dot(c, vec3(0.2125, 0.7154, 0.0721));
float knee = 0.25;
float w = clamp((luma - threshold) / max(knee, 1e-4), 0.0, 1.0);
w = w * w * (3.0 - 2.0 * w);
vec3 masked = c * w;
float knee = max(threshold * softKnee, 1e-4);
float soft = clamp(luma - threshold + knee, 0.0, 2.0 * knee);
soft = (soft * soft) / max(4.0 * knee + 1e-4, 1e-4);
float contribution = max(soft, luma - threshold);
contribution /= max(luma, 1e-4);
vec3 masked = c * clamp(contribution, 0.0, 1.0);
FragColor = vec4(masked, 1.0);
}

View File

@@ -7,6 +7,11 @@ uniform sampler2D sceneTex;
uniform sampler2D bloomTex;
uniform sampler2D historyTex;
uniform bool enableHDR = true;
uniform int toneMapper = 2;
uniform float whitePoint = 4.0;
uniform float gamma = 2.2;
uniform bool enableBloom = false;
uniform float bloomIntensity = 0.8;
@@ -19,6 +24,8 @@ uniform vec3 colorFilter = vec3(1.0);
uniform bool enableMotionBlur = false;
uniform bool hasHistory = false;
uniform float motionBlurStrength = 0.15;
uniform float motionBlurThreshold = 0.04;
uniform float motionBlurClamp = 0.35;
uniform bool enableVignette = false;
uniform float vignetteIntensity = 0.35;
@@ -27,9 +34,13 @@ uniform float vignetteSmoothness = 0.25;
uniform bool enableChromatic = false;
uniform float chromaticAmount = 0.0025;
uniform bool enableSharpen = false;
uniform float sharpenStrength = 0.15;
uniform bool enableAO = false;
uniform float aoRadius = 0.0035;
uniform float aoStrength = 0.6;
uniform vec2 texelSize = vec2(1.0 / 1280.0, 1.0 / 720.0);
vec3 applyColorAdjust(vec3 color) {
if (enableColorAdjust) {
@@ -82,6 +93,41 @@ float computeAOFactor(vec2 uv) {
return clamp(1.0 - occlusion * aoStrength, 0.0, 1.0);
}
vec3 applySharpening(vec2 uv, vec3 color) {
if (!enableSharpen) {
return color;
}
vec3 north = sampleBase(uv + vec2(0.0, texelSize.y));
vec3 south = sampleBase(uv - vec2(0.0, texelSize.y));
vec3 east = sampleBase(uv + vec2(texelSize.x, 0.0));
vec3 west = sampleBase(uv - vec2(texelSize.x, 0.0));
vec3 blurred = (north + south + east + west) * 0.25;
vec3 sharpened = color + (color - blurred) * sharpenStrength;
return max(sharpened, vec3(0.0));
}
vec3 toneMap(vec3 color) {
vec3 mapped = max(color, vec3(0.0));
if (enableHDR) {
float wp = max(whitePoint, 0.001);
vec3 scaled = mapped / wp;
if (toneMapper == 1) {
mapped = scaled / (vec3(1.0) + scaled);
} else if (toneMapper == 2) {
vec3 x = scaled;
mapped = clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
} else {
mapped = clamp(scaled, 0.0, 1.0);
}
} else {
mapped = clamp(mapped, 0.0, 1.0);
}
float safeGamma = max(gamma, 0.001);
return pow(clamp(mapped, 0.0, 1.0), vec3(1.0 / safeGamma));
}
void main() {
vec3 color = sampleBase(TexCoord);
@@ -98,29 +144,14 @@ void main() {
}
if (enableMotionBlur && hasHistory) {
vec2 dir = TexCoord - vec2(0.5);
float len = length(dir);
dir = (len > 0.0001) ? dir / len : vec2(0.0);
float smear = clamp(motionBlurStrength, 0.0, 0.98) * 0.035; // subtle default
vec3 accum = vec3(0.0);
float weightSum = 0.0;
for (int i = 0; i < 3; ++i) {
float t = (float(i) + 1.0) / 3.0;
float w = 1.0 - t * 0.4;
vec2 offsetUv = TexCoord - dir * smear * t;
offsetUv = clamp(offsetUv, vec2(0.002), vec2(0.998));
vec3 sampleCol = texture(historyTex, offsetUv).rgb;
accum += sampleCol * w;
weightSum += w;
}
vec3 history = (weightSum > 0.0) ? accum / weightSum : texture(historyTex, TexCoord).rgb;
float diff = length(color - history);
float motionWeight = smoothstep(0.01, 0.08, diff); // suppress blur when camera still
float mixAmt = clamp(motionBlurStrength * 0.85, 0.0, 0.9) * motionWeight;
if (mixAmt > 0.0001) {
color = mix(color, history, mixAmt);
}
vec3 history = texture(historyTex, TexCoord).rgb;
vec3 delta = clamp(history - color, vec3(-motionBlurClamp), vec3(motionBlurClamp));
float diff = max(max(abs(delta.r), abs(delta.g)), abs(delta.b));
float response = smoothstep(motionBlurThreshold,
max(motionBlurThreshold * 4.0, motionBlurThreshold + 0.0001),
diff);
float mixAmt = clamp(motionBlurStrength * response, 0.0, 0.92);
color += delta * mixAmt;
}
if (enableBloom) {
@@ -128,5 +159,6 @@ void main() {
color += glow;
}
FragColor = vec4(color, 1.0);
color = applySharpening(TexCoord, color);
FragColor = vec4(toneMap(color), 1.0);
}

View File

@@ -13,6 +13,7 @@ uniform bool unlit = false;
uniform float uTime = 0.0;
uniform vec3 materialColor = vec3(1.0);
uniform float materialAlpha = 1.0;
uniform float ambientStrength = 0.2;
uniform float specularStrength = 0.5;
uniform float shininess = 32.0;
@@ -33,9 +34,13 @@ void main()
color = mix(color, overlayColor, clamp(mixAmount, 0.0, 1.0));
}
color *= materialColor;
float alpha = baseSample.a * materialAlpha;
if (alpha <= 0.001) {
discard;
}
if (unlit) {
FragColor = vec4(color, baseSample.a);
FragColor = vec4(color, alpha);
return;
}
@@ -48,5 +53,5 @@ void main()
float spec = pow(max(dot(N, H), 0.0), max(shininess, 1.0)) * clamp(specularStrength, 0.0, 2.0);
vec3 lit = color * (clamp(ambientStrength, 0.0, 1.0) + diffuse) + vec3(spec);
FragColor = vec4(lit, baseSample.a);
FragColor = vec4(lit, alpha);
}

View File

@@ -1,22 +1,179 @@
#include "ScriptRuntime.h"
#include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h"
#include <array>
#include <string>
#include <unordered_map>
namespace {
float walkSpeed = 4.0f;
float runSpeed = 7.0f;
float acceleration = 18.0f;
float drag = 8.0f;
float animationFps = 10.0f;
float runAnimationFps = 14.0f;
float movementThreshold = 0.15f;
bool useRigidbody2D = true;
bool warnedMissingRb = false;
} // namespace
bool useSpriteAnimation = true;
extern "C" void Script_OnInspector(ScriptContext& ctx) {
enum class FacingDirection : int {
Down = 0,
Up = 1,
Right = 2,
Left = 3
};
struct ControllerState {
FacingDirection facing = FacingDirection::Down;
float animationTime = 0.0f;
bool warnedMissingRb = false;
bool warnedMissingSprite = false;
};
std::unordered_map<int, ControllerState> controllerStates;
std::array<int, 4> idleClips = { 0, 0, 0, 0 };
std::array<std::array<int, 4>, 4> walkClips = {{
{{ 0, 0, 0, 0 }},
{{ 0, 0, 0, 0 }},
{{ 0, 0, 0, 0 }},
{{ 0, 0, 0, 0 }}
}};
constexpr const char* kDirectionLabels[4] = { "Down", "Up", "Right", "Left" };
int loadIntSetting(ScriptContext& ctx, const std::string& key, int fallback) {
const std::string raw = ctx.GetSetting(key, "");
if (raw.empty()) return fallback;
try {
return std::stoi(raw);
} catch (...) {
return fallback;
}
}
void saveIntSettingIfChanged(ScriptContext& ctx, const std::string& key, int value) {
const std::string desired = std::to_string(value);
if (ctx.GetSetting(key, "") != desired) {
ctx.SetSetting(key, desired);
}
}
void bindSettings(ScriptContext& ctx) {
ctx.AutoSetting("walkSpeed", walkSpeed);
ctx.AutoSetting("runSpeed", runSpeed);
ctx.AutoSetting("acceleration", acceleration);
ctx.AutoSetting("drag", drag);
ctx.AutoSetting("animationFps", animationFps);
ctx.AutoSetting("runAnimationFps", runAnimationFps);
ctx.AutoSetting("movementThreshold", movementThreshold);
ctx.AutoSetting("useRigidbody2D", useRigidbody2D);
ctx.AutoSetting("useSpriteAnimation", useSpriteAnimation);
for (int dir = 0; dir < 4; ++dir) {
idleClips[dir] = loadIntSetting(ctx, "idle" + std::to_string(dir), idleClips[dir]);
for (int frame = 0; frame < 4; ++frame) {
walkClips[dir][frame] = loadIntSetting(ctx,
"walk" + std::to_string(dir) + "_" + std::to_string(frame),
walkClips[dir][frame]);
}
}
}
bool isValidClip(const ScriptContext& ctx, int clip) {
return clip >= 0 && clip < ctx.GetSpriteClipCount();
}
int facingIndex(FacingDirection dir) {
return static_cast<int>(dir);
}
FacingDirection resolveFacing(const glm::vec2& motion, FacingDirection fallback) {
if (glm::dot(motion, motion) <= 1e-6f) {
return fallback;
}
if (std::abs(motion.x) > std::abs(motion.y)) {
return (motion.x >= 0.0f) ? FacingDirection::Right : FacingDirection::Left;
}
return (motion.y >= 0.0f) ? FacingDirection::Up : FacingDirection::Down;
}
int selectWalkClip(const ScriptContext& ctx, FacingDirection dir, int frameIndex) {
const std::array<int, 4>& clips = walkClips[facingIndex(dir)];
int candidate = clips[std::clamp(frameIndex, 0, 3)];
if (isValidClip(ctx, candidate)) return candidate;
for (int clip : clips) {
if (isValidClip(ctx, clip)) return clip;
}
candidate = idleClips[facingIndex(dir)];
return isValidClip(ctx, candidate) ? candidate : -1;
}
void applySpriteAnimation(ScriptContext& ctx, ControllerState& state, const glm::vec2& motion, float dt, bool isRunning) {
if (!useSpriteAnimation || !ctx.object) return;
const int clipCount = ctx.GetSpriteClipCount();
if (clipCount <= 0) {
if (!state.warnedMissingSprite) {
ctx.AddConsoleMessage("TopDownMovement2D: sprite animation needs Sprite Sheet clips on this object.",
ConsoleMessageType::Warning);
state.warnedMissingSprite = true;
}
return;
}
state.warnedMissingSprite = false;
const float speed = glm::length(motion);
if (speed > movementThreshold) {
state.facing = resolveFacing(motion, state.facing);
state.animationTime += dt;
const float activeAnimationFps = std::max(1.0f, isRunning ? runAnimationFps : animationFps);
const int walkFrame = static_cast<int>(state.animationTime * activeAnimationFps) % 4;
const int clip = selectWalkClip(ctx, state.facing, walkFrame);
if (clip >= 0) {
ctx.SetSpriteClipIndex(clip);
}
return;
}
state.animationTime = 0.0f;
const int idleClip = idleClips[facingIndex(state.facing)];
if (isValidClip(ctx, idleClip)) {
ctx.SetSpriteClipIndex(idleClip);
}
}
bool drawClipSelector(ScriptContext& ctx, const char* label, int& clipIndex) {
const int clipCount = ctx.GetSpriteClipCount();
bool changed = false;
std::string preview = isValidClip(ctx, clipIndex)
? ctx.GetSpriteClipNameAt(clipIndex)
: std::string("<None>");
if (ImGui::BeginCombo(label, preview.c_str())) {
const bool noneSelected = (clipIndex < 0 || clipIndex >= clipCount);
if (ImGui::Selectable("<None>", noneSelected)) {
clipIndex = -1;
changed = true;
}
if (noneSelected) ImGui::SetItemDefaultFocus();
for (int i = 0; i < clipCount; ++i) {
const std::string clipName = ctx.GetSpriteClipNameAt(i);
const bool selected = (clipIndex == i);
if (ImGui::Selectable(clipName.c_str(), selected)) {
clipIndex = i;
changed = true;
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
return changed;
}
} // namespace
extern "C" void Script_OnInspector(ScriptContext& ctx) {
bindSettings(ctx);
ImGui::TextUnformatted("Top Down Movement 2D");
ImGui::Separator();
@@ -24,11 +181,55 @@ extern "C" void Script_OnInspector(ScriptContext& ctx) {
ImGui::DragFloat("Run Speed", &runSpeed, 0.1f, 0.0f, 80.0f, "%.2f");
ImGui::DragFloat("Acceleration", &acceleration, 0.1f, 0.0f, 200.0f, "%.2f");
ImGui::DragFloat("Drag", &drag, 0.1f, 0.0f, 200.0f, "%.2f");
ImGui::DragFloat("Animation FPS", &animationFps, 0.1f, 1.0f, 30.0f, "%.1f");
ImGui::DragFloat("Run Animation FPS", &runAnimationFps, 0.1f, 1.0f, 60.0f, "%.1f");
ImGui::DragFloat("Move Threshold", &movementThreshold, 0.01f, 0.01f, 10.0f, "%.2f");
ImGui::Checkbox("Use Rigidbody2D", &useRigidbody2D);
ImGui::Checkbox("Use Sprite Animation", &useSpriteAnimation);
bool clipSettingsChanged = false;
if (useSpriteAnimation) {
const int clipCount = ctx.GetSpriteClipCount();
ImGui::Separator();
ImGui::TextUnformatted("Sprite Clips");
if (clipCount <= 0) {
ImGui::TextDisabled("Enable Sprite Sheet clips on this object to assign animation frames.");
} else {
ImGui::TextDisabled("%d sprite clips available.", clipCount);
for (int dir = 0; dir < 4; ++dir) {
if (ImGui::CollapsingHeader(kDirectionLabels[dir], ImGuiTreeNodeFlags_DefaultOpen)) {
clipSettingsChanged |= drawClipSelector(ctx,
("Idle##" + std::string(kDirectionLabels[dir])).c_str(),
idleClips[dir]);
for (int frame = 0; frame < 4; ++frame) {
clipSettingsChanged |= drawClipSelector(ctx,
("Walk " + std::to_string(frame + 1) + "##" +
std::string(kDirectionLabels[dir])).c_str(),
walkClips[dir][frame]);
}
}
}
}
}
ctx.SaveAutoSettings();
if (clipSettingsChanged) {
for (int dir = 0; dir < 4; ++dir) {
saveIntSettingIfChanged(ctx, "idle" + std::to_string(dir), idleClips[dir]);
for (int frame = 0; frame < 4; ++frame) {
saveIntSettingIfChanged(ctx,
"walk" + std::to_string(dir) + "_" + std::to_string(frame),
walkClips[dir][frame]);
}
}
}
}
void TickUpdate(ScriptContext& ctx, float dt) {
if (!ctx.object || dt <= 0.0f) return;
bindSettings(ctx);
ControllerState& state = controllerStates[ctx.object->id];
glm::vec2 input(0.0f);
if (ImGui::IsKeyDown(ImGuiKey_W)) input.y += 1.0f;
@@ -37,17 +238,21 @@ void TickUpdate(ScriptContext& ctx, float dt) {
if (ImGui::IsKeyDown(ImGuiKey_A)) input.x -= 1.0f;
if (glm::length(input) > 1e-3f) input = glm::normalize(input);
float speed = ctx.IsSprintDown() ? runSpeed : walkSpeed;
const bool isRunning = ctx.IsSprintDown();
float speed = isRunning ? runSpeed : walkSpeed;
glm::vec2 targetVel = input * speed;
glm::vec2 actualVelocity = targetVel;
if (useRigidbody2D) {
if (!ctx.HasRigidbody2D()) {
if (!warnedMissingRb) {
if (!state.warnedMissingRb) {
ctx.AddConsoleMessage("TopDownMovement2D: add Rigidbody2D to use velocity-based motion.", ConsoleMessageType::Warning);
warnedMissingRb = true;
state.warnedMissingRb = true;
}
applySpriteAnimation(ctx, state, targetVel, dt, isRunning);
return;
}
state.warnedMissingRb = false;
glm::vec2 vel(0.0f);
ctx.GetRigidbody2DVelocity(vel);
if (acceleration <= 0.0f) {
@@ -66,9 +271,12 @@ void TickUpdate(ScriptContext& ctx, float dt) {
vel *= damp;
}
ctx.SetRigidbody2DVelocity(vel);
actualVelocity = vel;
} else {
glm::vec2 pos = ctx.object->ui.position;
pos += targetVel * dt;
ctx.SetPosition2D(pos);
}
applySpriteAnimation(ctx, state, actualVelocity, dt, isRunning);
}

View File

@@ -2,6 +2,7 @@
#define SHADER_H
#include <string>
#include <unordered_map>
#include "../../ThirdParty/glm/glm.hpp"
class Shader
@@ -23,9 +24,11 @@ public:
void setMat4Array(const std::string &name, const glm::mat4 *data, int count) const;
private:
int getUniformLocation(const std::string& name) const;
std::string readShaderFile(const char* filePath);
void compileShaders(const char* vertexSource, const char* fragmentSource);
void checkCompileErrors(unsigned int shader, std::string type);
mutable std::unordered_map<std::string, int> uniformLocationCache;
};
#endif

View File

@@ -2,6 +2,7 @@
#define TEXTURE_H
#include <string>
#include <cstddef>
#include <glad/glad.h>
class Texture
@@ -21,6 +22,7 @@ public:
GLuint GetID() const { return m_ID; }
int GetWidth() const { return m_Width; }
int GetHeight() const { return m_Height; }
size_t GetApproxMemoryBytes() const { return m_ApproxMemoryBytes; }
private:
GLuint m_ID = 0;
@@ -29,6 +31,7 @@ private:
int m_Channels = 0;
GLenum m_InternalFormat = GL_RGBA;
GLenum m_DataFormat = GL_RGBA;
size_t m_ApproxMemoryBytes = 0;
};
#endif
#endif

View File

@@ -19,6 +19,8 @@ public:
float speed = CAMERA_SPEED;
float lastX = 400.0f, lastY = 300.0f;
bool firstMouse = true;
bool orthographic = false;
float pixelsPerUnit = 100.0f;
void processMouse(double xpos, double ypos);
void processKeyboard(float deltaTime, GLFWwindow* window);

View File

@@ -1,4 +1,5 @@
#include "EditorUI.h"
#include <chrono>
#include <unordered_map>
namespace {
@@ -31,6 +32,46 @@ bool hasScrollableAxis(const ImGuiWindow* window, int axis) {
return window->ScrollMax[axis] > 0.0f;
}
FileBrowser::RefreshResult BuildFileBrowserRefreshResult(const fs::path& currentPath,
const std::string& searchFilter,
bool showHiddenFiles) {
FileBrowser::RefreshResult result;
result.path = currentPath;
result.filter = searchFilter;
result.showHiddenFiles = showHiddenFiles;
std::string filterLower = searchFilter;
std::transform(filterLower.begin(), filterLower.end(), filterLower.begin(), ::tolower);
try {
for (const auto& entry : fs::directory_iterator(currentPath)) {
std::string filename = entry.path().filename().string();
if (!showHiddenFiles && !filename.empty() && filename[0] == '.') {
continue;
}
if (!filterLower.empty()) {
std::string filenameLower = filename;
std::transform(filenameLower.begin(), filenameLower.end(), filenameLower.begin(), ::tolower);
if (filenameLower.find(filterLower) == std::string::npos) {
continue;
}
}
result.entries.push_back(entry);
}
std::sort(result.entries.begin(), result.entries.end(), [](const auto& a, const auto& b) {
if (a.is_directory() != b.is_directory()) {
return a.is_directory() > b.is_directory();
}
return a.path().filename().string() < b.path().filename().string();
});
} catch (...) {
}
return result;
}
bool isTouchScrollableWindow(const ImGuiWindow* window) {
if (!window || !window->Active || window->Collapsed || window->SkipItems) {
return false;
@@ -72,32 +113,32 @@ FileBrowser::FileBrowser() {
}
void FileBrowser::refresh() {
entries.clear();
try {
for (const auto& entry : fs::directory_iterator(currentPath)) {
// Skip hidden files if not showing them
std::string filename = entry.path().filename().string();
if (!showHiddenFiles && !filename.empty() && filename[0] == '.') {
continue;
if (refreshInFlight && refreshFuture.valid()) {
const auto status = refreshFuture.wait_for(std::chrono::milliseconds(0));
if (status == std::future_status::ready) {
RefreshResult result = refreshFuture.get();
refreshInFlight = false;
if (result.path == currentPath &&
result.filter == searchFilter &&
result.showHiddenFiles == showHiddenFiles) {
entries = std::move(result.entries);
needsRefresh = false;
}
// Apply search filter if any
if (!matchesFilter(entry)) {
continue;
}
entries.push_back(entry);
}
// Sort: folders first, then alphabetically
std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
if (a.is_directory() != b.is_directory()) {
return a.is_directory() > b.is_directory();
}
return a.path().filename().string() < b.path().filename().string();
});
} catch (...) {
}
needsRefresh = false;
if (!needsRefresh || refreshInFlight) {
return;
}
const fs::path pathSnapshot = currentPath;
const std::string filterSnapshot = searchFilter;
const bool showHiddenSnapshot = showHiddenFiles;
refreshInFlight = true;
refreshFuture = std::async(std::launch::async,
[pathSnapshot, filterSnapshot, showHiddenSnapshot]() {
return BuildFileBrowserRefreshResult(pathSnapshot, filterSnapshot, showHiddenSnapshot);
});
}
void FileBrowser::navigateUp() {

View File

@@ -1,6 +1,7 @@
#pragma once
#include <functional>
#include <future>
#include "Common.h"
#pragma region File Browser Enums
@@ -28,6 +29,13 @@ enum class FileCategory {
class FileBrowser {
public:
struct RefreshResult {
fs::path path;
std::string filter;
bool showHiddenFiles = false;
std::vector<fs::directory_entry> entries;
};
fs::path currentPath;
fs::path selectedFile;
fs::path projectRoot; // Root of current project
@@ -42,11 +50,14 @@ public:
std::vector<fs::path> pathHistory;
int historyIndex = -1;
std::future<RefreshResult> refreshFuture;
bool refreshInFlight = false;
FileBrowser();
// Call refresh after mutating currentPath/searchFilter/showHiddenFiles.
void refresh();
bool isRefreshing() const { return refreshInFlight; }
void navigateUp();
void navigateTo(const fs::path& path);
void navigateBack();

View File

@@ -1122,6 +1122,7 @@ void Engine::renderFileBrowserPanel() {
if (created) {
fileBrowser.selectedFile = target;
fileBrowser.needsRefresh = true;
fileBrowser.refresh();
addConsoleMessage("Created: " + target.string(), ConsoleMessageType::Success);
return true;
@@ -1270,6 +1271,11 @@ void Engine::renderFileBrowserPanel() {
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Clear search");
}
if (fileBrowser.isRefreshing()) {
ImGui::SameLine();
ImGui::TextDisabled("Refreshing...");
}
ImGui::SameLine();
bool isGridMode = fileBrowser.viewMode == FileBrowserViewMode::Grid;
if (isGridMode) {
@@ -1693,6 +1699,19 @@ void Engine::renderFileBrowserPanel() {
loadPixelSpriteDocument(entry.path());
}
if (ImGui::MenuItem("Create Sprite2D")) {
int canvasId = -1;
for (const auto& obj : sceneObjects) {
if (obj.hasUI && obj.ui.type == UIElementType::Canvas) {
canvasId = obj.id;
break;
}
}
if (canvasId < 0) {
addObject(ObjectType::Canvas, "Canvas");
if (!sceneObjects.empty()) {
canvasId = sceneObjects.back().id;
}
}
addObject(ObjectType::Sprite2D, entry.path().stem().string());
if (!sceneObjects.empty()) {
SceneObject& created = sceneObjects.back();
@@ -1704,6 +1723,9 @@ void Engine::renderFileBrowserPanel() {
static_cast<float>(tex->GetHeight()));
}
}
if (canvasId >= 0) {
setParent(created.id, canvasId);
}
projectManager.currentProject.hasUnsavedChanges = true;
}
}
@@ -1964,6 +1986,19 @@ void Engine::renderFileBrowserPanel() {
loadPixelSpriteDocument(entry.path());
}
if (ImGui::MenuItem("Create Sprite2D")) {
int canvasId = -1;
for (const auto& obj : sceneObjects) {
if (obj.hasUI && obj.ui.type == UIElementType::Canvas) {
canvasId = obj.id;
break;
}
}
if (canvasId < 0) {
addObject(ObjectType::Canvas, "Canvas");
if (!sceneObjects.empty()) {
canvasId = sceneObjects.back().id;
}
}
addObject(ObjectType::Sprite2D, entry.path().stem().string());
if (!sceneObjects.empty()) {
SceneObject& created = sceneObjects.back();
@@ -1975,6 +2010,9 @@ void Engine::renderFileBrowserPanel() {
static_cast<float>(tex->GetHeight()));
}
}
if (canvasId >= 0) {
setParent(created.id, canvasId);
}
projectManager.currentProject.hasUnsavedChanges = true;
}
}

View File

@@ -53,6 +53,16 @@ glm::ivec4 NormalizeRect(glm::ivec2 a, glm::ivec2 b) {
return glm::ivec4(minX, minY, maxX - minX + 1, maxY - minY + 1);
}
glm::ivec4 ClampSpriteClipRect(glm::ivec4 rect, int width, int height) {
rect.z = std::max(1, rect.z);
rect.w = std::max(1, rect.w);
rect.x = std::clamp(rect.x, 0, std::max(0, width - 1));
rect.y = std::clamp(rect.y, 0, std::max(0, height - 1));
rect.z = std::min(rect.z, std::max(1, width - rect.x));
rect.w = std::min(rect.w, std::max(1, height - rect.y));
return rect;
}
void FloodFill(std::vector<unsigned char>& pixels, int width, int height, int startX, int startY, const PixelRgba& target, const PixelRgba& replacement) {
if (target.r == replacement.r && target.g == replacement.g &&
target.b == replacement.b && target.a == replacement.a) {
@@ -88,6 +98,18 @@ void EnsureSpriteClipNames(std::vector<std::string>& names, size_t count) {
}
}
void EnsureSpriteClipScales(std::vector<glm::vec2>& scales, size_t count) {
if (scales.size() < count) {
scales.resize(count, glm::vec2(1.0f));
} else if (scales.size() > count) {
scales.resize(count);
}
for (glm::vec2& scale : scales) {
scale.x = std::max(0.01f, scale.x);
scale.y = std::max(0.01f, scale.y);
}
}
void EnsureSpriteLayers(std::vector<SpritesheetLayer>& layers) {
if (layers.empty()) {
layers.push_back({"Layer_0"});
@@ -141,12 +163,14 @@ bool Engine::loadPixelSpriteDocument(const fs::path& imagePath) {
pixelSpriteDocument.strictValidation = parsed.document.strictValidation;
pixelSpriteDocument.spriteFrames = parsed.document.rects;
pixelSpriteDocument.spriteFrameNames = parsed.document.names;
pixelSpriteDocument.spriteFrameScales = parsed.document.scales;
pixelSpriteDocument.layers = parsed.document.layers;
for (const SpritesheetParseMessage& message : parsed.messages) {
addConsoleMessage(message.text, ConsoleMessageType::Warning);
}
}
EnsureSpriteClipNames(pixelSpriteDocument.spriteFrameNames, pixelSpriteDocument.spriteFrames.size());
EnsureSpriteClipScales(pixelSpriteDocument.spriteFrameScales, pixelSpriteDocument.spriteFrames.size());
EnsureSpriteLayers(pixelSpriteDocument.layers);
pixelSpriteUndoStack.clear();
@@ -162,6 +186,7 @@ bool Engine::loadPixelSpriteDocument(const fs::path& imagePath) {
initialState.strictValidation = pixelSpriteDocument.strictValidation;
initialState.spriteFrames = pixelSpriteDocument.spriteFrames;
initialState.spriteFrameNames = pixelSpriteDocument.spriteFrameNames;
initialState.spriteFrameScales = pixelSpriteDocument.spriteFrameScales;
initialState.layers = pixelSpriteDocument.layers;
initialState.activeLayer = pixelSpriteDocument.activeLayer;
initialState.activeFrame = pixelSpriteDocument.activeFrame;
@@ -196,6 +221,7 @@ bool Engine::savePixelSpriteDocument() {
std::ofstream sidecar(pixelSpriteDocument.sidecarPath);
if (sidecar.is_open()) {
EnsureSpriteClipNames(pixelSpriteDocument.spriteFrameNames, pixelSpriteDocument.spriteFrames.size());
EnsureSpriteClipScales(pixelSpriteDocument.spriteFrameScales, pixelSpriteDocument.spriteFrames.size());
EnsureSpriteLayers(pixelSpriteDocument.layers);
SpritesheetDocument sidecarDocument;
sidecarDocument.linkedSpriteName = pixelSpriteDocument.imagePath.lexically_relative(projectManager.currentProject.projectPath).generic_string();
@@ -210,6 +236,7 @@ bool Engine::savePixelSpriteDocument() {
sidecarDocument.strictValidation = pixelSpriteDocument.strictValidation;
sidecarDocument.rects = pixelSpriteDocument.spriteFrames;
sidecarDocument.names = pixelSpriteDocument.spriteFrameNames;
sidecarDocument.scales = pixelSpriteDocument.spriteFrameScales;
sidecarDocument.layers = pixelSpriteDocument.layers;
sidecar << WriteSpritesheet(sidecarDocument);
}
@@ -245,12 +272,14 @@ void Engine::renderPixelSpriteEditorWindow() {
initialState.strictValidation = pixelSpriteDocument.strictValidation;
initialState.spriteFrames = pixelSpriteDocument.spriteFrames;
initialState.spriteFrameNames = pixelSpriteDocument.spriteFrameNames;
initialState.spriteFrameScales = pixelSpriteDocument.spriteFrameScales;
initialState.layers = pixelSpriteDocument.layers;
initialState.activeLayer = pixelSpriteDocument.activeLayer;
initialState.activeFrame = pixelSpriteDocument.activeFrame;
pixelSpriteUndoStack.push_back(std::move(initialState));
}
EnsureSpriteLayers(pixelSpriteDocument.layers);
EnsureSpriteClipScales(pixelSpriteDocument.spriteFrameScales, pixelSpriteDocument.spriteFrames.size());
pixelSpriteDocument.activeLayer = std::clamp(pixelSpriteDocument.activeLayer, 0, std::max(0, static_cast<int>(pixelSpriteDocument.layers.size()) - 1));
if (!ImGui::Begin("Pixel Sprite Editor", &showPixelSpriteEditorWindow, ImGuiWindowFlags_NoCollapse)) {
@@ -281,6 +310,7 @@ void Engine::renderPixelSpriteEditorWindow() {
state.strictValidation = pixelSpriteDocument.strictValidation;
state.spriteFrames = pixelSpriteDocument.spriteFrames;
state.spriteFrameNames = pixelSpriteDocument.spriteFrameNames;
state.spriteFrameScales = pixelSpriteDocument.spriteFrameScales;
state.layers = pixelSpriteDocument.layers;
state.activeLayer = pixelSpriteDocument.activeLayer;
state.activeFrame = pixelSpriteDocument.activeFrame;
@@ -303,6 +333,7 @@ void Engine::renderPixelSpriteEditorWindow() {
pixelSpriteUndoStack.back().strictValidation = pixelSpriteDocument.strictValidation;
pixelSpriteUndoStack.back().spriteFrames = pixelSpriteDocument.spriteFrames;
pixelSpriteUndoStack.back().spriteFrameNames = pixelSpriteDocument.spriteFrameNames;
pixelSpriteUndoStack.back().spriteFrameScales = pixelSpriteDocument.spriteFrameScales;
pixelSpriteUndoStack.back().layers = pixelSpriteDocument.layers;
pixelSpriteUndoStack.back().activeLayer = pixelSpriteDocument.activeLayer;
pixelSpriteUndoStack.back().activeFrame = pixelSpriteDocument.activeFrame;
@@ -324,6 +355,7 @@ void Engine::renderPixelSpriteEditorWindow() {
pixelSpriteDocument.strictValidation = state.strictValidation;
pixelSpriteDocument.spriteFrames = state.spriteFrames;
pixelSpriteDocument.spriteFrameNames = state.spriteFrameNames;
pixelSpriteDocument.spriteFrameScales = state.spriteFrameScales;
pixelSpriteDocument.layers = state.layers;
pixelSpriteDocument.activeLayer = std::clamp(state.activeLayer, 0, std::max(0, static_cast<int>(state.layers.size()) - 1));
pixelSpriteDocument.activeFrame = std::clamp(state.activeFrame, 0, std::max(0, static_cast<int>(state.spriteFrames.size()) - 1));
@@ -345,6 +377,7 @@ void Engine::renderPixelSpriteEditorWindow() {
pixelSpriteDocument.strictValidation = state.strictValidation;
pixelSpriteDocument.spriteFrames = state.spriteFrames;
pixelSpriteDocument.spriteFrameNames = state.spriteFrameNames;
pixelSpriteDocument.spriteFrameScales = state.spriteFrameScales;
pixelSpriteDocument.layers = state.layers;
pixelSpriteDocument.activeLayer = std::clamp(state.activeLayer, 0, std::max(0, static_cast<int>(state.layers.size()) - 1));
pixelSpriteDocument.activeFrame = std::clamp(state.activeFrame, 0, std::max(0, static_cast<int>(state.spriteFrames.size()) - 1));
@@ -372,6 +405,7 @@ void Engine::renderPixelSpriteEditorWindow() {
selected->ui.spriteSourceHeight = pixelSpriteDocument.height;
selected->ui.spriteCustomFrames = pixelSpriteDocument.spriteFrames;
selected->ui.spriteCustomFrameNames = pixelSpriteDocument.spriteFrameNames;
selected->ui.spriteCustomFrameScales = pixelSpriteDocument.spriteFrameScales;
if (!pixelSpriteDocument.spriteFrames.empty()) {
selected->ui.size.x = static_cast<float>(pixelSpriteDocument.spriteFrames[0].z);
selected->ui.size.y = static_cast<float>(pixelSpriteDocument.spriteFrames[0].w);
@@ -501,6 +535,7 @@ void Engine::renderPixelSpriteEditorWindow() {
pushHistory();
pixelSpriteDocument.spriteFrames.push_back(NormalizeRect(pixelSpriteDocument.selectionStart, pixelSpriteDocument.selectionEnd));
pixelSpriteDocument.spriteFrameNames.push_back("Rect_" + std::to_string(pixelSpriteDocument.spriteFrames.size() - 1));
pixelSpriteDocument.spriteFrameScales.push_back(glm::vec2(1.0f));
pixelSpriteDocument.activeFrame = static_cast<int>(pixelSpriteDocument.spriteFrames.size()) - 1;
pixelSpriteDocument.dirty = true;
commitHistoryTop();
@@ -510,12 +545,14 @@ void Engine::renderPixelSpriteEditorWindow() {
pushHistory();
pixelSpriteDocument.spriteFrames.clear();
pixelSpriteDocument.spriteFrameNames.clear();
pixelSpriteDocument.spriteFrameScales.clear();
pixelSpriteDocument.activeFrame = 0;
pixelSpriteDocument.dirty = true;
commitHistoryTop();
}
if (!pixelSpriteDocument.spriteFrames.empty()) {
EnsureSpriteClipNames(pixelSpriteDocument.spriteFrameNames, pixelSpriteDocument.spriteFrames.size());
EnsureSpriteClipScales(pixelSpriteDocument.spriteFrameScales, pixelSpriteDocument.spriteFrames.size());
ImGui::TextDisabled("%d clipped sprites", static_cast<int>(pixelSpriteDocument.spriteFrames.size()));
ImGui::SliderInt("Selected Clip", &pixelSpriteDocument.activeFrame, 0, static_cast<int>(pixelSpriteDocument.spriteFrames.size()) - 1);
char clipNameBuf[128];
@@ -526,6 +563,26 @@ void Engine::renderPixelSpriteEditorWindow() {
pixelSpriteDocument.dirty = true;
commitHistoryTop();
}
glm::ivec4 clipRect = pixelSpriteDocument.spriteFrames[pixelSpriteDocument.activeFrame];
int clipPosition[2] = { clipRect.x, clipRect.y };
int clipSize[2] = { clipRect.z, clipRect.w };
bool clipRectChanged = false;
if (ImGui::DragInt2("Clip Position", clipPosition, 1.0f, 0, std::max(pixelSpriteDocument.width, pixelSpriteDocument.height))) {
clipRect.x = clipPosition[0];
clipRect.y = clipPosition[1];
clipRectChanged = true;
}
if (ImGui::DragInt2("Clip Size", clipSize, 1.0f, 1, std::max(pixelSpriteDocument.width, pixelSpriteDocument.height))) {
clipRect.z = clipSize[0];
clipRect.w = clipSize[1];
clipRectChanged = true;
}
if (clipRectChanged) {
pixelSpriteDocument.spriteFrames[pixelSpriteDocument.activeFrame] =
ClampSpriteClipRect(clipRect, pixelSpriteDocument.width, pixelSpriteDocument.height);
pixelSpriteDocument.dirty = true;
commitHistoryTop();
}
}
ImGui::SeparatorText("Spritesheet");

View File

@@ -476,9 +476,283 @@ static std::vector<LauncherTemplateEntry> GatherTemplateEntries() {
});
return templates;
}
constexpr const char* kModularityTermsVersion = "modularity-tos-v1";
constexpr const char* kModularityTermsText = R"(Modularity Engine Terms of Service
Copyright (c) 2025-2026
Shock Interactive LLC and Tareno Labs LLC
By using the Modularity Engine, editor, runtime, tools, or official components, you agree to these Terms of Service.
1. Definitions
- Engine: the Modularity engine source code, core runtime, editor, tools, and official components distributed as part of the Modularity project.
- Software Built Using the Engine: any video game, application, service, tool, or other software created using the Engine.
- Marketplace Content: assets, plugins, ModuPaks, extensions, templates, or other packages intended for use with the Engine.
2. Permission to Use
You may use, copy, modify, merge, publish, and distribute software built using the Engine, free of charge, subject to these terms.
3. Commercial Use
You may use the Engine to develop commercial or closed-source software.
You may:
- Sell video games, applications, or services built using the Engine.
- Distribute commercial software built using the Engine.
- Keep the source code of software built using the Engine private.
Software built using the Engine is not required to be open source.
4. Marketplace Content
You may create and distribute Marketplace Content for use with the Engine.
Marketplace Content:
- May be distributed commercially or free of charge.
- May be licensed under any license chosen by its creator, including closed-source licenses.
- Does not automatically become part of the Engine.
Creators retain ownership and licensing control over their Marketplace Content.
5. Modifications to the Engine
You may modify the Engine for personal, research, or internal use without publishing those modifications.
If you distribute the Engine or a modified version of the Engine:
- The full corresponding source code of the modified Engine must be released under these same terms.
- The source code must be made available in a publicly accessible location without unreasonable access restrictions.
- Distributed versions must include a clear notice describing the modifications made.
- All original copyright notices must be retained.
These requirements apply only to the Engine itself, not to software built using the Engine.
6. Distribution of the Engine
You may distribute the Engine in original or modified form only under these terms.
You may not:
- Sell the Engine itself as a standalone commercial engine product.
- Sublicense the Engine under a different license.
- Rebrand the Engine and present it as a different engine.
Forks or modified versions must clearly acknowledge that they are based on the Modularity Engine.
7. Attribution
Software distributed using the Engine must include visible attribution to Modularity in at least one of the following locations:
- Software credits
- Documentation
- An About section
- A similar visible acknowledgment
Example attribution:
Powered by the Modularity Engine
8. Trademarks
The names "Modularity" and "ModuEngine" are trademarks of Shock Interactive LLC and Tareno Labs LLC.
These trademarks may not be used to imply endorsement, official status, or affiliation without explicit permission from the trademark holders.
9. Disclaimer
The Engine is provided "as is", without warranty of any kind, express or implied, including merchantability, fitness for a particular purpose, and noninfringement.
In no event shall the authors or copyright holders be liable for any claim, damages, or other liability arising from the use of the Engine.)";
} // namespace
#pragma endregion
bool Engine::requiresTermsOfServiceAcceptance() const {
#ifdef MODULARITY_PLAYER
return false;
#else
return projectManager.acceptedTermsVersion != kModularityTermsVersion;
#endif
}
void Engine::renderTermsOfServiceModal() {
#ifdef MODULARITY_PLAYER
return;
#else
if (!requiresTermsOfServiceAcceptance()) {
termsPopupOpened = false;
return;
}
if (showLauncher && !launcherIntroFinished) {
return;
}
if (!termsPopupOpened) {
ImGui::OpenPopup("Modularity Terms of Service");
termsPopupOpened = true;
}
const ImVec2 displaySize = ImGui::GetIO().DisplaySize;
const ImVec2 popupSize(
ImClamp(displaySize.x * 0.72f, 640.0f, 920.0f),
ImClamp(displaySize.y * 0.80f, 520.0f, 760.0f));
ImGui::SetNextWindowPos(ImVec2(displaySize.x * 0.5f, displaySize.y * 0.5f), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(popupSize, ImGuiCond_Appearing);
const ImGuiWindowFlags popupFlags =
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoSavedSettings;
if (ImGui::BeginPopupModal("Modularity Terms of Service", nullptr, popupFlags)) {
auto centeredText = [](const char* text, const ImVec4& color) {
const ImVec2 textSize = ImGui::CalcTextSize(text);
const float avail = ImGui::GetContentRegionAvail().x;
if (avail > textSize.x) {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail - textSize.x) * 0.5f);
}
ImGui::TextColored(color, "%s", text);
};
auto beginCenteredColumn = [](float maxWidth) {
const float avail = ImGui::GetContentRegionAvail().x;
const float width = ImMax(120.0f, ImMin(maxWidth, avail));
if (avail > width) {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (avail - width) * 0.5f);
}
ImGui::BeginGroup();
ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + width);
return width;
};
auto endCenteredColumn = []() {
ImGui::PopTextWrapPos();
ImGui::EndGroup();
};
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 10.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 12.0f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.12f, 0.16f, 0.96f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.24f, 0.30f, 0.40f, 0.95f));
centeredText("Modularity Engine Terms of Service", ImVec4(0.92f, 0.96f, 1.00f, 1.0f));
ImGui::Spacing();
beginCenteredColumn(520.0f);
ImGui::TextWrapped("Please review these terms before using Modularity. Accepting records this version for this installation so you only see it again if the terms change.");
endCenteredColumn();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
const float footerHeight = ImGui::GetFrameHeightWithSpacing() * 3.6f;
if (ImGui::BeginChild("TermsOfServiceScroll", ImVec2(0.0f, -footerHeight), true, ImGuiWindowFlags_AlwaysVerticalScrollbar)) {
beginCenteredColumn(720.0f);
auto renderWrappedParagraph = [](const std::string& text, const ImVec4* color = nullptr) {
if (color) {
ImGui::PushStyleColor(ImGuiCol_Text, *color);
}
ImGui::TextWrapped("%s", text.c_str());
if (color) {
ImGui::PopStyleColor();
}
};
auto renderWrappedBullet = [](const std::string& text) {
const float bulletStartX = ImGui::GetCursorPosX();
ImGui::Bullet();
const float textStartX = ImGui::GetCursorPosX();
ImGui::SameLine(0.0f, 6.0f);
ImGui::PushTextWrapPos(textStartX + ImGui::GetContentRegionAvail().x);
ImGui::TextUnformatted(text.c_str());
ImGui::PopTextWrapPos();
const float lineHeight = ImGui::GetTextLineHeight();
if (ImGui::GetCursorPosX() < bulletStartX) {
ImGui::SetCursorPosX(bulletStartX);
}
if (ImGui::GetTextLineHeightWithSpacing() > lineHeight) {
ImGui::Spacing();
}
};
std::istringstream termsStream(kModularityTermsText);
std::string line;
while (std::getline(termsStream, line)) {
if (line.empty()) {
ImGui::Spacing();
continue;
}
if (line == "Modularity Engine Terms of Service") {
continue;
}
if (line.rfind("Copyright", 0) == 0) {
ImGui::TextDisabled("%s", line.c_str());
continue;
}
if (line.rfind("By using", 0) == 0) {
ImGui::Spacing();
renderWrappedParagraph(line);
continue;
}
if (std::isdigit(static_cast<unsigned char>(line[0])) && line.find('.') != std::string::npos) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.88f, 0.93f, 1.00f, 1.0f), "%s", line.c_str());
continue;
}
if (line.rfind("- ", 0) == 0) {
renderWrappedBullet(line.substr(2));
continue;
}
if (line.rfind("Example attribution:", 0) == 0) {
ImGui::Spacing();
const ImVec4 accentColor(0.70f, 0.80f, 0.96f, 1.0f);
renderWrappedParagraph(line, &accentColor);
continue;
}
renderWrappedParagraph(line);
}
endCenteredColumn();
}
ImGui::EndChild();
ImGui::PopStyleColor(2);
ImGui::PopStyleVar(2);
ImGui::Spacing();
beginCenteredColumn(520.0f);
ImGui::TextDisabled("Version: %s", kModularityTermsVersion);
endCenteredColumn();
ImGui::Spacing();
const ImGuiStyle& style = ImGui::GetStyle();
const float declineWidth = 170.0f;
const float acceptWidth = 190.0f;
const float totalButtonWidth = declineWidth + style.ItemSpacing.x + acceptWidth;
const float buttonAvail = ImGui::GetContentRegionAvail().x;
if (buttonAvail > totalButtonWidth) {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (buttonAvail - totalButtonWidth) * 0.5f);
}
if (ImGui::Button("Decline and Exit", ImVec2(170.0f, 0.0f))) {
glfwSetWindowShouldClose(editorWindow, GLFW_TRUE);
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.20f, 0.38f, 0.66f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.24f, 0.44f, 0.74f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.18f, 0.33f, 0.58f, 1.0f));
if (ImGui::Button("Accept and Continue", ImVec2(190.0f, 0.0f))) {
projectManager.acceptedTermsVersion = kModularityTermsVersion;
projectManager.saveLauncherSettings();
termsPopupOpened = false;
ImGui::CloseCurrentPopup();
}
ImGui::PopStyleColor(3);
ImGui::EndPopup();
}
#endif
}
#pragma region Launcher
void Engine::renderLauncher() {
ImGuiIO& io = ImGui::GetIO();

View File

@@ -38,14 +38,14 @@ namespace {
return imagePath;
}
std::vector<glm::ivec4> LoadSpriteSheetRects(const fs::path& sidecarPath) {
std::optional<SpritesheetDocument> LoadSpriteSheetDocument(const fs::path& sidecarPath) {
std::ifstream sidecar(sidecarPath);
if (!sidecar.is_open()) {
return {};
return std::nullopt;
}
std::ostringstream buffer;
buffer << sidecar.rdbuf();
return ParseSpritesheet(buffer.str()).document.rects;
return ParseSpritesheet(buffer.str()).document;
}
std::optional<std::string> InferManagedTypeFromSource(const std::string& source,
@@ -615,16 +615,6 @@ std::vector<glm::ivec4> LoadSpriteSheetRects(const fs::path& sidecarPath) {
}
}
std::vector<std::string> LoadSpriteSheetNames(const fs::path& sidecarPath) {
std::ifstream sidecar(sidecarPath);
if (!sidecar.is_open()) {
return {};
}
std::ostringstream buffer;
buffer << sidecar.rdbuf();
return ParseSpritesheet(buffer.str()).document.names;
}
void EnsureSpriteClipNames(std::vector<std::string>& names, size_t count) {
if (names.size() < count) {
for (size_t i = names.size(); i < count; ++i) {
@@ -634,6 +624,19 @@ void EnsureSpriteClipNames(std::vector<std::string>& names, size_t count) {
names.resize(count);
}
}
void EnsureSpriteClipScales(std::vector<glm::vec2>& scales, size_t count) {
if (scales.size() < count) {
scales.resize(count, glm::vec2(1.0f));
} else if (scales.size() > count) {
scales.resize(count);
}
for (glm::vec2& scale : scales) {
scale.x = std::max(0.01f, scale.x);
scale.y = std::max(0.01f, scale.y);
}
}
#pragma endregion
#pragma region Hierarchy Panel
@@ -806,7 +809,7 @@ void Engine::renderHierarchyPanel() {
// ── Other / Effects ───────────────────────
if (ImGui::BeginMenu("Effects"))
{
if (ImGui::MenuItem("Post FX Node")) addObject(ObjectType::PostFXNode, "Post FX");
if (ImGui::MenuItem("ModuVolume")) addObject(ObjectType::PostFXNode, "ModuVolume");
if (ImGui::MenuItem("Audio Reverb Zone")) createReverbZoneObject();
ImGui::EndMenu();
}
@@ -980,6 +983,8 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter,
if (!alreadyAssigned) {
ScriptComponent sc;
sc.path = path;
sc.lastBinaryPath.clear();
sc.lastBinaryVerified = false;
std::string ext = fs::path(path).extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
@@ -992,6 +997,7 @@ void Engine::renderObjectNode(SceneObject& obj, const std::string& filter,
sc.language = ScriptLanguage::Cpp;
}
obj.scripts.push_back(sc);
markRuntimeScriptBindingsDirty();
projectManager.currentProject.hasUnsavedChanges = true;
addConsoleMessage("Assigned script to " + obj.name, ConsoleMessageType::Success);
}
@@ -1464,17 +1470,21 @@ void Engine::renderInspectorPanel() {
target.albedoTexturePath = imagePath.string();
const fs::path sidecarPath = IsSpriteSheetSidecarPath(sourcePath) ? sourcePath : fs::path(imagePath.string() + ".spritesheet");
std::vector<glm::ivec4> clips;
if (fs::exists(sidecarPath)) {
clips = LoadSpriteSheetRects(sidecarPath);
}
std::vector<std::string> clipNames;
std::vector<glm::vec2> clipScales;
if (fs::exists(sidecarPath)) {
clipNames = LoadSpriteSheetNames(sidecarPath);
if (std::optional<SpritesheetDocument> sidecar = LoadSpriteSheetDocument(sidecarPath)) {
clips = std::move(sidecar->rects);
clipNames = std::move(sidecar->names);
clipScales = std::move(sidecar->scales);
}
}
target.ui.spriteCustomFrames = std::move(clips);
target.ui.spriteCustomFrameNames = std::move(clipNames);
target.ui.spriteCustomFrameScales = std::move(clipScales);
EnsureSpriteClipNames(target.ui.spriteCustomFrameNames, target.ui.spriteCustomFrames.size());
EnsureSpriteClipScales(target.ui.spriteCustomFrameScales, target.ui.spriteCustomFrames.size());
target.ui.spriteCustomFramesEnabled = !target.ui.spriteCustomFrames.empty();
target.ui.spriteSheetEnabled = target.ui.spriteCustomFramesEnabled || target.ui.spriteSheetEnabled;
target.ui.spriteSheetFrame = 0;
@@ -1549,7 +1559,10 @@ void Engine::renderInspectorPanel() {
ImGui::Spacing();
bool matChanged = false;
if (ImGui::ColorEdit3("Base Color", &inspectedMaterial.color.x)) {
glm::vec4 inspectedBaseColor(inspectedMaterial.color, inspectedMaterial.alpha);
if (ImGui::ColorEdit4("Base Color", &inspectedBaseColor.x)) {
inspectedMaterial.color = glm::vec3(inspectedBaseColor);
inspectedMaterial.alpha = std::clamp(inspectedBaseColor.w, 0.0f, 1.0f);
matChanged = true;
}
float metallic = inspectedMaterial.specularStrength;
@@ -1926,7 +1939,7 @@ void Engine::renderInspectorPanel() {
} else if (obj.hasCamera) {
typeLabel = "Camera";
} else if (obj.hasPostFX) {
typeLabel = "Post FX Node";
typeLabel = "ModuVolume";
}
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "%s", typeLabel);
@@ -2041,7 +2054,7 @@ void Engine::renderInspectorPanel() {
changed = true;
}
glm::vec2 minSize(8.0f, 8.0f);
glm::vec2 minSize(1.0f, 1.0f);
if (ImGui::DragFloat2("Size (px)", &obj.ui.size.x, 1.0f, minSize.x, 4096.0f)) {
obj.ui.size.x = std::max(minSize.x, obj.ui.size.x);
obj.ui.size.y = std::max(minSize.y, obj.ui.size.y);
@@ -2355,6 +2368,10 @@ void Engine::renderInspectorPanel() {
ImGui::TextDisabled("Uses mesh from the object (OBJ/Model). Non-convex is static-only.");
}
if (ImGui::DragFloat3("Offset", &obj.collider.offset.x, 0.01f, -1000.0f, 1000.0f, "%.3f")) {
changed = true;
}
ImGui::SeparatorText("Surface");
if (ImGui::DragFloat("Static Friction", &obj.collider.staticFriction, 0.01f, 0.0f, 4.0f, "%.2f")) {
obj.collider.staticFriction = std::clamp(obj.collider.staticFriction, 0.0f, 4.0f);
@@ -2688,6 +2705,10 @@ void Engine::renderInspectorPanel() {
}
}
if (ImGui::DragFloat2("Offset", &obj.collider2D.offset.x, 0.1f, -10000.0f, 10000.0f, "%.2f")) {
changed = true;
}
ImGui::Unindent(10.0f);
ImGui::PopID();
}
@@ -3502,7 +3523,7 @@ void Engine::renderInspectorPanel() {
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.25f, 0.55f, 0.6f, 1.0f));
bool changed = false;
bool removePostFx = false;
auto header = drawComponentHeader("Post Processing", "PostFX", &obj.postFx.enabled, true, [&]() {
auto header = drawComponentHeader("ModuVolume", "PostFX", &obj.postFx.enabled, true, [&]() {
if (ImGui::MenuItem("Remove")) {
removePostFx = true;
}
@@ -3513,96 +3534,168 @@ void Engine::renderInspectorPanel() {
if (header.open) {
ImGui::PushID("PostFX");
ImGui::Indent(10.0f);
if (ImGui::CollapsingHeader("Volume", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Checkbox("Global Volume", &obj.postFx.isGlobal)) {
changed = true;
}
if (ImGui::DragFloat("Priority", &obj.postFx.priority, 0.05f, -100.0f, 100.0f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Blend Weight", &obj.postFx.blendWeight, 0.0f, 1.0f, "%.2f")) {
changed = true;
}
if (!obj.postFx.isGlobal) {
if (ImGui::DragFloat("Blend Radius", &obj.postFx.blendRadius, 0.1f, 0.1f, 1000.0f, "%.2f")) {
obj.postFx.blendRadius = std::max(0.1f, obj.postFx.blendRadius);
changed = true;
}
ImGui::TextDisabled("Local volumes use this object's transform and scale as bounds.");
}
}
ImGui::Separator();
ImGui::TextDisabled("Bloom");
if (ImGui::Checkbox("Bloom Enabled", &obj.postFx.bloomEnabled)) {
changed = true;
if (ImGui::CollapsingHeader("HDR & Tone Mapping", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Checkbox("HDR Enabled", &obj.postFx.hdrEnabled)) {
changed = true;
}
const char* toneMapperNames[] = { "None", "Reinhard", "ACES" };
int toneMapper = static_cast<int>(obj.postFx.toneMapper);
if (ImGui::Combo("Tone Mapper", &toneMapper, toneMapperNames, IM_ARRAYSIZE(toneMapperNames))) {
obj.postFx.toneMapper = static_cast<PostFXToneMapper>(toneMapper);
changed = true;
}
if (ImGui::SliderFloat("White Point", &obj.postFx.whitePoint, 0.25f, 16.0f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Output Gamma", &obj.postFx.gamma, 1.0f, 3.0f, "%.2f")) {
changed = true;
}
}
ImGui::BeginDisabled(!obj.postFx.bloomEnabled);
if (ImGui::SliderFloat("Threshold", &obj.postFx.bloomThreshold, 0.0f, 3.0f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Intensity##Bloom", &obj.postFx.bloomIntensity, 0.0f, 3.0f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Spread", &obj.postFx.bloomRadius, 0.5f, 3.5f, "%.2f")) {
changed = true;
}
ImGui::EndDisabled();
ImGui::Separator();
ImGui::TextDisabled("Color Adjustments");
if (ImGui::Checkbox("Enable Color Adjust", &obj.postFx.colorAdjustEnabled)) {
changed = true;
if (ImGui::CollapsingHeader("Bloom", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Checkbox("Enabled##Bloom", &obj.postFx.bloomEnabled)) {
changed = true;
}
ImGui::BeginDisabled(!obj.postFx.bloomEnabled);
if (ImGui::SliderFloat("Threshold", &obj.postFx.bloomThreshold, 0.0f, 4.0f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Soft Knee", &obj.postFx.bloomSoftKnee, 0.0f, 1.0f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Intensity##Bloom", &obj.postFx.bloomIntensity, 0.0f, 4.0f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Spread", &obj.postFx.bloomRadius, 0.5f, 4.5f, "%.2f")) {
changed = true;
}
ImGui::EndDisabled();
}
ImGui::BeginDisabled(!obj.postFx.colorAdjustEnabled);
if (ImGui::SliderFloat("Exposure (EV)", &obj.postFx.exposure, -5.0f, 5.0f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Contrast", &obj.postFx.contrast, 0.0f, 2.5f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Saturation", &obj.postFx.saturation, 0.0f, 2.5f, "%.2f")) {
changed = true;
}
if (ImGui::ColorEdit3("Color Filter", &obj.postFx.colorFilter.x)) {
changed = true;
}
ImGui::EndDisabled();
ImGui::Separator();
ImGui::TextDisabled("Motion Blur");
if (ImGui::Checkbox("Enable Motion Blur", &obj.postFx.motionBlurEnabled)) {
changed = true;
if (ImGui::CollapsingHeader("Color", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Checkbox("Enabled##ColorAdjust", &obj.postFx.colorAdjustEnabled)) {
changed = true;
}
ImGui::BeginDisabled(!obj.postFx.colorAdjustEnabled);
if (ImGui::SliderFloat("Exposure (EV)", &obj.postFx.exposure, -5.0f, 5.0f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Contrast", &obj.postFx.contrast, 0.0f, 2.5f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Saturation", &obj.postFx.saturation, 0.0f, 2.5f, "%.2f")) {
changed = true;
}
if (ImGui::ColorEdit3("Color Filter", &obj.postFx.colorFilter.x)) {
changed = true;
}
ImGui::EndDisabled();
}
ImGui::BeginDisabled(!obj.postFx.motionBlurEnabled);
if (ImGui::SliderFloat("Strength", &obj.postFx.motionBlurStrength, 0.0f, 0.95f, "%.2f")) {
changed = true;
}
ImGui::EndDisabled();
ImGui::Separator();
ImGui::TextDisabled("Vignette");
if (ImGui::Checkbox("Enable Vignette", &obj.postFx.vignetteEnabled)) {
changed = true;
if (ImGui::CollapsingHeader("Motion Blur", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Checkbox("Enabled##MotionBlur", &obj.postFx.motionBlurEnabled)) {
changed = true;
}
ImGui::BeginDisabled(!obj.postFx.motionBlurEnabled);
if (ImGui::SliderFloat("Strength", &obj.postFx.motionBlurStrength, 0.0f, 0.95f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Threshold", &obj.postFx.motionBlurThreshold, 0.0f, 0.25f, "%.3f")) {
changed = true;
}
if (ImGui::SliderFloat("Clamp", &obj.postFx.motionBlurClamp, 0.0f, 1.5f, "%.2f")) {
changed = true;
}
ImGui::EndDisabled();
}
ImGui::BeginDisabled(!obj.postFx.vignetteEnabled);
if (ImGui::SliderFloat("Intensity##Vignette", &obj.postFx.vignetteIntensity, 0.0f, 1.5f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Smoothness", &obj.postFx.vignetteSmoothness, 0.05f, 1.0f, "%.2f")) {
changed = true;
}
ImGui::EndDisabled();
ImGui::Separator();
ImGui::TextDisabled("Ambient Occlusion");
if (ImGui::Checkbox("Enable AO", &obj.postFx.ambientOcclusionEnabled)) {
changed = true;
}
ImGui::BeginDisabled(!obj.postFx.ambientOcclusionEnabled);
if (ImGui::SliderFloat("AO Radius", &obj.postFx.aoRadius, 0.0005f, 0.01f, "%.4f")) {
changed = true;
}
if (ImGui::SliderFloat("AO Strength", &obj.postFx.aoStrength, 0.0f, 2.0f, "%.2f")) {
changed = true;
}
ImGui::EndDisabled();
if (ImGui::CollapsingHeader("Lens", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Checkbox("Vignette", &obj.postFx.vignetteEnabled)) {
changed = true;
}
ImGui::BeginDisabled(!obj.postFx.vignetteEnabled);
if (ImGui::SliderFloat("Intensity##Vignette", &obj.postFx.vignetteIntensity, 0.0f, 1.5f, "%.2f")) {
changed = true;
}
if (ImGui::SliderFloat("Smoothness", &obj.postFx.vignetteSmoothness, 0.05f, 1.0f, "%.2f")) {
changed = true;
}
ImGui::EndDisabled();
ImGui::Separator();
ImGui::TextDisabled("Chromatic Aberration");
if (ImGui::Checkbox("Enable Chromatic", &obj.postFx.chromaticAberrationEnabled)) {
changed = true;
}
ImGui::BeginDisabled(!obj.postFx.chromaticAberrationEnabled);
if (ImGui::SliderFloat("Fringe Amount", &obj.postFx.chromaticAmount, 0.0f, 0.01f, "%.4f")) {
changed = true;
}
ImGui::EndDisabled();
if (ImGui::Checkbox("Chromatic Aberration", &obj.postFx.chromaticAberrationEnabled)) {
changed = true;
}
ImGui::BeginDisabled(!obj.postFx.chromaticAberrationEnabled);
if (ImGui::SliderFloat("Fringe Amount", &obj.postFx.chromaticAmount, 0.0f, 0.01f, "%.4f")) {
changed = true;
}
ImGui::EndDisabled();
if (ImGui::Checkbox("Sharpen", &obj.postFx.sharpenEnabled)) {
changed = true;
}
ImGui::BeginDisabled(!obj.postFx.sharpenEnabled);
if (ImGui::SliderFloat("Sharpen Strength", &obj.postFx.sharpenStrength, 0.0f, 1.0f, "%.2f")) {
changed = true;
}
ImGui::EndDisabled();
}
if (ImGui::CollapsingHeader("Ambient Occlusion", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::Checkbox("Enabled##AO", &obj.postFx.ambientOcclusionEnabled)) {
changed = true;
}
ImGui::BeginDisabled(!obj.postFx.ambientOcclusionEnabled);
if (ImGui::SliderFloat("AO Radius", &obj.postFx.aoRadius, 0.0005f, 0.01f, "%.4f")) {
changed = true;
}
if (ImGui::SliderFloat("AO Strength", &obj.postFx.aoStrength, 0.0f, 2.0f, "%.2f")) {
changed = true;
}
ImGui::EndDisabled();
}
if (ImGui::CollapsingHeader("Profiling", ImGuiTreeNodeFlags_DefaultOpen)) {
static const Renderer::PostProcessStats zeroPostStats{};
const Renderer::PostProcessStats& postStats = rendererInitialized
? renderer.getLastViewportPostStats()
: zeroPostStats;
const bool activeLastFrame = (postStats.resolvedVolumeId == obj.id);
ImGui::Text("Resolved Last Frame: %s", activeLastFrame ? "Yes" : "No");
if (!postStats.resolvedVolumeName.empty()) {
ImGui::Text("Resolved Volume: %s", postStats.resolvedVolumeName.c_str());
}
ImGui::Text("Active Volumes: %d", postStats.activeVolumeCount);
ImGui::Text("Blend: %.2f", activeLastFrame ? postStats.resolvedBlend : 0.0f);
ImGui::Text("Effects: %d", postStats.activeEffectCount);
ImGui::Text("Resolve: %.2f ms", postStats.resolveMs);
ImGui::Text("Bloom Extract: %.2f ms", postStats.bloomExtractMs);
ImGui::Text("Bloom Blur: %.2f ms", postStats.bloomBlurMs);
ImGui::Text("Composite: %.2f ms", postStats.compositeMs);
ImGui::Text("Total: %.2f ms", postStats.totalMs);
ImGui::TextDisabled("Highest-priority active volume wins; local volumes fade by blend radius.");
ImGui::TextDisabled("Wireframe/line mode auto-disables post effects.");
}
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();
}
@@ -3693,7 +3786,10 @@ void Engine::renderInspectorPanel() {
bool materialChanged = false;
ImGui::TextDisabled("Surface Inputs");
if (ImGui::ColorEdit3("Base Color", &obj.material.color.x)) {
glm::vec4 baseColor(obj.material.color, obj.material.alpha);
if (ImGui::ColorEdit4("Base Color", &baseColor.x)) {
obj.material.color = glm::vec3(baseColor);
obj.material.alpha = std::clamp(baseColor.w, 0.0f, 1.0f);
materialChanged = true;
}
@@ -4316,6 +4412,8 @@ void Engine::renderInspectorPanel() {
ImGui::SetNextItemWidth(-140);
if (ImGui::InputText("##ScriptPath", pathBuf, sizeof(pathBuf))) {
sc.path = pathBuf;
sc.lastBinaryPath.clear();
sc.lastBinaryVerified = false;
scriptsChanged = true;
if (sc.language == ScriptLanguage::CSharp) {
std::string stem = fs::path(sc.path).stem().string();
@@ -4344,6 +4442,8 @@ void Engine::renderInspectorPanel() {
}
if (useSelection) {
sc.path = entry.path().string();
sc.lastBinaryPath.clear();
sc.lastBinaryVerified = false;
scriptsChanged = true;
if (isNativeScriptLanguage(sc.language)) {
sc.language = inferNativeLanguageFromPath(entry.path());
@@ -4380,14 +4480,18 @@ void Engine::renderInspectorPanel() {
// Scope script inspector to avoid shared ImGui IDs across objects or multiple instances
std::string inspectorId = "ScriptInspector##" + std::to_string(obj.id) + sc.path;
if (isNativeScriptLanguage(sc.language)) {
fs::path binary = resolveScriptBinary(sc.path);
if (binary.empty() && !sc.lastBinaryPath.empty()) {
fs::path fallback = sc.lastBinaryPath;
if (fs::exists(fallback)) {
binary = fallback;
fs::path binary;
if (!sc.lastBinaryPath.empty()) {
fs::path cachedBinary = sc.lastBinaryPath;
if (fs::exists(cachedBinary)) {
binary = std::move(cachedBinary);
}
}
if (binary.empty()) {
binary = resolveScriptBinary(sc.path);
}
sc.lastBinaryPath = binary.string();
sc.lastBinaryVerified = !binary.empty();
ScriptRuntime::InspectorFn inspector = scriptRuntime.getInspector(binary);
if (inspector) {
ImGui::Separator();
@@ -4403,14 +4507,18 @@ void Engine::renderInspectorPanel() {
ImGui::TextDisabled("No inspector exported (Script_OnInspector)");
}
} else {
fs::path assembly = resolveManagedAssembly(sc.path);
if (assembly.empty() && !sc.lastBinaryPath.empty()) {
fs::path fallback = sc.lastBinaryPath;
if (fs::exists(fallback)) {
assembly = fallback;
fs::path assembly;
if (!sc.lastBinaryPath.empty()) {
fs::path cachedAssembly = sc.lastBinaryPath;
if (fs::exists(cachedAssembly)) {
assembly = std::move(cachedAssembly);
}
}
if (assembly.empty()) {
assembly = resolveManagedAssembly(sc.path);
}
sc.lastBinaryPath = assembly.string();
sc.lastBinaryVerified = !assembly.empty();
bool hasInspector = managedRuntime.hasInspector(assembly, sc.managedType);
if (hasInspector) {
ImGui::Separator();
@@ -4677,7 +4785,7 @@ void Engine::renderInspectorPanel() {
obj.cameraFollow2D = CameraFollow2DComponent{};
componentChanged = true;
});
addEntry("Rendering/Post Processing", !obj.hasPostFX, [&]() {
addEntry("Rendering/ModuVolume", !obj.hasPostFX, [&]() {
obj.hasPostFX = true;
obj.postFx = PostFXSettings{};
UpdateLegacyTypeFromComponents(obj);
@@ -4914,6 +5022,8 @@ void Engine::renderInspectorPanel() {
sc.language = ScriptLanguage::Cpp;
}
sc.path = path.string();
sc.lastBinaryPath.clear();
sc.lastBinaryVerified = false;
if (sc.language == ScriptLanguage::CSharp) {
sc.managedType = path.stem().string();
}
@@ -4934,6 +5044,7 @@ void Engine::renderInspectorPanel() {
sc.language = ScriptLanguage::Cpp;
sc.path = bin.string();
sc.lastBinaryPath = bin.string();
sc.lastBinaryVerified = true;
obj.scripts.push_back(std::move(sc));
scriptsChanged = true;
componentChanged = true;
@@ -5026,6 +5137,7 @@ void Engine::renderInspectorPanel() {
ImGui::PopID();
if (scriptsChanged) {
markRuntimeScriptBindingsDirty();
projectManager.currentProject.hasUnsavedChanges = true;
}
if (componentChanged) {

File diff suppressed because it is too large Load Diff

View File

@@ -148,6 +148,8 @@ bool readMaterialFile(const std::string& path, MaterialFileData& outData) {
std::string val = line.substr(pos + 1);
if (key == "color") {
sscanf(val.c_str(), "%f,%f,%f", &outData.props.color.r, &outData.props.color.g, &outData.props.color.b);
} else if (key == "opacity" || key == "alpha") {
outData.props.alpha = std::clamp(std::stof(val), 0.0f, 1.0f);
} else if (key == "ambient") {
outData.props.ambientStrength = std::stof(val);
} else if (key == "specular") {
@@ -189,6 +191,7 @@ bool writeMaterialFile(const MaterialFileData& data, const std::string& path) {
}
f << "# Material\n";
f << "color=" << data.props.color.r << "," << data.props.color.g << "," << data.props.color.b << "\n";
f << "opacity=" << data.props.alpha << "\n";
f << "ambient=" << data.props.ambientStrength << "\n";
f << "specular=" << data.props.specularStrength << "\n";
f << "shininess=" << data.props.shininess << "\n";
@@ -294,6 +297,7 @@ void ApplyObjectPreset(SceneObject& obj, ObjectType preset) {
break;
case ObjectType::PostFXNode:
obj.hasPostFX = true;
obj.scale = glm::vec3(6.0f);
obj.postFx.enabled = true;
obj.postFx.bloomEnabled = true;
obj.postFx.colorAdjustEnabled = true;
@@ -374,6 +378,42 @@ void Engine::applyProjectPipelineDefaults(bool force) {
}
}
namespace {
bool HasMeaningfulSpriteFrameScales(const UIElementComponent& ui) {
if (ui.spriteCustomFrameScales.size() != ui.spriteCustomFrames.size() ||
ui.spriteCustomFrameScales.empty()) {
return false;
}
for (const glm::vec2& scale : ui.spriteCustomFrameScales) {
if (std::abs(scale.x - 1.0f) > 0.0001f || std::abs(scale.y - 1.0f) > 0.0001f) {
return true;
}
}
return false;
}
glm::vec2 ResolveSpriteFrameScale(const UIElementComponent& ui, int frameIndex) {
if (!ui.spriteCustomFramesEnabled || ui.spriteCustomFrames.empty()) {
return glm::vec2(1.0f);
}
const int frameCount = static_cast<int>(ui.spriteCustomFrames.size());
const int frame = std::clamp(frameIndex, 0, frameCount - 1);
if (HasMeaningfulSpriteFrameScales(ui)) {
const glm::vec2 authored = ui.spriteCustomFrameScales[static_cast<size_t>(frame)];
return glm::vec2(std::max(0.01f, authored.x), std::max(0.01f, authored.y));
}
const glm::ivec4& referenceRect = ui.spriteCustomFrames.front();
const glm::ivec4& frameRect = ui.spriteCustomFrames[static_cast<size_t>(frame)];
const float referenceWidth = static_cast<float>(std::max(1, referenceRect.z));
const float referenceHeight = static_cast<float>(std::max(1, referenceRect.w));
return glm::vec2(
static_cast<float>(std::max(1, frameRect.z)) / referenceWidth,
static_cast<float>(std::max(1, frameRect.w)) / referenceHeight);
}
}
int Engine::resolveSpriteSheetFrame(const SceneObject& obj) const {
if (obj.ui.spriteCustomFramesEnabled && !obj.ui.spriteCustomFrames.empty()) {
int total = static_cast<int>(obj.ui.spriteCustomFrames.size());
@@ -392,12 +432,7 @@ int Engine::resolveSpriteSheetFrame(const SceneObject& obj) const {
glm::vec2 Engine::getSpriteDisplaySize(const SceneObject& obj) const {
glm::vec2 size(std::max(1.0f, obj.ui.size.x), std::max(1.0f, obj.ui.size.y));
if (obj.ui.spriteCustomFramesEnabled && !obj.ui.spriteCustomFrames.empty()) {
const glm::ivec4 rect = obj.ui.spriteCustomFrames[resolveSpriteSheetFrame(obj)];
size.x = std::max(size.x, static_cast<float>(std::max(1, rect.z)));
size.y = std::max(size.y, static_cast<float>(std::max(1, rect.w)));
}
return size;
return size * ResolveSpriteFrameScale(obj.ui, resolveSpriteSheetFrame(obj));
}
std::array<ImVec2, 4> Engine::buildSpriteSheetUvs(const SceneObject& obj) const {
@@ -1161,6 +1196,8 @@ void Engine::clearSelection() {
Camera Engine::makeCameraFromObject(const SceneObject& obj) const {
Camera cam;
cam.position = obj.position;
cam.orthographic = isProject2DPipeline() || obj.camera.use2D;
cam.pixelsPerUnit = std::max(1.0f, obj.camera.pixelsPerUnit);
glm::quat q = glm::quat(glm::radians(obj.rotation));
glm::mat3 rot = glm::mat3_cast(q);
cam.front = glm::normalize(rot * glm::vec3(0.0f, 0.0f, -1.0f));
@@ -1359,6 +1396,36 @@ void Engine::recordState(const char* /*reason*/) {
redoStack.clear();
}
void Engine::capturePlayModeSnapshot() {
playModeSnapshot.scene.objects = sceneObjects;
playModeSnapshot.scene.selectedIds = selectedObjectIds;
playModeSnapshot.scene.nextId = nextObjectId;
playModeSnapshot.hadUnsavedChanges = projectManager.currentProject.hasUnsavedChanges;
playModeSnapshot.valid = true;
}
void Engine::restorePlayModeSnapshot() {
if (!playModeSnapshot.valid) {
return;
}
sceneObjects = playModeSnapshot.scene.objects;
selectedObjectIds = playModeSnapshot.scene.selectedIds;
selectedObjectId = selectedObjectIds.empty() ? -1 : selectedObjectIds.back();
nextObjectId = playModeSnapshot.scene.nextId;
projectManager.currentProject.hasUnsavedChanges = playModeSnapshot.hadUnsavedChanges;
sceneObjectIndexById.clear();
sceneObjectIndexData = nullptr;
sceneObjectIndexCount = 0;
markRuntimeScriptBindingsDirty();
aiAgentRuntimeStates.clear();
activePlayerId = -1;
updateHierarchyWorldTransforms();
playModeSnapshot = {};
}
void Engine::undo() {
if (undoStack.empty()) return;
@@ -1600,10 +1667,18 @@ void Engine::run() {
pollProjectLoad();
pollSceneLoad();
if (!showLauncher) {
const bool termsPending = requiresTermsOfServiceAcceptance();
if (!showLauncher && !termsPending) {
handleKeyboardShortcuts();
}
if (termsPending) {
cursorLocked = false;
gameViewCursorLocked = false;
viewportController.setFocused(false);
}
if (gameViewCursorLocked) {
cursorLocked = false;
viewportController.setFocused(false);
@@ -1645,7 +1720,9 @@ void Engine::run() {
updateRigidbody2D(deltaTime);
}
updateCameraFollow2D(deltaTime);
if (isPlaying) {
updateCameraFollow2D(deltaTime);
}
updateSkeletalAnimations(deltaTime);
updateHierarchyWorldTransforms();
@@ -1756,7 +1833,7 @@ void Engine::run() {
}
}
if (!showLauncher && projectManager.currentProject.isLoaded && rendererInitialized) {
if (playerMode && !showLauncher && projectManager.currentProject.isLoaded && rendererInitialized) {
glm::mat4 view = camera.getViewMatrix();
float renderFov = buildSettings.editorCameraFov;
float renderNear = buildSettings.editorCameraNear;
@@ -1845,17 +1922,21 @@ void Engine::run() {
if (showProjectBrowser) renderProjectBrowserPanel();
}
if (showBuildSettings) renderBuildSettingsWindow();
renderScriptEditorWindows();
renderViewport();
if (showGameViewport) renderGameViewportWindow();
if (showConsole) renderConsolePanel();
renderDialogs();
renderLatestErrorBar();
} else {
mainDockspaceId = 0;
renderPlayerViewport();
}
if (showBuildSettings) renderBuildSettingsWindow();
renderScriptEditorWindows();
renderViewport();
if (showGameViewport) renderGameViewportWindow();
if (showConsole) renderConsolePanel();
renderLatestErrorBar();
} else {
mainDockspaceId = 0;
renderPlayerViewport();
}
if (!playerMode) {
renderTermsOfServiceModal();
renderDialogs();
}
if (firstFrame) {
std::cerr << "[DEBUG] First frame: UI rendering complete, finalizing frame..." << std::endl;
@@ -2630,54 +2711,104 @@ void Engine::handleKeyboardShortcuts() {
#pragma region Runtime Updates
void Engine::updateScripts(float delta) {
if (sceneObjects.empty()) return;
if (runtimeScriptBindingsCachedVersion != runtimeScriptBindingsVersion) {
rebuildRuntimeScriptBindings();
}
if (runtimeScriptBindings.empty()) return;
refreshSceneObjectIndexCache();
for (auto& obj : sceneObjects) {
for (const RuntimeScriptBinding& binding : runtimeScriptBindings) {
auto objIt = sceneObjectIndexById.find(binding.objectId);
if (objIt == sceneObjectIndexById.end()) continue;
SceneObject& obj = sceneObjects[objIt->second];
if (!obj.enabled) continue;
for (auto& sc : obj.scripts) {
if (!sc.enabled) continue;
if (sc.path.empty()) continue;
ScriptContext ctx;
ctx.engine = this;
ctx.object = &obj;
ctx.script = &sc;
if (sc.language == ScriptLanguage::CSharp) {
fs::path assembly = resolveManagedAssembly(sc.path);
if (assembly.empty() || !fs::exists(assembly)) continue;
managedRuntime.tickModule(assembly, sc.managedType, ctx, delta, specMode, testMode);
} else {
fs::path binary = resolveScriptBinary(sc.path);
std::error_code ec;
bool hasBinary = !binary.empty() && fs::exists(binary, ec) && !ec;
if (!hasBinary) {
fs::path scriptPath(sc.path);
std::string missingKey = scriptPath.lexically_normal().string();
if (nativeScriptMissingLogged.insert(missingKey).second) {
std::cerr << "[Script] Native script binary missing for '" << sc.path
<< "'. Compile scripts before running/exporting.\n";
addConsoleMessage(
"Native script binary missing for '" + sc.path +
"'. Compile scripts before running/exporting.",
ConsoleMessageType::Warning);
}
continue;
}
if (binding.scriptIndex >= obj.scripts.size()) continue;
ScriptComponent& sc = obj.scripts[binding.scriptIndex];
if (!sc.enabled) continue;
if (sc.path.empty()) continue;
std::string binaryKey = binary.lexically_normal().string();
nativeScriptMissingLogged.erase(fs::path(sc.path).lexically_normal().string());
scriptRuntime.tickModule(binary, ctx, delta, specMode, testMode);
const std::string& runtimeError = scriptRuntime.getLastError();
if (!runtimeError.empty()) {
std::string errorKey = binaryKey + "|" + runtimeError;
if (nativeScriptLoadErrorLogged.insert(errorKey).second) {
std::cerr << "[Script] Failed to load native script '" << binary.filename().string()
<< "': " << runtimeError << "\n";
addConsoleMessage(
"Failed to load native script '" + binary.filename().string() +
"': " + runtimeError,
ConsoleMessageType::Error);
ScriptContext ctx;
ctx.engine = this;
ctx.object = &obj;
ctx.script = &sc;
if (sc.language == ScriptLanguage::CSharp) {
fs::path assembly;
if (!sc.lastBinaryPath.empty()) {
if (sc.lastBinaryVerified) {
assembly = fs::path(sc.lastBinaryPath);
} else {
fs::path cachedAssembly = sc.lastBinaryPath;
if (fs::exists(cachedAssembly)) {
assembly = std::move(cachedAssembly);
sc.lastBinaryVerified = true;
}
}
}
if (assembly.empty()) {
assembly = resolveManagedAssembly(sc.path);
sc.lastBinaryPath = assembly.string();
std::error_code ec;
sc.lastBinaryVerified = !assembly.empty() && fs::exists(assembly, ec) && !ec;
}
if (assembly.empty()) continue;
if (!sc.lastBinaryVerified) {
std::error_code ec;
if (!fs::exists(assembly, ec) || ec) continue;
sc.lastBinaryVerified = true;
}
managedRuntime.tickModule(assembly, sc.managedType, ctx, delta, specMode, testMode);
} else {
fs::path binary;
if (!sc.lastBinaryPath.empty()) {
if (sc.lastBinaryVerified) {
binary = fs::path(sc.lastBinaryPath);
} else {
fs::path cachedBinary = sc.lastBinaryPath;
if (fs::exists(cachedBinary)) {
binary = std::move(cachedBinary);
sc.lastBinaryVerified = true;
}
}
}
if (binary.empty()) {
binary = resolveScriptBinary(sc.path);
sc.lastBinaryPath = binary.string();
}
bool hasBinary = !binary.empty();
if (!hasBinary || !sc.lastBinaryVerified) {
std::error_code ec;
hasBinary = !binary.empty() && fs::exists(binary, ec) && !ec;
sc.lastBinaryVerified = hasBinary;
}
if (!hasBinary) {
fs::path scriptPath(sc.path);
std::string missingKey = scriptPath.lexically_normal().string();
if (nativeScriptMissingLogged.insert(missingKey).second) {
std::cerr << "[Script] Native script binary missing for '" << sc.path
<< "'. Compile scripts before running/exporting.\n";
addConsoleMessage(
"Native script binary missing for '" + sc.path +
"'. Compile scripts before running/exporting.",
ConsoleMessageType::Warning);
}
continue;
}
std::string binaryKey = binary.lexically_normal().string();
nativeScriptMissingLogged.erase(fs::path(sc.path).lexically_normal().string());
scriptRuntime.tickModule(binary, ctx, delta, specMode, testMode);
const std::string& runtimeError = scriptRuntime.getLastError();
if (!runtimeError.empty()) {
std::string errorKey = binaryKey + "|" + runtimeError;
if (nativeScriptLoadErrorLogged.insert(errorKey).second) {
std::cerr << "[Script] Failed to load native script '" << binary.filename().string()
<< "': " << runtimeError << "\n";
addConsoleMessage(
"Failed to load native script '" + binary.filename().string() +
"': " + runtimeError,
ConsoleMessageType::Error);
}
}
}
}
}
@@ -3122,22 +3253,55 @@ void Engine::updatePlayerController(float delta) {
void Engine::updateRigidbody2D(float delta) {
if (delta <= 0.0f) return;
refreshSceneObjectIndexCache();
const float gravity = -9.81f;
const float minEdgeThickness = 0.01f;
auto getParentOffset = [&](const SceneObject& obj) {
glm::vec2 offset(0.0f);
const SceneObject* current = &obj;
while (current && current->parentId >= 0) {
auto pit = std::find_if(sceneObjects.begin(), sceneObjects.end(),
[&](const SceneObject& o) { return o.id == current->parentId; });
if (pit == sceneObjects.end()) break;
current = &(*pit);
if (current->hasUI && current->ui.type != UIElementType::None) {
offset += glm::vec2(current->ui.position.x, current->ui.position.y);
}
struct UiHierarchyCache {
const std::vector<SceneObject>& objects;
const std::unordered_map<int, size_t>& indexById;
std::unordered_map<int, glm::vec2> worldPositionCache;
UiHierarchyCache(const std::vector<SceneObject>& objects,
const std::unordered_map<int, size_t>& indexById)
: objects(objects), indexById(indexById) {
worldPositionCache.reserve(objects.size());
}
glm::vec2 getWorldPosition(const SceneObject& obj) {
if (!(obj.hasUI && obj.ui.type != UIElementType::None)) {
return glm::vec2(obj.position.x, obj.position.y);
}
auto cached = worldPositionCache.find(obj.id);
if (cached != worldPositionCache.end()) {
return cached->second;
}
glm::vec2 pos(obj.ui.position.x, obj.ui.position.y);
if (obj.parentId >= 0) {
auto it = indexById.find(obj.parentId);
if (it != indexById.end()) {
pos += getWorldPosition(objects[it->second]);
}
}
worldPositionCache.emplace(obj.id, pos);
return pos;
}
glm::vec2 getParentOffset(const SceneObject& obj) {
if (obj.parentId < 0) {
return glm::vec2(0.0f);
}
auto it = indexById.find(obj.parentId);
if (it == indexById.end()) {
return glm::vec2(0.0f);
}
return getWorldPosition(objects[it->second]);
}
return offset;
};
UiHierarchyCache uiHierarchyCache(sceneObjects, sceneObjectIndexById);
auto rotatePoint = [](const glm::vec2& p, float c, float s) {
return glm::vec2(p.x * c - p.y * s, p.x * s + p.y * c);
};
@@ -3226,7 +3390,6 @@ void Engine::updateRigidbody2D(float delta) {
struct Body2DRef {
int index = -1;
bool dynamic = false;
glm::vec2 parentOffset = glm::vec2(0.0f);
glm::vec2 pivotWorld = glm::vec2(0.0f);
float rotationRad = 0.0f;
std::vector<glm::vec2> poly;
@@ -3238,6 +3401,8 @@ void Engine::updateRigidbody2D(float delta) {
};
std::vector<Body2DRef> bodies;
bodies.reserve(sceneObjects.size());
float broadPhaseExtentSum = 0.0f;
size_t broadPhaseExtentCount = 0;
for (auto& obj : sceneObjects) {
if (!obj.enabled || !HasUIComponent(obj)) continue;
bool hasDynamic = obj.hasRigidbody2D && obj.rigidbody2D.enabled;
@@ -3260,20 +3425,21 @@ void Engine::updateRigidbody2D(float delta) {
Body2DRef body;
body.index = static_cast<int>(&obj - &sceneObjects[0]);
body.dynamic = hasDynamic;
body.parentOffset = getParentOffset(obj);
body.pivotWorld = body.parentOffset + obj.ui.position;
body.pivotWorld = uiHierarchyCache.getParentOffset(obj) + obj.ui.position;
body.rotationRad = glm::radians(obj.ui.rotation);
float c = std::cos(body.rotationRad);
float s = std::sin(body.rotationRad);
glm::vec2 size = glm::vec2(std::max(1.0f, obj.ui.size.x), std::max(1.0f, obj.ui.size.y));
Collider2DType type = Collider2DType::Box;
glm::vec2 boxSize = size;
glm::vec2 colliderOffset(0.0f);
std::vector<glm::vec2> localPoints;
bool closed = false;
float edgeThickness = minEdgeThickness;
if (hasCollider2D) {
type = obj.collider2D.type;
boxSize = obj.collider2D.boxSize;
colliderOffset = obj.collider2D.offset;
if (boxSize.x <= 0.0f || boxSize.y <= 0.0f) {
boxSize = size;
}
@@ -3284,10 +3450,10 @@ void Engine::updateRigidbody2D(float delta) {
if (type == Collider2DType::Box) {
glm::vec2 half = boxSize * 0.5f;
localPoints = {
glm::vec2(-half.x, -half.y),
glm::vec2( half.x, -half.y),
glm::vec2( half.x, half.y),
glm::vec2(-half.x, half.y)
glm::vec2(-half.x, -half.y) + colliderOffset,
glm::vec2( half.x, -half.y) + colliderOffset,
glm::vec2( half.x, half.y) + colliderOffset,
glm::vec2(-half.x, half.y) + colliderOffset
};
} else if (type == Collider2DType::Polygon) {
if (localPoints.empty()) {
@@ -3297,7 +3463,15 @@ void Engine::updateRigidbody2D(float delta) {
} else if (type == Collider2DType::Edge) {
if (localPoints.size() < 2) {
float half = boxSize.x * 0.5f;
localPoints = { glm::vec2(-half, 0.0f), glm::vec2(half, 0.0f) };
localPoints = {
glm::vec2(-half, 0.0f) + colliderOffset,
glm::vec2(half, 0.0f) + colliderOffset
};
}
}
if (type != Collider2DType::Box && (colliderOffset.x != 0.0f || colliderOffset.y != 0.0f)) {
for (glm::vec2& point : localPoints) {
point += colliderOffset;
}
}
@@ -3314,6 +3488,22 @@ void Engine::updateRigidbody2D(float delta) {
glm::vec2 b = rotatePoint(localPoints.front(), c, s) + body.pivotWorld;
body.segments.emplace_back(a, b);
}
bool hasBounds = false;
for (const auto& seg : body.segments) {
const float halfThickness = std::max(minEdgeThickness, body.edgeThickness) * 0.5f;
glm::vec2 segMin(std::min(seg.first.x, seg.second.x) - halfThickness,
std::min(seg.first.y, seg.second.y) - halfThickness);
glm::vec2 segMax(std::max(seg.first.x, seg.second.x) + halfThickness,
std::max(seg.first.y, seg.second.y) + halfThickness);
if (!hasBounds) {
body.aabbMin = segMin;
body.aabbMax = segMax;
hasBounds = true;
} else {
body.aabbMin = glm::min(body.aabbMin, segMin);
body.aabbMax = glm::max(body.aabbMax, segMax);
}
}
} else {
body.poly.reserve(localPoints.size());
for (const auto& p : localPoints) {
@@ -3321,6 +3511,9 @@ void Engine::updateRigidbody2D(float delta) {
}
computeAabb(body.poly, body.aabbMin, body.aabbMax);
}
glm::vec2 bodySize = body.aabbMax - body.aabbMin;
broadPhaseExtentSum += std::max(bodySize.x, bodySize.y);
++broadPhaseExtentCount;
bodies.push_back(body);
}
@@ -3347,97 +3540,156 @@ void Engine::updateRigidbody2D(float delta) {
}
};
for (size_t i = 0; i < bodies.size(); ++i) {
for (size_t j = i + 1; j < bodies.size(); ++j) {
Body2DRef& a = bodies[i];
Body2DRef& b = bodies[j];
if (!a.dynamic && !b.dynamic) continue;
const float broadPhaseCellSize = std::clamp(
broadPhaseExtentCount > 0 ? (broadPhaseExtentSum / static_cast<float>(broadPhaseExtentCount)) : 64.0f,
16.0f,
512.0f);
auto makeCellKey = [](int x, int y) -> uint64_t {
return (static_cast<uint64_t>(static_cast<uint32_t>(x)) << 32) |
static_cast<uint32_t>(y);
};
std::unordered_map<uint64_t, std::vector<int>> broadPhaseCells;
broadPhaseCells.reserve(bodies.size() * 2);
for (size_t bodyIndex = 0; bodyIndex < bodies.size(); ++bodyIndex) {
const Body2DRef& body = bodies[bodyIndex];
int minCellX = static_cast<int>(std::floor(body.aabbMin.x / broadPhaseCellSize));
int maxCellX = static_cast<int>(std::floor(body.aabbMax.x / broadPhaseCellSize));
int minCellY = static_cast<int>(std::floor(body.aabbMin.y / broadPhaseCellSize));
int maxCellY = static_cast<int>(std::floor(body.aabbMax.y / broadPhaseCellSize));
for (int cellY = minCellY; cellY <= maxCellY; ++cellY) {
for (int cellX = minCellX; cellX <= maxCellX; ++cellX) {
broadPhaseCells[makeCellKey(cellX, cellY)].push_back(static_cast<int>(bodyIndex));
}
}
}
auto polyVsPoly = [&](Body2DRef& pA, Body2DRef& pB) {
if (pA.poly.empty() || pB.poly.empty()) return;
if (pA.aabbMax.x <= pB.aabbMin.x || pA.aabbMin.x >= pB.aabbMax.x ||
pA.aabbMax.y <= pB.aabbMin.y || pA.aabbMin.y >= pB.aabbMax.y) {
return;
std::unordered_set<uint64_t> candidatePairs;
candidatePairs.reserve(bodies.size() * 4);
for (const auto& [cellKey, indices] : broadPhaseCells) {
(void)cellKey;
for (size_t i = 0; i < indices.size(); ++i) {
for (size_t j = i + 1; j < indices.size(); ++j) {
uint32_t aIndex = static_cast<uint32_t>(std::min(indices[i], indices[j]));
uint32_t bIndex = static_cast<uint32_t>(std::max(indices[i], indices[j]));
candidatePairs.insert((static_cast<uint64_t>(aIndex) << 32) | bIndex);
}
}
}
for (uint64_t pairKey : candidatePairs) {
const size_t i = static_cast<size_t>(pairKey >> 32);
const size_t j = static_cast<size_t>(pairKey & 0xffffffffu);
if (i >= bodies.size() || j >= bodies.size()) continue;
Body2DRef& a = bodies[i];
Body2DRef& b = bodies[j];
if (!a.dynamic && !b.dynamic) continue;
if (a.aabbMax.x <= b.aabbMin.x || a.aabbMin.x >= b.aabbMax.x ||
a.aabbMax.y <= b.aabbMin.y || a.aabbMin.y >= b.aabbMax.y) {
continue;
}
auto polyVsPoly = [&](Body2DRef& pA, Body2DRef& pB) {
if (pA.poly.empty() || pB.poly.empty()) return;
glm::vec2 axis(0.0f);
float depth = 0.0f;
if (!satOverlap(pA.poly, pB.poly, axis, depth)) return;
glm::vec2 sep = axis * depth;
if (pA.dynamic && pB.dynamic) {
applySeparation(pA, -sep * 0.5f, -axis);
applySeparation(pB, sep * 0.5f, axis);
} else if (pA.dynamic) {
applySeparation(pA, -sep, -axis);
} else if (pB.dynamic) {
applySeparation(pB, sep, axis);
}
};
auto polyVsEdge = [&](Body2DRef& polyBody, Body2DRef& edgeBody) {
if (polyBody.poly.empty() || edgeBody.segments.empty()) return;
std::vector<glm::vec2> rect;
for (const auto& seg : edgeBody.segments) {
segmentRect(seg.first, seg.second, edgeBody.edgeThickness, rect);
if (rect.size() < 3) continue;
glm::vec2 rectMin(0.0f);
glm::vec2 rectMax(0.0f);
computeAabb(rect, rectMin, rectMax);
if (polyBody.aabbMax.x <= rectMin.x || polyBody.aabbMin.x >= rectMax.x ||
polyBody.aabbMax.y <= rectMin.y || polyBody.aabbMin.y >= rectMax.y) {
continue;
}
glm::vec2 axis(0.0f);
float depth = 0.0f;
if (!satOverlap(pA.poly, pB.poly, axis, depth)) return;
if (!satOverlap(polyBody.poly, rect, axis, depth)) continue;
glm::vec2 sep = axis * depth;
if (pA.dynamic && pB.dynamic) {
applySeparation(pA, -sep * 0.5f, -axis);
applySeparation(pB, sep * 0.5f, axis);
} else if (pA.dynamic) {
applySeparation(pA, -sep, -axis);
} else if (pB.dynamic) {
applySeparation(pB, sep, axis);
if (polyBody.dynamic && edgeBody.dynamic) {
applySeparation(polyBody, -sep * 0.5f, -axis);
applySeparation(edgeBody, sep * 0.5f, axis);
} else if (polyBody.dynamic) {
applySeparation(polyBody, -sep, -axis);
} else if (edgeBody.dynamic) {
applySeparation(edgeBody, sep, axis);
}
};
auto polyVsEdge = [&](Body2DRef& polyBody, Body2DRef& edgeBody) {
if (polyBody.poly.empty() || edgeBody.segments.empty()) return;
std::vector<glm::vec2> rect;
for (const auto& seg : edgeBody.segments) {
segmentRect(seg.first, seg.second, edgeBody.edgeThickness, rect);
if (rect.size() < 3) continue;
glm::vec2 axis(0.0f);
float depth = 0.0f;
if (!satOverlap(polyBody.poly, rect, axis, depth)) continue;
glm::vec2 sep = axis * depth;
if (polyBody.dynamic && edgeBody.dynamic) {
applySeparation(polyBody, -sep * 0.5f, -axis);
applySeparation(edgeBody, sep * 0.5f, axis);
} else if (polyBody.dynamic) {
applySeparation(polyBody, -sep, -axis);
} else if (edgeBody.dynamic) {
applySeparation(edgeBody, sep, axis);
}
}
};
if (!a.isEdge && !b.isEdge) {
polyVsPoly(a, b);
} else if (!a.isEdge && b.isEdge) {
polyVsEdge(a, b);
} else if (a.isEdge && !b.isEdge) {
polyVsEdge(b, a);
}
};
if (!a.isEdge && !b.isEdge) {
polyVsPoly(a, b);
} else if (!a.isEdge && b.isEdge) {
polyVsEdge(a, b);
} else if (a.isEdge && !b.isEdge) {
polyVsEdge(b, a);
}
}
}
void Engine::updateCameraFollow2D(float delta) {
if (sceneObjects.empty()) return;
refreshSceneObjectIndexCache();
struct UiHierarchyCache {
const std::vector<SceneObject>& objects;
const std::unordered_map<int, size_t>& indexById;
std::unordered_map<int, glm::vec2> worldPositionCache;
std::unordered_map<int, size_t> indexById;
indexById.reserve(sceneObjects.size());
for (size_t i = 0; i < sceneObjects.size(); ++i) {
indexById[sceneObjects[i].id] = i;
}
auto getUiWorldPosition = [&](const SceneObject& target) {
glm::vec2 pos(target.ui.position.x, target.ui.position.y);
int parentId = target.parentId;
while (parentId >= 0) {
auto it = indexById.find(parentId);
if (it == indexById.end()) break;
const SceneObject& parent = sceneObjects[it->second];
if (parent.hasUI && parent.ui.type != UIElementType::None) {
pos += glm::vec2(parent.ui.position.x, parent.ui.position.y);
}
parentId = parent.parentId;
UiHierarchyCache(const std::vector<SceneObject>& objects,
const std::unordered_map<int, size_t>& indexById)
: objects(objects), indexById(indexById) {
worldPositionCache.reserve(objects.size());
}
glm::vec2 getWorldPosition(const SceneObject& obj) {
if (!(obj.hasUI && obj.ui.type != UIElementType::None)) {
return glm::vec2(obj.position.x, obj.position.y);
}
auto cached = worldPositionCache.find(obj.id);
if (cached != worldPositionCache.end()) {
return cached->second;
}
glm::vec2 pos(obj.ui.position.x, obj.ui.position.y);
if (obj.parentId >= 0) {
auto it = indexById.find(obj.parentId);
if (it != indexById.end()) {
pos += getWorldPosition(objects[it->second]);
}
}
worldPositionCache.emplace(obj.id, pos);
return pos;
}
return pos;
};
UiHierarchyCache uiHierarchyCache(sceneObjects, sceneObjectIndexById);
for (auto& obj : sceneObjects) {
if (!obj.enabled || !obj.hasCamera || !obj.hasCameraFollow2D || !obj.cameraFollow2D.enabled) continue;
if (obj.cameraFollow2D.targetId < 0) continue;
auto targetIt = indexById.find(obj.cameraFollow2D.targetId);
if (targetIt == indexById.end()) continue;
auto targetIt = sceneObjectIndexById.find(obj.cameraFollow2D.targetId);
if (targetIt == sceneObjectIndexById.end()) continue;
const SceneObject& target = sceneObjects[targetIt->second];
glm::vec2 desired2D = (target.hasUI && target.ui.type != UIElementType::None)
? getUiWorldPosition(target)
? uiHierarchyCache.getWorldPosition(target)
: glm::vec2(target.position.x, target.position.y);
desired2D += obj.cameraFollow2D.offset;
glm::vec3 desired(desired2D.x, desired2D.y, obj.position.z);
@@ -3453,8 +3705,8 @@ void Engine::updateCameraFollow2D(float delta) {
obj.localPosition = obj.position;
obj.localInitialized = true;
} else {
auto parentIt = indexById.find(obj.parentId);
if (parentIt != indexById.end()) {
auto parentIt = sceneObjectIndexById.find(obj.parentId);
if (parentIt != sceneObjectIndexById.end()) {
const SceneObject& parent = sceneObjects[parentIt->second];
updateLocalFromWorld(obj,
parent.position,
@@ -3690,13 +3942,70 @@ void Engine::initializeLocalTransformsFromWorld(int sceneVersion) {
updateHierarchyWorldTransforms();
}
void Engine::refreshSceneObjectIndexCache() {
const SceneObject* currentData = sceneObjects.empty() ? nullptr : sceneObjects.data();
if (sceneObjectIndexData == currentData &&
sceneObjectIndexCount == sceneObjects.size() &&
sceneObjectIndexById.size() == sceneObjects.size()) {
return;
}
sceneObjectIndexById.clear();
sceneObjectIndexById.reserve(sceneObjects.size());
for (size_t i = 0; i < sceneObjects.size(); ++i) {
sceneObjectIndexById[sceneObjects[i].id] = i;
}
sceneObjectIndexData = currentData;
sceneObjectIndexCount = sceneObjects.size();
}
void Engine::rebuildRuntimeScriptBindings() {
runtimeScriptBindings.clear();
runtimeScriptBindings.reserve(sceneObjects.size());
for (const auto& obj : sceneObjects) {
if (obj.scripts.empty()) continue;
for (size_t i = 0; i < obj.scripts.size(); ++i) {
runtimeScriptBindings.push_back({obj.id, i});
}
}
runtimeScriptBindingsCachedVersion = runtimeScriptBindingsVersion;
}
void Engine::updateHierarchyWorldTransforms() {
if (sceneObjects.empty()) return;
refreshSceneObjectIndexCache();
std::unordered_map<int, size_t> indexById;
indexById.reserve(sceneObjects.size());
for (size_t i = 0; i < sceneObjects.size(); ++i) {
indexById[sceneObjects[i].id] = i;
bool hasHierarchyLinks = false;
for (const auto& obj : sceneObjects) {
if (obj.parentId != -1 || !obj.childIds.empty()) {
hasHierarchyLinks = true;
break;
}
}
if (!hasHierarchyLinks) {
const glm::vec3 rootPos(0.0f);
const glm::quat rootRot(1.0f, 0.0f, 0.0f, 0.0f);
const glm::vec3 rootScale(1.0f);
for (auto& obj : sceneObjects) {
if (!obj.localInitialized) {
obj.localPosition = obj.position;
obj.localRotation = NormalizeEulerDegrees(obj.rotation);
obj.localScale = obj.scale;
obj.localInitialized = true;
}
bool useWorldAuthoritative = obj.hasRigidbody && obj.rigidbody.enabled && !obj.rigidbody.isKinematic;
if (useWorldAuthoritative) {
updateLocalFromWorld(obj, rootPos, rootRot, rootScale);
continue;
}
obj.position = obj.localPosition;
obj.rotation = NormalizeEulerDegrees(obj.localRotation);
obj.scale = obj.localScale;
}
return;
}
auto unwrapNear = [](float angle, float reference) {
@@ -3715,8 +4024,8 @@ void Engine::updateHierarchyWorldTransforms() {
[&](int id, const glm::vec3& parentPos, const glm::quat& parentRot, const glm::vec3& parentScale) {
if (visited.count(id)) return;
if (visiting.count(id)) return;
auto itIndex = indexById.find(id);
if (itIndex == indexById.end()) return;
auto itIndex = sceneObjectIndexById.find(id);
if (itIndex == sceneObjectIndexById.end()) return;
visiting.insert(id);
SceneObject& obj = sceneObjects[itIndex->second];
@@ -3766,7 +4075,7 @@ void Engine::updateHierarchyWorldTransforms() {
};
for (const auto& obj : sceneObjects) {
if (obj.parentId == -1 || indexById.find(obj.parentId) == indexById.end()) {
if (obj.parentId == -1 || sceneObjectIndexById.find(obj.parentId) == sceneObjectIndexById.end()) {
processNode(obj.id, glm::vec3(0.0f), glm::quat(1.0f, 0.0f, 0.0f, 0.0f), glm::vec3(1.0f));
}
}
@@ -5316,6 +5625,7 @@ void Engine::loadScene(const std::string& sceneName) {
int sceneVersion = 9;
float loadedTimeOfDay = -1.0f;
if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId, sceneVersion, &loadedTimeOfDay)) {
markRuntimeScriptBindingsDirty();
initializeLocalTransformsFromWorld(sceneVersion);
rebuildSkeletalBindings();
undoStack.clear();
@@ -5348,6 +5658,7 @@ void Engine::createNewScene(const std::string& sceneName) {
}
sceneObjects.clear();
markRuntimeScriptBindingsDirty();
clearSelection();
nextObjectId = 0;
undoStack.clear();
@@ -5378,6 +5689,7 @@ void Engine::addObject(ObjectType type, const std::string& baseName) {
obj.localScale = obj.scale;
obj.localInitialized = true;
sceneObjects.push_back(obj);
markRuntimeScriptBindingsDirty();
setPrimarySelection(id);
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
@@ -5452,6 +5764,7 @@ void Engine::duplicateSelected() {
newObj.ui = it->ui;
sceneObjects.push_back(newObj);
markRuntimeScriptBindingsDirty();
setPrimarySelection(id);
if (projectManager.currentProject.isLoaded) {
projectManager.currentProject.hasUnsavedChanges = true;
@@ -5510,6 +5823,7 @@ void Engine::deleteSelected() {
if (it != sceneObjects.end()) {
sceneObjects.erase(it, sceneObjects.end());
markRuntimeScriptBindingsDirty();
for (auto& obj : sceneObjects) {
if (toDelete.count(obj.parentId) > 0) {
obj.parentId = -1;
@@ -5675,11 +5989,11 @@ SceneObject* Engine::findObjectByName(const std::string& name) {
}
SceneObject* Engine::findObjectById(int id) {
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(), [&](const SceneObject& o) {
return o.id == id;
});
if (it != sceneObjects.end()) return &(*it);
return nullptr;
refreshSceneObjectIndexCache();
auto it = sceneObjectIndexById.find(id);
if (it == sceneObjectIndexById.end()) return nullptr;
if (it->second >= sceneObjects.size()) return nullptr;
return &sceneObjects[it->second];
}
#pragma endregion
@@ -6197,6 +6511,7 @@ void Engine::updateCompileJob() {
for (auto& sc : obj.scripts) {
if (sc.language != ScriptLanguage::CSharp) continue;
sc.lastBinaryPath = result.binaryPath.string();
sc.lastBinaryVerified = true;
}
}
} else {
@@ -6207,6 +6522,7 @@ void Engine::updateCompileJob() {
std::string scPathNorm = (ec ? fs::path(sc.path) : scAbs).lexically_normal().string();
if (scPathNorm == result.compiledSource) {
sc.lastBinaryPath = result.binaryPath.string();
sc.lastBinaryVerified = true;
}
}
}

View File

@@ -60,10 +60,26 @@ private:
std::vector<int> selectedIds;
int nextId = 0;
};
struct PlayModeSnapshot {
SceneSnapshot scene;
bool hadUnsavedChanges = false;
bool valid = false;
};
std::vector<SceneSnapshot> undoStack;
std::vector<SceneSnapshot> redoStack;
PlayModeSnapshot playModeSnapshot;
std::vector<SceneObject> sceneObjects;
std::unordered_map<int, size_t> sceneObjectIndexById;
const SceneObject* sceneObjectIndexData = nullptr;
size_t sceneObjectIndexCount = 0;
struct RuntimeScriptBinding {
int objectId = -1;
size_t scriptIndex = 0;
};
std::vector<RuntimeScriptBinding> runtimeScriptBindings;
uint64_t runtimeScriptBindingsVersion = 1;
uint64_t runtimeScriptBindingsCachedVersion = 0;
int selectedObjectId = -1; // primary selection (last)
std::vector<int> selectedObjectIds; // multi-select
int nextObjectId = 0;
@@ -114,6 +130,7 @@ private:
double launcherTransitionStartTime = 0.0;
ImVec2 launcherTransitionFocus = ImVec2(0.0f, 0.0f);
std::string launcherLoadingPreviewPath;
bool termsPopupOpened = false;
bool showNewSceneDialog = false;
bool showSaveSceneAsDialog = false;
char newSceneName[128] = "";
@@ -274,6 +291,7 @@ private:
bool strictValidation = false;
std::vector<glm::ivec4> spriteFrames;
std::vector<std::string> spriteFrameNames;
std::vector<glm::vec2> spriteFrameScales;
std::vector<SpritesheetLayer> layers;
int activeLayer = 0;
int activeFrame = 0;
@@ -289,6 +307,7 @@ private:
bool strictValidation = false;
std::vector<glm::ivec4> spriteFrames;
std::vector<std::string> spriteFrameNames;
std::vector<glm::vec2> spriteFrameScales;
std::vector<SpritesheetLayer> layers;
int activeLayer = 0;
int activeFrame = 0;
@@ -540,6 +559,9 @@ private:
static void DecomposeMatrix(const glm::mat4& matrix, glm::vec3& pos, glm::vec3& rot, glm::vec3& scale);
static glm::mat4 ComposeTransform(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& scale);
static glm::mat4 ComposeTransform(const glm::vec3& position, const glm::vec3& rotationDeg, const glm::vec3& scale);
void refreshSceneObjectIndexCache();
void markRuntimeScriptBindingsDirty() { runtimeScriptBindingsVersion++; }
void rebuildRuntimeScriptBindings();
void updateHierarchyWorldTransforms();
void updateLocalFromWorld(SceneObject& obj, const glm::vec3& parentPos, const glm::quat& parentRot, const glm::vec3& parentScale);
void initializeLocalTransformsFromWorld(int sceneVersion);
@@ -556,6 +578,8 @@ private:
// UI rendering methods
void renderLauncher();
bool requiresTermsOfServiceAcceptance() const;
void renderTermsOfServiceModal();
void renderNewProjectDialog();
void renderOpenProjectDialog();
void renderMainMenuBar();
@@ -668,6 +692,8 @@ private:
void loadMaterialFromFile(SceneObject& obj);
void saveMaterialToFile(const SceneObject& obj);
void recordState(const char* reason = "");
void capturePlayModeSnapshot();
void restorePlayModeSnapshot();
void undo();
void redo();

View File

@@ -992,6 +992,7 @@ static bool buildSceneMeshes(const std::string& filepath, const aiScene* scene,
loaded.isSkinned = isSkinned;
loaded.boneNames = std::move(boneNames);
loaded.inverseBindMatrices = std::move(inverseBindMatrices);
loaded.baseVertices = vertices;
if (isSkinned) {
loaded.boneIds.reserve(boneVertices.size());
loaded.boneWeights.reserve(boneVertices.size());
@@ -999,7 +1000,6 @@ static bool buildSceneMeshes(const std::string& filepath, const aiScene* scene,
loaded.boneIds.emplace_back(bv.ids[0], bv.ids[1], bv.ids[2], bv.ids[3]);
loaded.boneWeights.emplace_back(bv.weights[0], bv.weights[1], bv.weights[2], bv.weights[3]);
}
loaded.baseVertices = vertices;
}
out.meshMaterialIndices[i] = mesh->mMaterialIndex < (int)out.materials.size()

View File

@@ -300,6 +300,7 @@ bool PhysicsSystem::attachColliderShape(PxRigidActor* actor, const SceneObject&
bool ownsMaterial = false;
PxMaterial* shapeMaterial = CreateShapeMaterial(mPhysics, mDefaultMaterial, obj, ownsMaterial);
PxShape* shape = nullptr;
const PxVec3 localOffset = ToPxVec3(obj.collider.offset);
auto tuneShape = [](PxShape* s, float minDim, bool /*swept*/) {
if (!s) return;
float contact = std::clamp(minDim * 0.12f, 0.015f, 0.12f);
@@ -311,6 +312,9 @@ bool PhysicsSystem::attachColliderShape(PxRigidActor* actor, const SceneObject&
if (obj.collider.type == ColliderType::Box) {
glm::vec3 half = glm::max(obj.collider.boxSize * 0.5f, glm::vec3(0.01f));
shape = mPhysics->createShape(PxBoxGeometry(ToPxVec3(half)), *shapeMaterial, true);
if (shape) {
shape->setLocalPose(PxTransform(localOffset, PxQuat(PxIdentity)));
}
minDim = std::min({half.x, half.y, half.z}) * 2.0f;
} else if (obj.collider.type == ColliderType::Capsule) {
float radius = std::max({obj.collider.boxSize.x, obj.collider.boxSize.z}) * 0.5f;
@@ -320,7 +324,7 @@ bool PhysicsSystem::attachColliderShape(PxRigidActor* actor, const SceneObject&
shape = mPhysics->createShape(PxCapsuleGeometry(radius, halfHeight), *shapeMaterial, true);
if (shape) {
// Rotate capsule so its axis matches the engine's Y-up expectation
shape->setLocalPose(PxTransform(PxQuat(PxHalfPi, PxVec3(0, 0, 1))));
shape->setLocalPose(PxTransform(localOffset, PxQuat(PxHalfPi, PxVec3(0, 0, 1))));
}
minDim = std::min(radius * 2.0f, halfHeight * 2.0f);
} else {
@@ -344,7 +348,7 @@ bool PhysicsSystem::attachColliderShape(PxRigidActor* actor, const SceneObject&
glm::vec3 center = (boundsMax + boundsMin) * 0.5f;
PxShape* box = mPhysics->createShape(PxBoxGeometry(ToPxVec3(halfExtents)), *shapeMaterial, true);
if (box) {
box->setLocalPose(PxTransform(ToPxVec3(center), PxQuat(PxIdentity)));
box->setLocalPose(PxTransform(ToPxVec3(center) + localOffset, PxQuat(PxIdentity)));
}
return box;
};
@@ -401,6 +405,9 @@ bool PhysicsSystem::attachColliderShape(PxRigidActor* actor, const SceneObject&
if (convex) {
PxConvexMeshGeometry geom(convex, PxMeshScale(ToPxVec3(obj.scale), PxQuat(PxIdentity)));
shape = mPhysics->createShape(geom, *shapeMaterial, true);
if (shape) {
shape->setLocalPose(PxTransform(localOffset, PxQuat(PxIdentity)));
}
convex->release();
}
} else {
@@ -408,6 +415,9 @@ bool PhysicsSystem::attachColliderShape(PxRigidActor* actor, const SceneObject&
if (tri) {
PxTriangleMeshGeometry geom(tri, PxMeshScale(ToPxVec3(obj.scale), PxQuat(PxIdentity)));
shape = mPhysics->createShape(geom, *shapeMaterial, true);
if (shape) {
shape->setLocalPose(PxTransform(localOffset, PxQuat(PxIdentity)));
}
tri->release();
}
}

View File

@@ -297,6 +297,7 @@ void ProjectManager::saveRecentProjects() {
void ProjectManager::loadLauncherSettings() {
defaultProjectLocation[0] = '\0';
acceptedTermsVersion.clear();
fs::path settingsFile = appDataPath / "launcher_settings.modu";
if (fs::exists(settingsFile)) {
@@ -313,6 +314,8 @@ void ProjectManager::loadLauncherSettings() {
const std::string value = TrimCopy(cleaned.substr(eq + 1));
if (key == "defaultProjectLocation" && !value.empty()) {
std::snprintf(defaultProjectLocation, sizeof(defaultProjectLocation), "%s", value.c_str());
} else if (key == "acceptedTermsVersion") {
acceptedTermsVersion = value;
}
}
}
@@ -331,6 +334,9 @@ void ProjectManager::saveLauncherSettings() const {
}
file << "# Modularity launcher settings\n";
if (!acceptedTermsVersion.empty()) {
file << "acceptedTermsVersion=" << acceptedTermsVersion << "\n";
}
file << "defaultProjectLocation=" << defaultProjectLocation << "\n";
}
@@ -439,6 +445,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "collider2dEnabled=" << (obj.collider2D.enabled ? 1 : 0) << "\n";
file << "collider2dType=" << static_cast<int>(obj.collider2D.type) << "\n";
file << "collider2dBox=" << obj.collider2D.boxSize.x << "," << obj.collider2D.boxSize.y << "\n";
file << "collider2dOffset=" << obj.collider2D.offset.x << "," << obj.collider2D.offset.y << "\n";
file << "collider2dClosed=" << (obj.collider2D.closed ? 1 : 0) << "\n";
file << "collider2dEdgeThickness=" << obj.collider2D.edgeThickness << "\n";
file << "collider2dPoints=";
@@ -469,6 +476,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "colliderEnabled=" << (obj.collider.enabled ? 1 : 0) << "\n";
file << "colliderType=" << static_cast<int>(obj.collider.type) << "\n";
file << "colliderBox=" << obj.collider.boxSize.x << "," << obj.collider.boxSize.y << "," << obj.collider.boxSize.z << "\n";
file << "colliderOffset=" << obj.collider.offset.x << "," << obj.collider.offset.y << "," << obj.collider.offset.z << "\n";
file << "colliderConvex=" << (obj.collider.convex ? 1 : 0) << "\n";
file << "colliderStaticFriction=" << obj.collider.staticFriction << "\n";
file << "colliderDynamicFriction=" << obj.collider.dynamicFriction << "\n";
@@ -613,6 +621,7 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "skelMaxBones=" << obj.skeletal.maxBones << "\n";
}
file << "materialColor=" << obj.material.color.r << "," << obj.material.color.g << "," << obj.material.color.b << "\n";
file << "materialAlpha=" << obj.material.alpha << "\n";
file << "materialAmbient=" << obj.material.ambientStrength << "\n";
file << "materialSpecular=" << obj.material.specularStrength << "\n";
file << "materialShininess=" << obj.material.shininess << "\n";
@@ -703,10 +712,28 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
}
file << "\n";
}
if (!obj.ui.spriteCustomFrameScales.empty()) {
file << "uiSpriteCustomFrameScales=";
for (size_t i = 0; i < obj.ui.spriteCustomFrameScales.size(); ++i) {
if (i > 0) file << ";";
const glm::vec2& scale = obj.ui.spriteCustomFrameScales[i];
file << scale.x << "," << scale.y;
}
file << "\n";
}
if (obj.hasPostFX) {
file << "postEnabled=" << (obj.postFx.enabled ? 1 : 0) << "\n";
file << "postVolumeGlobal=" << (obj.postFx.isGlobal ? 1 : 0) << "\n";
file << "postVolumePriority=" << obj.postFx.priority << "\n";
file << "postVolumeWeight=" << obj.postFx.blendWeight << "\n";
file << "postVolumeBlendRadius=" << obj.postFx.blendRadius << "\n";
file << "postHDREnabled=" << (obj.postFx.hdrEnabled ? 1 : 0) << "\n";
file << "postToneMapper=" << static_cast<int>(obj.postFx.toneMapper) << "\n";
file << "postWhitePoint=" << obj.postFx.whitePoint << "\n";
file << "postGamma=" << obj.postFx.gamma << "\n";
file << "postBloomEnabled=" << (obj.postFx.bloomEnabled ? 1 : 0) << "\n";
file << "postBloomThreshold=" << obj.postFx.bloomThreshold << "\n";
file << "postBloomSoftKnee=" << obj.postFx.bloomSoftKnee << "\n";
file << "postBloomIntensity=" << obj.postFx.bloomIntensity << "\n";
file << "postBloomRadius=" << obj.postFx.bloomRadius << "\n";
file << "postColorAdjustEnabled=" << (obj.postFx.colorAdjustEnabled ? 1 : 0) << "\n";
@@ -716,11 +743,15 @@ bool SceneSerializer::saveScene(const fs::path& filePath,
file << "postColorFilter=" << obj.postFx.colorFilter.r << "," << obj.postFx.colorFilter.g << "," << obj.postFx.colorFilter.b << "\n";
file << "postMotionBlurEnabled=" << (obj.postFx.motionBlurEnabled ? 1 : 0) << "\n";
file << "postMotionBlurStrength=" << obj.postFx.motionBlurStrength << "\n";
file << "postMotionBlurThreshold=" << obj.postFx.motionBlurThreshold << "\n";
file << "postMotionBlurClamp=" << obj.postFx.motionBlurClamp << "\n";
file << "postVignetteEnabled=" << (obj.postFx.vignetteEnabled ? 1 : 0) << "\n";
file << "postVignetteIntensity=" << obj.postFx.vignetteIntensity << "\n";
file << "postVignetteSmoothness=" << obj.postFx.vignetteSmoothness << "\n";
file << "postChromaticEnabled=" << (obj.postFx.chromaticAberrationEnabled ? 1 : 0) << "\n";
file << "postChromaticAmount=" << obj.postFx.chromaticAmount << "\n";
file << "postSharpenEnabled=" << (obj.postFx.sharpenEnabled ? 1 : 0) << "\n";
file << "postSharpenStrength=" << obj.postFx.sharpenStrength << "\n";
file << "postAOEnabled=" << (obj.postFx.ambientOcclusionEnabled ? 1 : 0) << "\n";
file << "postAORadius=" << obj.postFx.aoRadius << "\n";
file << "postAOStrength=" << obj.postFx.aoStrength << "\n";
@@ -993,6 +1024,7 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
{"collider2dEnabled", +[](SceneObject& obj, const std::string& value) { obj.collider2D.enabled = std::stoi(value) != 0; }},
{"collider2dType", +[](SceneObject& obj, const std::string& value) { obj.collider2D.type = static_cast<Collider2DType>(std::stoi(value)); }},
{"collider2dBox", +[](SceneObject& obj, const std::string& value) { ParseVec2(value, obj.collider2D.boxSize); }},
{"collider2dOffset", +[](SceneObject& obj, const std::string& value) { ParseVec2(value, obj.collider2D.offset); }},
{"collider2dClosed", +[](SceneObject& obj, const std::string& value) { obj.collider2D.closed = std::stoi(value) != 0; }},
{"collider2dEdgeThickness", +[](SceneObject& obj, const std::string& value) { obj.collider2D.edgeThickness = std::stof(value); }},
{"collider2dPoints", +[](SceneObject& obj, const std::string& value) { ParseVec2List(value, obj.collider2D.points); }},
@@ -1012,6 +1044,7 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
{"colliderEnabled", +[](SceneObject& obj, const std::string& value) { obj.collider.enabled = std::stoi(value) != 0; }},
{"colliderType", +[](SceneObject& obj, const std::string& value) { obj.collider.type = static_cast<ColliderType>(std::stoi(value)); }},
{"colliderBox", +[](SceneObject& obj, const std::string& value) { ParseVec3(value, obj.collider.boxSize); }},
{"colliderOffset", +[](SceneObject& obj, const std::string& value) { ParseVec3(value, obj.collider.offset); }},
{"colliderConvex", +[](SceneObject& obj, const std::string& value) { obj.collider.convex = std::stoi(value) != 0; }},
{"colliderStaticFriction", +[](SceneObject& obj, const std::string& value) { obj.collider.staticFriction = std::stof(value); }},
{"colliderDynamicFriction", +[](SceneObject& obj, const std::string& value) { obj.collider.dynamicFriction = std::stof(value); }},
@@ -1114,6 +1147,7 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
{"skelLoop", +[](SceneObject& obj, const std::string& value) { obj.skeletal.loop = std::stoi(value) != 0; }},
{"skelMaxBones", +[](SceneObject& obj, const std::string& value) { obj.skeletal.maxBones = std::stoi(value); }},
{"materialColor", +[](SceneObject& obj, const std::string& value) { ParseVec3(value, obj.material.color); }},
{"materialAlpha", +[](SceneObject& obj, const std::string& value) { obj.material.alpha = std::clamp(std::stof(value), 0.0f, 1.0f); }},
{"materialAmbient", +[](SceneObject& obj, const std::string& value) { obj.material.ambientStrength = std::stof(value); }},
{"materialSpecular", +[](SceneObject& obj, const std::string& value) { obj.material.specularStrength = std::stof(value); }},
{"materialShininess", +[](SceneObject& obj, const std::string& value) { obj.material.shininess = std::stof(value); }},
@@ -1220,9 +1254,30 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
obj.ui.spriteCustomFrameNames.push_back(item);
}
}},
{"uiSpriteCustomFrameScales", +[](SceneObject& obj, const std::string& value) {
ParseVec2List(value, obj.ui.spriteCustomFrameScales);
if (obj.ui.spriteCustomFrameScales.size() < obj.ui.spriteCustomFrames.size()) {
obj.ui.spriteCustomFrameScales.resize(obj.ui.spriteCustomFrames.size(), glm::vec2(1.0f));
} else if (obj.ui.spriteCustomFrameScales.size() > obj.ui.spriteCustomFrames.size()) {
obj.ui.spriteCustomFrameScales.resize(obj.ui.spriteCustomFrames.size());
}
for (glm::vec2& scale : obj.ui.spriteCustomFrameScales) {
scale.x = std::max(0.01f, scale.x);
scale.y = std::max(0.01f, scale.y);
}
}},
{"postEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.enabled = (std::stoi(value) != 0); }},
{"postVolumeGlobal", +[](SceneObject& obj, const std::string& value) { obj.postFx.isGlobal = (std::stoi(value) != 0); }},
{"postVolumePriority", +[](SceneObject& obj, const std::string& value) { obj.postFx.priority = std::stof(value); }},
{"postVolumeWeight", +[](SceneObject& obj, const std::string& value) { obj.postFx.blendWeight = std::stof(value); }},
{"postVolumeBlendRadius", +[](SceneObject& obj, const std::string& value) { obj.postFx.blendRadius = std::stof(value); }},
{"postHDREnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.hdrEnabled = (std::stoi(value) != 0); }},
{"postToneMapper", +[](SceneObject& obj, const std::string& value) { obj.postFx.toneMapper = static_cast<PostFXToneMapper>(std::stoi(value)); }},
{"postWhitePoint", +[](SceneObject& obj, const std::string& value) { obj.postFx.whitePoint = std::stof(value); }},
{"postGamma", +[](SceneObject& obj, const std::string& value) { obj.postFx.gamma = std::stof(value); }},
{"postBloomEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.bloomEnabled = (std::stoi(value) != 0); }},
{"postBloomThreshold", +[](SceneObject& obj, const std::string& value) { obj.postFx.bloomThreshold = std::stof(value); }},
{"postBloomSoftKnee", +[](SceneObject& obj, const std::string& value) { obj.postFx.bloomSoftKnee = std::stof(value); }},
{"postBloomIntensity", +[](SceneObject& obj, const std::string& value) { obj.postFx.bloomIntensity = std::stof(value); }},
{"postBloomRadius", +[](SceneObject& obj, const std::string& value) { obj.postFx.bloomRadius = std::stof(value); }},
{"postColorAdjustEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.colorAdjustEnabled = (std::stoi(value) != 0); }},
@@ -1232,11 +1287,15 @@ const std::unordered_map<std::string, KeyHandler>& GetSceneObjectKeyHandlers() {
{"postColorFilter", +[](SceneObject& obj, const std::string& value) { ParseVec3(value, obj.postFx.colorFilter); }},
{"postMotionBlurEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.motionBlurEnabled = (std::stoi(value) != 0); }},
{"postMotionBlurStrength", +[](SceneObject& obj, const std::string& value) { obj.postFx.motionBlurStrength = std::stof(value); }},
{"postMotionBlurThreshold", +[](SceneObject& obj, const std::string& value) { obj.postFx.motionBlurThreshold = std::stof(value); }},
{"postMotionBlurClamp", +[](SceneObject& obj, const std::string& value) { obj.postFx.motionBlurClamp = std::stof(value); }},
{"postVignetteEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.vignetteEnabled = (std::stoi(value) != 0); }},
{"postVignetteIntensity", +[](SceneObject& obj, const std::string& value) { obj.postFx.vignetteIntensity = std::stof(value); }},
{"postVignetteSmoothness", +[](SceneObject& obj, const std::string& value) { obj.postFx.vignetteSmoothness = std::stof(value); }},
{"postChromaticEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.chromaticAberrationEnabled = (std::stoi(value) != 0); }},
{"postChromaticAmount", +[](SceneObject& obj, const std::string& value) { obj.postFx.chromaticAmount = std::stof(value); }},
{"postSharpenEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.sharpenEnabled = (std::stoi(value) != 0); }},
{"postSharpenStrength", +[](SceneObject& obj, const std::string& value) { obj.postFx.sharpenStrength = std::stof(value); }},
{"postAOEnabled", +[](SceneObject& obj, const std::string& value) { obj.postFx.ambientOcclusionEnabled = (std::stoi(value) != 0); }},
{"postAORadius", +[](SceneObject& obj, const std::string& value) { obj.postFx.aoRadius = std::stof(value); }},
{"postAOStrength", +[](SceneObject& obj, const std::string& value) { obj.postFx.aoStrength = std::stof(value); }},

View File

@@ -49,6 +49,7 @@ public:
char openProjectPath[512] = "";
bool showNewProjectDialog = false;
bool showOpenProjectDialog = false;
std::string acceptedTermsVersion;
int newProjectPipelineMode = 0;
int newProjectRendererMode = 0;
bool newProjectImportLastPackages = true;

File diff suppressed because it is too large Load Diff

View File

@@ -83,6 +83,35 @@ public:
int meshDraws = 0;
int fullscreenDraws = 0;
};
struct StaticMergeBatch {
std::unique_ptr<Mesh> mesh;
std::string vertPath;
std::string fragPath;
MaterialProperties material;
std::string materialPath;
std::string albedoTexturePath;
std::string overlayTexturePath;
std::string normalMapPath;
bool useOverlay = false;
glm::vec3 boundsCenter = glm::vec3(0.0f);
float boundsRadius = 0.0f;
};
struct PostProcessStats {
float totalMs = 0.0f;
float resolveMs = 0.0f;
float bloomExtractMs = 0.0f;
float bloomBlurMs = 0.0f;
float compositeMs = 0.0f;
int activeVolumeCount = 0;
int resolvedVolumeId = -1;
std::string resolvedVolumeName;
float resolvedBlend = 0.0f;
int activeEffectCount = 0;
bool hdrEnabled = false;
bool bloomUsed = false;
bool motionBlurUsed = false;
};
private:
unsigned int framebuffer = 0, viewportTexture = 0, rbo = 0;
@@ -94,6 +123,14 @@ private:
int width = 0;
int height = 0;
bool hasAlpha = false;
bool hdr = false;
};
struct ResolvedPostFX {
PostFXSettings settings;
int activeVolumeCount = 0;
int resolvedVolumeId = -1;
std::string resolvedVolumeName;
float resolvedBlend = 0.0f;
};
RenderTarget previewTarget;
std::unordered_map<int, RenderTarget> extraPreviewTargets;
@@ -109,6 +146,15 @@ private:
unsigned int depthCube = 0;
int resolution = 0;
};
struct MirrorUpdateState {
glm::vec3 lastCameraPos = glm::vec3(FLT_MAX);
glm::vec3 lastCameraFront = glm::vec3(0.0f);
glm::vec3 lastCameraUp = glm::vec3(0.0f);
glm::vec3 lastMirrorPos = glm::vec3(FLT_MAX);
glm::vec3 lastMirrorRot = glm::vec3(FLT_MAX);
glm::vec3 lastMirrorScale = glm::vec3(FLT_MAX);
double lastUpdateTime = -1.0;
};
Shader* shader = nullptr;
Shader* defaultShader = nullptr;
Shader* postShader = nullptr;
@@ -119,8 +165,13 @@ private:
Texture* texture2 = nullptr;
unsigned int debugWhiteTexture = 0;
unsigned int missingMaterialFallbackTexture = 0;
std::unordered_map<std::string, std::unique_ptr<Texture>> textureCacheBilinear;
std::unordered_map<std::string, std::unique_ptr<Texture>> textureCachePoint;
struct CachedTextureEntry {
std::unique_ptr<Texture> texture;
size_t approxBytes = 0;
double lastUsedTime = 0.0;
};
std::unordered_map<std::string, CachedTextureEntry> textureCacheBilinear;
std::unordered_map<std::string, CachedTextureEntry> textureCachePoint;
struct ShaderEntry {
std::unique_ptr<Shader> shader;
fs::file_time_type vertTime;
@@ -154,11 +205,19 @@ private:
unsigned int quadVBO = 0;
unsigned int displayTexture = 0;
bool historyValid = false;
size_t textureCacheBudgetBytes = 384ull * 1024ull * 1024ull;
size_t textureCacheUsageBytes = 0;
uint64_t staticMergeSceneSignature = 0;
std::unordered_map<int, ShadowCubeMap> shadowCubeMaps;
std::unordered_map<int, RenderTarget> mirrorTargets;
std::unordered_map<int, MirrorUpdateState> mirrorUpdateStates;
std::unordered_map<int, RenderTarget> uiTargets;
std::vector<StaticMergeBatch> staticMergeBatches;
std::unordered_set<int> staticMergeSourceIds;
RenderStats viewportStats;
RenderStats previewStats;
PostProcessStats viewportPostStats;
PostProcessStats previewPostStats;
RenderStats* activeStats = nullptr;
float selectionOutlineBlend = 0.0f;
double selectionOutlineLastUpdateSec = 0.0;
@@ -169,10 +228,13 @@ private:
void setupFBO();
void ensureRenderTarget(RenderTarget& target, int w, int h);
void ensureRenderTarget(RenderTarget& target, int w, int h, bool alpha);
void ensureRenderTarget(RenderTarget& target, int w, int h, bool alpha, bool hdr);
void releaseRenderTarget(RenderTarget& target);
void updateMirrorTargets(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane);
void ensureQuad();
void drawFullscreenQuad();
void purgeTextureCacheIfNeeded();
void rebuildStaticMergeBatches(const std::vector<SceneObject>& sceneObjects);
void resetStats(RenderStats& stats);
void recordDrawCall();
void recordMeshDraw();
@@ -180,8 +242,8 @@ private:
void clearHistory();
void clearTarget(RenderTarget& target);
void renderSceneInternal(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, bool unbindFramebuffer, float fovDeg, float nearPlane, float farPlane, bool drawMirrorObjects);
unsigned int applyPostProcessing(const std::vector<SceneObject>& sceneObjects, unsigned int sourceTexture, int width, int height, bool allowHistory);
PostFXSettings gatherPostFX(const std::vector<SceneObject>& sceneObjects) const;
unsigned int applyPostProcessing(const Camera& camera, const std::vector<SceneObject>& sceneObjects, unsigned int sourceTexture, int width, int height, bool allowHistory);
ResolvedPostFX gatherPostFX(const Camera& camera, const std::vector<SceneObject>& sceneObjects) const;
public:
Renderer() = default;
@@ -208,13 +270,15 @@ public:
void renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int selectedId = -1, float fovDeg = FOV, float nearPlane = NEAR_PLANE, float farPlane = FAR_PLANE, bool drawColliders = false, const std::vector<int>* selectedIds = nullptr);
void renderSelectionOutline(const Camera& camera, const std::vector<SceneObject>& sceneObjects, const std::vector<int>& selectedIds, float fovDeg, float nearPlane, float farPlane);
unsigned int renderScenePreview(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane, bool applyPostFX = false, int previewSlot = 0);
void renderCollisionOverlay(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane);
void renderCollisionOverlay(const Camera& camera, const std::vector<SceneObject>& sceneObjects, int width, int height, float fovDeg, float nearPlane, float farPlane, const std::vector<int>* previewIds = nullptr);
void endRender();
Skybox* getSkybox() { return skybox; }
unsigned int getViewportTexture() const { return displayTexture ? displayTexture : viewportTexture; }
const RenderStats& getLastViewportStats() const { return viewportStats; }
const RenderStats& getLastPreviewStats() const { return previewStats; }
const PostProcessStats& getLastViewportPostStats() const { return viewportPostStats; }
const PostProcessStats& getLastPreviewPostStats() const { return previewPostStats; }
struct UiTargetInfo {
unsigned int fbo = 0;

View File

@@ -58,6 +58,7 @@ struct MaterialProperties {
};
glm::vec3 color = glm::vec3(1.0f);
float alpha = 1.0f;
float ambientStrength = 0.2f;
float specularStrength = 0.5f;
float shininess = 32.0f;
@@ -211,6 +212,12 @@ enum class SceneCameraType {
Player = 1
};
enum class PostFXToneMapper {
None = 0,
Reinhard = 1,
ACES = 2
};
struct CameraComponent {
SceneCameraType type = SceneCameraType::Scene;
float fov = FOV;
@@ -223,8 +230,17 @@ struct CameraComponent {
struct PostFXSettings {
bool enabled = true;
bool isGlobal = true;
float priority = 0.0f;
float blendWeight = 1.0f;
float blendRadius = 4.0f;
bool hdrEnabled = true;
PostFXToneMapper toneMapper = PostFXToneMapper::ACES;
float whitePoint = 4.0f;
float gamma = 2.2f;
bool bloomEnabled = true;
float bloomThreshold = 1.1f;
float bloomSoftKnee = 0.25f;
float bloomIntensity = 0.8f;
float bloomRadius = 1.5f;
bool colorAdjustEnabled = false;
@@ -234,11 +250,15 @@ struct PostFXSettings {
glm::vec3 colorFilter = glm::vec3(1.0f);
bool motionBlurEnabled = false;
float motionBlurStrength = 0.15f; // 0..1 blend with previous frame
float motionBlurThreshold = 0.04f;
float motionBlurClamp = 0.35f;
bool vignetteEnabled = false;
float vignetteIntensity = 0.35f;
float vignetteSmoothness = 0.25f;
bool chromaticAberrationEnabled = false;
float chromaticAmount = 0.0025f;
bool sharpenEnabled = false;
float sharpenStrength = 0.15f;
bool ambientOcclusionEnabled = false;
float aoRadius = 0.0035f;
float aoStrength = 0.6f;
@@ -269,6 +289,7 @@ struct ScriptComponent {
std::string managedType;
std::vector<ScriptSetting> settings;
std::string lastBinaryPath;
bool lastBinaryVerified = false;
std::vector<void*> activeIEnums; // function pointers registered via IEnum_Start
};
@@ -295,6 +316,7 @@ struct ColliderComponent {
bool enabled = true;
ColliderType type = ColliderType::Box;
glm::vec3 boxSize = glm::vec3(1.0f);
glm::vec3 offset = glm::vec3(0.0f);
bool convex = true; // For mesh colliders: true = convex hull, false = triangle mesh (static only)
float staticFriction = 0.9f;
float dynamicFriction = 0.8f;
@@ -350,6 +372,7 @@ struct UIElementComponent {
int spriteSourceHeight = 0;
std::vector<glm::ivec4> spriteCustomFrames;
std::vector<std::string> spriteCustomFrameNames;
std::vector<glm::vec2> spriteCustomFrameScales;
};
struct Rigidbody2DComponent {
@@ -370,6 +393,7 @@ struct Collider2DComponent {
bool enabled = true;
Collider2DType type = Collider2DType::Box;
glm::vec2 boxSize = glm::vec2(1.0f);
glm::vec2 offset = glm::vec2(0.0f);
std::vector<glm::vec2> points;
bool closed = false;
float edgeThickness = 0.05f;

View File

@@ -84,6 +84,17 @@ void Shader::use()
glUseProgram(ID);
}
int Shader::getUniformLocation(const std::string& name) const
{
auto it = uniformLocationCache.find(name);
if (it != uniformLocationCache.end()) {
return it->second;
}
int location = glGetUniformLocation(ID, name.c_str());
uniformLocationCache.emplace(name, location);
return location;
}
void Shader::checkCompileErrors(unsigned int shader, std::string type)
{
int success;
@@ -111,41 +122,41 @@ void Shader::checkCompileErrors(unsigned int shader, std::string type)
void Shader::setBool(const std::string &name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
glUniform1i(getUniformLocation(name), (int)value);
}
void Shader::setInt(const std::string &name, int value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
glUniform1i(getUniformLocation(name), value);
}
void Shader::setFloat(const std::string &name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
glUniform1f(getUniformLocation(name), value);
}
void Shader::setVec2(const std::string &name, const glm::vec2 &value) const
{
glUniform2fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
glUniform2fv(getUniformLocation(name), 1, &value[0]);
}
void Shader::setVec3(const std::string &name, const glm::vec3 &value) const
{
glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
glUniform3fv(getUniformLocation(name), 1, &value[0]);
}
void Shader::setVec4(const std::string &name, const glm::vec4 &value) const
{
glUniform4fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
glUniform4fv(getUniformLocation(name), 1, &value[0]);
}
void Shader::setMat4(const std::string &name, const glm::mat4 &mat) const
{
glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, glm::value_ptr(mat));
glUniformMatrix4fv(getUniformLocation(name), 1, GL_FALSE, glm::value_ptr(mat));
}
void Shader::setMat4Array(const std::string &name, const glm::mat4 *data, int count) const
{
if (count <= 0 || !data) return;
glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), count, GL_FALSE, glm::value_ptr(data[0]));
glUniformMatrix4fv(getUniformLocation(name), count, GL_FALSE, glm::value_ptr(data[0]));
}

View File

@@ -83,11 +83,28 @@ struct Tokenizer {
break;
}
if (std::isdigit(static_cast<unsigned char>(c)) || (c == '-' && pos + 1 < input.size() && std::isdigit(static_cast<unsigned char>(input[pos + 1])))) {
if (std::isdigit(static_cast<unsigned char>(c)) ||
((c == '-' || c == '+') && pos + 1 < input.size() &&
(std::isdigit(static_cast<unsigned char>(input[pos + 1])) || input[pos + 1] == '.')) ||
(c == '.' && pos + 1 < input.size() && std::isdigit(static_cast<unsigned char>(input[pos + 1])))) {
token.type = TokenType::Number;
token.text.push_back(input[pos++]);
while (pos < input.size() && std::isdigit(static_cast<unsigned char>(input[pos]))) {
token.text.push_back(input[pos++]);
bool seenExponent = false;
while (pos < input.size()) {
const char next = input[pos];
if (std::isdigit(static_cast<unsigned char>(next)) || next == '.') {
token.text.push_back(input[pos++]);
continue;
}
if (!seenExponent && (next == 'e' || next == 'E')) {
seenExponent = true;
token.text.push_back(input[pos++]);
if (pos < input.size() && (input[pos] == '+' || input[pos] == '-')) {
token.text.push_back(input[pos++]);
}
continue;
}
break;
}
return token;
}
@@ -161,6 +178,12 @@ struct Parser {
return true;
}
bool parseFloat(float& out) {
if (peek().type != TokenType::Number) return false;
out = std::stof(advance().text);
return true;
}
bool parseAssignmentValue(const Token& key) {
if (!match(TokenType::Equals)) {
error(key.line, "expected '=' after identifier '" + key.text + "'");
@@ -300,6 +323,51 @@ struct Parser {
result.document.names = parsedNames;
}
void parseScalesBlock() {
std::vector<glm::vec2> parsedScales;
if (!match(TokenType::LBrace)) {
error(peek().line, "expected '{' after scales");
return;
}
while (peek().type != TokenType::End && peek().type != TokenType::RBrace) {
glm::vec2 scale(1.0f);
const int entryLine = peek().line;
bool ok = true;
if (!parseFloat(scale.x)) {
error(peek().line, "unexpected token '" + peek().text + "' inside scales block");
ok = false;
} else if (!match(TokenType::Comma)) {
error(peek().line, "expected ',' inside scales block");
ok = false;
} else if (!parseFloat(scale.y)) {
error(peek().line, "unexpected token '" + peek().text + "' inside scales block");
ok = false;
}
if (ok) {
if (!match(TokenType::Semicolon)) {
error(peek().line, "expected ';' after scale entry");
}
scale.x = std::max(0.01f, scale.x);
scale.y = std::max(0.01f, scale.y);
parsedScales.push_back(scale);
} else {
while (peek().type != TokenType::End && peek().type != TokenType::Semicolon && peek().type != TokenType::RBrace) {
advance();
}
match(TokenType::Semicolon);
parsedScales.clear();
while (peek().type != TokenType::End && peek().line == entryLine && peek().type != TokenType::RBrace) {
advance();
}
}
}
match(TokenType::RBrace);
result.document.scales = parsedScales;
}
void parseLayersBlock() {
if (!match(TokenType::LBrace)) {
error(peek().line, "expected '{' after info Layers");
@@ -350,6 +418,7 @@ struct Parser {
void parseAtlasInfoBlock() {
std::vector<glm::ivec4> defaultRects = result.document.rects;
std::vector<std::string> defaultNames = result.document.names;
std::vector<glm::vec2> defaultScales = result.document.scales;
if (!match(TokenType::LBrace)) {
error(peek().line, "expected '{' after info AtlasInfo");
return;
@@ -363,6 +432,7 @@ struct Parser {
const std::string blockName = advance().text;
if (blockName == "rects") parseRectsBlock();
else if (blockName == "names") parseNamesBlock();
else if (blockName == "scales") parseScalesBlock();
else {
error(peek().line, "unexpected block '" + blockName + "' inside info AtlasInfo");
if (peek().type == TokenType::LBrace) skipUnknownBlock();
@@ -371,6 +441,7 @@ struct Parser {
match(TokenType::RBrace);
if (result.document.rects.empty() && !defaultRects.empty()) result.document.rects = defaultRects;
if (result.document.names.empty() && !defaultNames.empty()) result.document.names = defaultNames;
if (result.document.scales.empty() && !defaultScales.empty()) result.document.scales = defaultScales;
}
void parseInfoBlock() {
@@ -401,11 +472,20 @@ struct Parser {
} else if (result.document.names.size() > result.document.rects.size()) {
result.document.names.resize(result.document.rects.size());
}
if (result.document.scales.size() < result.document.rects.size()) {
result.document.scales.resize(result.document.rects.size(), glm::vec2(1.0f));
} else if (result.document.scales.size() > result.document.rects.size()) {
result.document.scales.resize(result.document.rects.size());
}
for (size_t i = 0; i < result.document.names.size(); ++i) {
if (result.document.names[i].empty()) {
result.document.names[i] = "Rect_" + std::to_string(i);
}
}
for (glm::vec2& scale : result.document.scales) {
scale.x = std::max(0.01f, scale.x);
scale.y = std::max(0.01f, scale.y);
}
for (size_t i = 0; i < result.document.layers.size(); ++i) {
if (result.document.layers[i].name.empty()) {
result.document.layers[i].name = "Layer_" + std::to_string(i);
@@ -494,9 +574,16 @@ std::string WriteSpritesheet(const SpritesheetDocument& inputDocument) {
if (document.expectRects <= 0) document.expectRects = static_cast<int>(document.rects.size());
if (document.lastSavedUtc.empty()) document.lastSavedUtc = CurrentUtcIso8601();
if (document.names.size() < document.rects.size()) document.names.resize(document.rects.size());
if (document.names.size() > document.rects.size()) document.names.resize(document.rects.size());
if (document.scales.size() < document.rects.size()) document.scales.resize(document.rects.size(), glm::vec2(1.0f));
if (document.scales.size() > document.rects.size()) document.scales.resize(document.rects.size());
for (size_t i = 0; i < document.names.size(); ++i) {
if (document.names[i].empty()) document.names[i] = "Rect_" + std::to_string(i);
}
for (glm::vec2& scale : document.scales) {
scale.x = std::max(0.01f, scale.x);
scale.y = std::max(0.01f, scale.y);
}
for (size_t i = 0; i < document.layers.size(); ++i) {
if (document.layers[i].name.empty()) document.layers[i].name = "Layer_" + std::to_string(i);
}
@@ -527,6 +614,14 @@ std::string WriteSpritesheet(const SpritesheetDocument& inputDocument) {
out << " " << name << ";\n";
}
out << " }\n";
out << "\n";
out << " scales\n";
out << " {\n";
out << " // Per-clip display multipliers. Leave these at 1,1 to use the clip rect size ratio automatically.\n";
for (const glm::vec2& scale : document.scales) {
out << " " << scale.x << "," << scale.y << ";\n";
}
out << " }\n";
out << "}\n\n";
out << "info Layers\n";
out << "{\n";
@@ -554,12 +649,14 @@ void RunSpritesheetParserSelfTests() {
"Expect_Layers = 1;\n"
"Expect_rects = 2;\n"
"Confirmation.StrictValidation = false;\n"
"info AtlasInfo { rects { 1,2,3,4; 5,6,7,8; } names { A; ; } }\n"
"info AtlasInfo { rects { 1,2,3,4; 5,6,7,8; } names { A; ; } scales { 1,1; 1.5,0.5; } }\n"
"info Layers { }\n";
const auto validResult = ParseSpritesheet(valid);
assert(validResult.document.rects.size() == 2);
assert(validResult.document.names.size() == 2);
assert(validResult.document.names[1] == "Rect_1");
assert(validResult.document.scales.size() == 2);
assert(std::abs(validResult.document.scales[1].x - 1.5f) < 0.001f);
const std::string whitespace =
"LinkedSpriteName= \"A\" ;\n"
@@ -568,16 +665,17 @@ void RunSpritesheetParserSelfTests() {
"Expect_Layers= 1 ;\n"
"Expect_rects =2;\n"
"Confirmation.StrictValidation = false\n"
"info AtlasInfo{rects{1,1,1,1;2,2,2,2;}names{X;Y;}}\n"
"info AtlasInfo{rects{1,1,1,1;2,2,2,2;}names{X;Y;}scales{1,1;0.75,1.25;}}\n"
"info Layers{}\n";
const auto whitespaceResult = ParseSpritesheet(whitespace);
assert(whitespaceResult.document.linkedSpriteName == "A");
assert(whitespaceResult.document.rects.size() == 2);
assert(std::abs(whitespaceResult.document.scales[1].y - 1.25f) < 0.001f);
const std::string invalid =
"LinkedSpriteName \"bad\";\n"
"SpriteVersion == 2;\n"
"info AtlasInfo { rects { 1,2,3; ; } names { A; } }\n";
"info AtlasInfo { rects { 1,2,3; ; } names { A; } scales { nope; } }\n";
const auto invalidResult = ParseSpritesheet(invalid);
assert(invalidResult.document.linkedSpriteName.empty());
assert(invalidResult.document.spriteVersion == 1);

View File

@@ -16,6 +16,7 @@ struct SpritesheetDocument {
bool strictValidation = false;
std::vector<glm::ivec4> rects;
std::vector<std::string> names;
std::vector<glm::vec2> scales;
std::vector<SpritesheetLayer> layers;
};

View File

@@ -40,6 +40,12 @@ Texture::Texture(const std::string& path,
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, m_InternalFormat, m_Width, m_Height, 0, m_DataFormat, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
int bytesPerPixel = (m_Channels == 1) ? 1 : ((m_Channels == 3) ? 3 : 4);
m_ApproxMemoryBytes = static_cast<size_t>(m_Width) *
static_cast<size_t>(m_Height) *
static_cast<size_t>(bytesPerPixel);
// Mip chains for 2D textures are roughly 33% extra memory.
m_ApproxMemoryBytes += m_ApproxMemoryBytes / 3;
// params
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapS);
@@ -69,4 +75,4 @@ void Texture::Bind(GLenum unit) const
void Texture::Unbind() const
{
glBindTexture(GL_TEXTURE_2D, 0);
}
}

View File

@@ -131,8 +131,8 @@ std::string TextEditor::GetWordAtPublic(const Coordinates& aCoords) const
TextEditor::Coordinates TextEditor::SanitizeCoordinates(const Coordinates & aValue) const
{
auto line = aValue.mLine;
auto column = aValue.mColumn;
auto line = std::max(0, aValue.mLine);
auto column = std::max(0, aValue.mColumn);
if (line >= (int)mLines.size())
{
if (mLines.empty())
@@ -505,7 +505,7 @@ TextEditor::Coordinates TextEditor::FindNextWord(const Coordinates & aFrom) cons
int TextEditor::GetCharacterIndex(const Coordinates& aCoordinates) const
{
if (aCoordinates.mLine >= mLines.size())
if (aCoordinates.mLine < 0 || aCoordinates.mLine >= mLines.size())
return -1;
auto& line = mLines[aCoordinates.mLine];
int c = 0;
@@ -523,7 +523,7 @@ int TextEditor::GetCharacterIndex(const Coordinates& aCoordinates) const
int TextEditor::GetCharacterColumn(int aLine, int aIndex) const
{
if (aLine >= mLines.size())
if (aLine < 0 || aLine >= mLines.size())
return 0;
auto& line = mLines[aLine];
int col = 0;
@@ -542,7 +542,7 @@ int TextEditor::GetCharacterColumn(int aLine, int aIndex) const
int TextEditor::GetLineCharacterCount(int aLine) const
{
if (aLine >= mLines.size())
if (aLine < 0 || aLine >= mLines.size())
return 0;
auto& line = mLines[aLine];
int c = 0;
@@ -553,7 +553,7 @@ int TextEditor::GetLineCharacterCount(int aLine) const
int TextEditor::GetLineMaxColumn(int aLine) const
{
if (aLine >= mLines.size())
if (aLine < 0 || aLine >= mLines.size())
return 0;
auto& line = mLines[aLine];
int col = 0;
@@ -934,10 +934,10 @@ void TextEditor::Render()
ImVec2 cursorScreenPos = ImGui::GetCursorScreenPos();
auto scrollX = ImGui::GetScrollX();
auto scrollY = ImGui::GetScrollY();
auto scrollY = std::max(0.0f, ImGui::GetScrollY());
mCursorScreenPosValid = false;
auto lineNo = (int)floor(scrollY / mCharAdvance.y);
auto lineNo = std::max(0, (int)floor(scrollY / mCharAdvance.y));
auto globalLineMax = (int)mLines.size();
auto lineMax = std::max(0, std::min((int)mLines.size() - 1, lineNo + (int)floor((scrollY + contentSize.y) / mCharAdvance.y)));
@@ -1191,7 +1191,11 @@ void TextEditor::Render(const char* aTitle, const ImVec2& aSize, bool aBorder)
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(mPalette[(int)PaletteIndex::Background]));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
if (!mIgnoreImGuiChild)
ImGui::BeginChild(aTitle, aSize, aBorder, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoMove);
ImGui::BeginChild(aTitle, aSize, aBorder,
ImGuiWindowFlags_HorizontalScrollbar |
ImGuiWindowFlags_AlwaysHorizontalScrollbar |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoNavInputs);
if (mHandleKeyboardInputs)
{