diff --git a/.gitmodules b/.gitmodules index 1d9d30cc..21169963 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,9 @@ [submodule "extern/quickjs"] path = extern/quickjs url = https://github.com/quickjs-ng/quickjs.git -[submodule "extern/imgui"] - path = extern/imgui - url = https://github.com/ocornut/imgui.git [submodule "extern/QtDockingSystem"] path = extern/QtDockingSystem url = https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System.git +[submodule "extern/imgui"] + path = extern/imgui + url = https://github.com/ocornut/imgui.git diff --git a/README.md b/README.md index 5c355645..d0d8c1ba 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ It is built with C++ and uses OpenGL, Vulkan and Metal for rendering. It also ha an environment system and a debugging system with more to come. ![Atlas Screenshot](example.png) +![Editor Screenshot](editorExample.png) ## Features diff --git a/atlas/application/window.cpp b/atlas/application/window.cpp index fdac8fcd..f029126d 100644 --- a/atlas/application/window.cpp +++ b/atlas/application/window.cpp @@ -1917,9 +1917,6 @@ bool Window::stepFrame() { } if (!editorControlsRenderedInScenePass) { - if (this->usePathTracing) { - renderEditorGrid(commandBuffer); - } renderEditorOverlays(commandBuffer); } @@ -3862,6 +3859,13 @@ void Window::addPreferencedObject(Renderable *obj) { } } +void Window::removePreferencedObject(Renderable *obj) { + this->preferenceRenderables.erase( + std::remove(this->preferenceRenderables.begin(), + this->preferenceRenderables.end(), obj), + this->preferenceRenderables.end()); +} + void Window::addPreludeObject(Renderable *obj) { if (obj == nullptr) { return; @@ -5688,6 +5692,12 @@ void Window::configurePathTracing(int samplesPerPixel, int bounceLimit, accumulationFrames); } +void Window::resetPathTracingAccumulation() { + if (pathTracer != nullptr) { + pathTracer->resetAccumulation(); + } +} + bool Window::setEditorPathTracingPreview(bool enabled) { if (pathTracer == nullptr) { return false; diff --git a/atlas/graphics/deferred.cpp b/atlas/graphics/deferred.cpp index d70197cc..55a83cd2 100644 --- a/atlas/graphics/deferred.cpp +++ b/atlas/graphics/deferred.cpp @@ -1012,6 +1012,9 @@ void Window::deferredRendering( Window::mainWindow->currentScene->environment.rimLight.color.r, Window::mainWindow->currentScene->environment.rimLight.color.g, Window::mainWindow->currentScene->environment.rimLight.color.b); + lightPipeline->setUniform1f( + "environment.bloomThreshold", + Window::mainWindow->currentScene->environment.lightBloom.threshold); commandBuffer->bindDrawingState(quadState); commandBuffer->bindPipeline(lightPipeline); diff --git a/cli/src/main.rs b/cli/src/main.rs index 132e4d80..6a6323be 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2,7 +2,12 @@ use atlas_cli::*; use clap::Parser; #[derive(Parser)] -#[command(name = "atlas")] +#[command( + name = "atlas", + version = "Release Candidate for Beta 1", + about = "Atlas (Release Candidate for Beta 1)", + arg_required_else_help = true +)] pub struct Cli { #[command(subcommand)] command: Commands, diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 64dbf688..122de6ee 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -137,6 +137,7 @@ qt_add_resources(AtlasEditor "atlas_editor_assets" FILES "${CMAKE_SOURCE_DIR}/editor/assets/Icon-iOS-Default-1024x1024@1x.png" "${CMAKE_SOURCE_DIR}/editor/assets/iconFile-iOS-Dark-1024x1024@1x.png" + "${CMAKE_SOURCE_DIR}/editor/assets/atlas_ball_bright.png" "${CMAKE_SOURCE_DIR}/editor/assets/Manrope-VariableFont_wght.ttf" "${CMAKE_SOURCE_DIR}/editor/assets/Phosphor.ttf" "${CMAKE_SOURCE_DIR}/editor/assets/PHOSPHOR_LICENSE.txt" diff --git a/editor/views/editor/editor.cpp b/editor/views/editor/editor.cpp index 6c2d421a..c4b33bd4 100644 --- a/editor/views/editor/editor.cpp +++ b/editor/views/editor/editor.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -646,6 +647,12 @@ void EditorWindow::setupDocks() { materialEditorPanel = new MaterialEditorPanel(viewportPanel); postProcessingPanel = new PostProcessingPanel(viewportPanel); graphiteEditorPanel = new GraphiteEditorPanel(viewportPanel, projectFile); + connect(materialEditorPanel, &MaterialEditorPanel::materialSaved, this, + [this](const QString &) { workspaceChangesPending = true; }); + connect(postProcessingPanel, &PostProcessingPanel::settingsChanged, this, + [this] { workspaceChangesPending = true; }); + connect(graphiteEditorPanel, &GraphiteEditorPanel::documentSaved, this, + [this] { workspaceChangesPending = true; }); workspaceStack = new QStackedWidget(this); workspaceStack->setObjectName("editorWorkspaceStack"); workspaceStack->addWidget(viewportTools); @@ -689,11 +696,44 @@ void EditorWindow::setupDocks() { .area = EditorDockArea::Bottom, .icon = styling::icon(styling::Icon::FolderOpen, "#7E929C")}); + runtimeErrors = new QPlainTextEdit(this); + runtimeErrors->setObjectName("runtimeErrors"); + runtimeErrors->setReadOnly(true); + runtimeErrors->setLineWrapMode(QPlainTextEdit::NoWrap); + runtimeErrors->setPlaceholderText( + "Runtime and script errors will appear here."); + auto *errorDock = dockManager->addPanel( + {.id = "errors", + .title = "Errors", + .widget = runtimeErrors, + .area = EditorDockArea::Bottom, + .icon = styling::icon(styling::Icon::Warning, "#A17F7F")}); + if (contentDock->dockAreaWidget() != nullptr) { + coreManager->addDockWidget(ads::CenterDockWidgetArea, errorDock, + contentDock->dockAreaWidget()); + } + errorDock->toggleView(false); + connect(viewportPanel, &ViewportPanel::runtimeErrorOccurred, this, + [this, errorDock](const QString &message) { + const QString normalized = message.trimmed(); + if (normalized.isEmpty()) + return; + runtimeErrors->appendPlainText( + QStringLiteral("[%1] %2") + .arg(QDateTime::currentDateTime().toString("HH:mm:ss"), + normalized)); + errorDock->toggleView(true); + errorDock->setAsCurrentTab(); + errorDock->raise(); + statusBar()->showMessage("Runtime error reported", 5000); + }); + workspaceDock->setAsCurrentTab(); defaultDockState = coreManager->saveState(DockStateVersion); const QList managedDocks{workspaceDock, hierarchyDock, - inspectorDock, contentDock}; + inspectorDock, contentDock, + errorDock}; for (ads::CDockWidget *dock : managedDocks) { connect(dock, &ads::CDockWidget::topLevelChanged, this, [this](bool) { scheduleLayoutSave(); }); @@ -718,7 +758,8 @@ void EditorWindow::setupDocks() { {"Workspace", workspaceDock}, {"Hierarchy", hierarchyDock}, {"Inspector", inspectorDock}, - {"Content Browser", contentDock}}; + {"Content Browser", contentDock}, + {"Errors", errorDock}}; for (int index = 0; index < panels.size(); ++index) { const auto &[name, dock] = panels.at(index); auto *action = windowMenu->addAction( @@ -829,7 +870,9 @@ void EditorWindow::setupWorkspaceBar() { identityLayout->setSpacing(9); auto *mark = new QLabel(identity); mark->setObjectName("workspaceMark"); - mark->setPixmap(windowIcon().pixmap(24, 24)); + mark->setPixmap(QPixmap(":/editor/assets/atlas_ball_bright.png") + .scaled(24, 24, Qt::KeepAspectRatio, + Qt::SmoothTransformation)); auto *identityText = new QWidget(identity); identityText->setObjectName("workspaceIdentityText"); auto *identityTextLayout = new QVBoxLayout(identityText); @@ -1034,7 +1077,17 @@ void EditorWindow::activateWorkspace(int index) { if (workspaceStack == nullptr || index < 0 || index >= workspaceStack->count()) return; + const int previousIndex = workspaceStack->currentIndex(); + if (index == 0 && previousIndex == 1 && materialEditorPanel != nullptr) + materialEditorPanel->flushPendingSave(); + if (index == 0 && previousIndex == 3 && graphiteEditorPanel != nullptr) + graphiteEditorPanel->flushPendingSave(); workspaceStack->setCurrentIndex(index); + if (index == 0 && previousIndex != 0 && workspaceChangesPending && + viewportPanel != nullptr) { + workspaceChangesPending = false; + viewportPanel->reloadRuntime(); + } if (workspaceModeGroup != nullptr) { if (auto *button = workspaceModeGroup->button(index)) { button->setChecked(true); diff --git a/editor/views/editor/graphiteEditor.cpp b/editor/views/editor/graphiteEditor.cpp index 998e8f89..dc5c50e0 100644 --- a/editor/views/editor/graphiteEditor.cpp +++ b/editor/views/editor/graphiteEditor.cpp @@ -573,6 +573,7 @@ GraphiteEditorPanel::~GraphiteEditorPanel() { } void GraphiteEditorPanel::showEmptyState() { + documentDirty = false; document = QJsonObject{{"format", "atlas.graphite.ui"}, {"version", 1}, {"canvas", QJsonObject{{"width", 1280}, @@ -635,6 +636,7 @@ void GraphiteEditorPanel::openUI(const QString &path) { } } document = next; + documentDirty = repaired; undoStack->clear(); titleLabel->setText(QFileInfo(uiPath).completeBaseName()); statusLabel->setText("Ready"); @@ -655,7 +657,14 @@ void GraphiteEditorPanel::saveUI() { QMessageBox::warning(this, "Graphite", "The UI asset could not be saved."); return; } + documentDirty = false; statusLabel->setText("Saved"); + emit documentSaved(); +} + +void GraphiteEditorPanel::flushPendingSave() { + if (documentDirty) + saveUI(); } void GraphiteEditorPanel::undo() { undoStack->undo(); } @@ -720,6 +729,7 @@ void GraphiteEditorPanel::setDocument(const QJsonObject &next, bool recordUndo) canvas->setDocument(document, uiPath); canvas->setSelectedPath(path); rebuildInspector(); + documentDirty = true; statusLabel->setText("Modified"); }; if (recordUndo) @@ -1387,5 +1397,4 @@ void GraphiteEditorPanel::attachToScene(bool preview) { }); emit previewRequested(); } - viewport->reloadRuntime(); } diff --git a/editor/views/editor/inspector.cpp b/editor/views/editor/inspector.cpp index bfede19d..f64e4be5 100644 --- a/editor/views/editor/inspector.cpp +++ b/editor/views/editor/inspector.cpp @@ -255,7 +255,8 @@ QJsonObject environmentSchema() { {"weight", 0.02}, {"decay", 0.95}, {"exposure", 0.7}}}, - {"lightBloom", QJsonObject{{"radius", 0.01}, {"maxSamples", 6}}}, + {"lightBloom", + QJsonObject{{"threshold", 0.8}, {"radius", 0.01}, {"maxSamples", 6}}}, {"rimLight", QJsonObject{{"intensity", 0.0}, {"color", QJsonArray{1.0, 0.96, 0.86}}}}, {"atmosphere", @@ -1621,13 +1622,10 @@ void InspectorPanel::showObject(const QJsonObject &object) { auto update = [runtime, objectId](const QString &component, int index, const QString &path, const QJsonValue &value) { - QTimer::singleShot(0, - [runtime, objectId, component, index, path, value] { - if (runtime != nullptr) { - runtime->setRuntimeObjectProperty( - objectId, component, index, path, value); - } - }); + if (runtime != nullptr) { + runtime->setRuntimeObjectProperty(objectId, component, index, path, + value); + } }; QJsonObject transform{{"position", object.value("position")}, {"rotation", object.value("rotation")}, diff --git a/editor/views/editor/materialEditor.cpp b/editor/views/editor/materialEditor.cpp index 328289f8..5ad0d9f0 100644 --- a/editor/views/editor/materialEditor.cpp +++ b/editor/views/editor/materialEditor.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,9 @@ #include namespace { +constexpr int MaterialPreviewPathTracingFrames = 24; +constexpr int MaterialPreviewPathTracingIntervalMs = 50; + QColor jsonColor(const QJsonValue &value, const QColor &fallback) { const QJsonArray array = value.toArray(); if (array.size() < 3) { @@ -174,10 +178,51 @@ class MaterialPreviewWidget : public QWidget { scheduleFrame(); } + void mousePressEvent(QMouseEvent *event) override { + if (event->button() == Qt::LeftButton) { + rotating = true; + lastPointer = event->position(); + setCursor(Qt::ClosedHandCursor); + event->accept(); + return; + } + QWidget::mousePressEvent(event); + } + + void mouseMoveEvent(QMouseEvent *event) override { + if (rotating && runtimeContext != nullptr) { + const QPointF delta = event->position() - lastPointer; + lastPointer = event->position(); + if (runtimeContext->rotateMaterialPreview( + static_cast(delta.x() * 0.55), + static_cast(delta.y() * 0.55))) { + scheduleFrame(); + } + event->accept(); + return; + } + QWidget::mouseMoveEvent(event); + } + + void mouseReleaseEvent(QMouseEvent *event) override { + if (event->button() == Qt::LeftButton && rotating) { + rotating = false; + unsetCursor(); + event->accept(); + return; + } + QWidget::mouseReleaseEvent(event); + } + private: void scheduleFrame() { - pendingFrames = std::max(pendingFrames, 2); - if (isVisible() && frameTimer != nullptr) { + const int requestedFrames = + runtimeContext != nullptr && + runtimeContext->materialPreviewUsesPathTracing() + ? MaterialPreviewPathTracingFrames + : 2; + pendingFrames = std::max(pendingFrames, requestedFrames); + if (isVisible() && frameTimer != nullptr && !frameTimer->isActive()) { frameTimer->start(0); } } @@ -200,6 +245,10 @@ class MaterialPreviewWidget : public QWidget { return; } resizeRuntime(); + if (runtimeContext->materialPreviewUsesPathTracing()) { + pendingFrames = + std::max(pendingFrames, MaterialPreviewPathTracingFrames); + } } catch (const std::exception &error) { qWarning().noquote() << QStringLiteral("Failed to start runtime material preview: %1") @@ -241,8 +290,12 @@ class MaterialPreviewWidget : public QWidget { return; } pendingFrames = std::max(0, pendingFrames - 1); - if (pendingFrames > 0 && isVisible()) - frameTimer->start(1); + if (pendingFrames > 0 && isVisible()) { + frameTimer->start( + runtimeContext->materialPreviewUsesPathTracing() + ? MaterialPreviewPathTracingIntervalMs + : 1); + } } catch (const std::exception &error) { qWarning().noquote() << QStringLiteral("Runtime material preview frame failed: %1") @@ -277,6 +330,8 @@ class MaterialPreviewWidget : public QWidget { int runtimeHeight = 0; int environmentMode = 0; int pendingFrames = 0; + QPointF lastPointer; + bool rotating = false; }; MaterialEditorPanel::MaterialEditorPanel(ViewportPanel *viewport, @@ -337,6 +392,11 @@ MaterialEditorPanel::~MaterialEditorPanel() { } } +void MaterialEditorPanel::flushPendingSave() { + if (saveTimer->isActive()) + saveMaterial(); +} + QJsonObject MaterialEditorPanel::normalizedMaterial(const QJsonObject &source) const { QJsonObject result = source; @@ -445,7 +505,7 @@ void MaterialEditorPanel::showMaterial() { previewOptionsLayout->setContentsMargins(0, 0, 0, 0); auto *previewLabel = new QLabel("Preview Environment", previewOptions); auto *environment = new QComboBox(previewOptions); - environment->addItems({"Studio", "Sunset", "Open Sky"}); + environment->addItems({"Studio", "Sky", "Empty"}); previewOptionsLayout->addWidget(previewLabel); previewOptionsLayout->addStretch(); previewOptionsLayout->addWidget(environment); diff --git a/editor/views/editor/postProcessing.cpp b/editor/views/editor/postProcessing.cpp index 1e86d178..24e9199b 100644 --- a/editor/views/editor/postProcessing.cpp +++ b/editor/views/editor/postProcessing.cpp @@ -112,9 +112,6 @@ PostProcessingPanel::PostProcessingPanel(ViewportPanel *viewport, removeTargetButton->setText("Remove"); removeTargetButton->setIcon( styling::icon(styling::Icon::Trash, "#A17F7F")); - auto *applyButton = new QPushButton("Apply to Preview", toolbar); - applyButton->setIcon( - styling::icon(styling::Icon::Sparkle, "#849589")); statusLabel = new QLabel(toolbar); statusLabel->setObjectName("postProcessingStatus"); toolbarLayout->addWidget(title); @@ -123,7 +120,6 @@ PostProcessingPanel::PostProcessingPanel(ViewportPanel *viewport, toolbarLayout->addWidget(removeTargetButton); toolbarLayout->addStretch(); toolbarLayout->addWidget(statusLabel); - toolbarLayout->addWidget(applyButton); layout->addWidget(toolbar); auto *scroll = new QScrollArea(this); @@ -147,12 +143,6 @@ PostProcessingPanel::PostProcessingPanel(ViewportPanel *viewport, &PostProcessingPanel::addTarget); connect(removeTargetButton, &QToolButton::clicked, this, &PostProcessingPanel::removeTarget); - connect(applyButton, &QPushButton::clicked, this, [this] { - if (this->viewport != nullptr) { - statusLabel->setText("Reloading…"); - this->viewport->reloadRuntime(); - } - }); if (viewport != nullptr) { connect(viewport, &ViewportPanel::sceneSnapshotChanged, this, &PostProcessingPanel::applySceneSnapshot); @@ -169,10 +159,13 @@ void PostProcessingPanel::applySceneSnapshot(const QString &snapshot) { const QJsonDocument document = QJsonDocument::fromJson(snapshot.toUtf8()); if (!document.isObject()) return; - const QJsonArray nextTargets = document.object().value("targets").toArray(); - if (nextTargets == targets) + const QJsonObject root = document.object(); + const QJsonArray nextTargets = root.value("targets").toArray(); + const QJsonObject nextEnvironment = root.value("environment").toObject(); + if (nextTargets == targets && nextEnvironment == environment) return; targets = nextTargets; + environment = nextEnvironment; if (!applying) { rebuildTargetList(); } @@ -204,6 +197,19 @@ void PostProcessingPanel::rebuildEditor() { item->widget()->deleteLater(); delete item; } + + const QJsonObject lightBloom = environment.value("lightBloom").toObject(); + auto *bloom = new QGroupBox("Bloom", body); + auto *bloomForm = new QFormLayout(bloom); + auto *threshold = + effectNumber(lightBloom.value("threshold").toDouble(0.8), bloom); + threshold->setRange(0.0, 10000.0); + threshold->setSingleStep(0.05); + bloomForm->addRow("Threshold", threshold); + bodyLayout->addWidget(bloom); + connect(threshold, &QDoubleSpinBox::valueChanged, this, + [this](double value) { setBloomThreshold(value); }); + if (targetIndex < 0 || targetIndex >= targets.size()) { auto *empty = new QLabel( "Create a render target to build a post-processing stack.", body); @@ -434,9 +440,13 @@ void PostProcessingPanel::setTargetValue(const QString &path, target.insert(path.mid(1), value); targets.replace(targetIndex, target); applying = true; - viewport->setRuntimeSceneProperty("targets", targetIndex, path, value); + if (!viewport->setRuntimeSceneProperty("targets", targetIndex, path, value)) { + applying = false; + return; + } applying = false; - statusLabel->setText("Saved · apply to update preview"); + statusLabel->setText("Saved · return to Scene to update preview"); + emit settingsChanged(); if (path == "/name") rebuildTargetList(); } @@ -457,11 +467,32 @@ void PostProcessingPanel::setEffectValue(int effectIndex, const QString &key, setTargetValue("/effects", effects); } +void PostProcessingPanel::setBloomThreshold(double value) { + if (viewport == nullptr) + return; + QJsonObject lightBloom = environment.value("lightBloom").toObject(); + lightBloom.insert("threshold", value); + environment.insert("lightBloom", lightBloom); + applying = true; + if (!viewport->setRuntimeSceneProperty("environment", -1, + "/lightBloom/threshold", value)) { + applying = false; + return; + } + applying = false; + statusLabel->setText("Saved · return to Scene to update preview"); + emit settingsChanged(); +} + void PostProcessingPanel::replaceTargets() { if (viewport == nullptr) return; applying = true; - viewport->setRuntimeSceneProperty("targets", -1, QString(), targets); + if (!viewport->setRuntimeSceneProperty("targets", -1, QString(), targets)) { + applying = false; + return; + } applying = false; - statusLabel->setText("Saved · apply to update preview"); + statusLabel->setText("Saved · return to Scene to update preview"); + emit settingsChanged(); } diff --git a/editor/views/editor/viewport.cpp b/editor/views/editor/viewport.cpp index 94a90316..b4113ad1 100644 --- a/editor/views/editor/viewport.cpp +++ b/editor/views/editor/viewport.cpp @@ -60,6 +60,9 @@ constexpr int RuntimeEditorCameraKeyLeft = 2; constexpr int RuntimeEditorCameraKeyRight = 3; constexpr int RuntimeEditorCameraKeyUp = 4; constexpr int RuntimeEditorCameraKeyDown = 5; +constexpr int RuntimeFrameIntervalMs = 16; +constexpr int RuntimeSnapshotIntervalMs = 50; +constexpr int RuntimeFrameRateIntervalMs = 250; int runtimeMouseButton(Qt::MouseButton button) { switch (button) { @@ -265,21 +268,16 @@ ViewportPanel::ViewportPanel(const QString &projectFile, QWidget *parent) frameTimer = new QTimer(this); resizeTimer = new QTimer(this); - environmentReloadTimer = new QTimer(this); undoStack = new QUndoStack(this); frameTimer->setTimerType(Qt::PreciseTimer); frameTimer->setSingleShot(true); resizeTimer->setSingleShot(true); resizeTimer->setInterval(0); - environmentReloadTimer->setSingleShot(true); - environmentReloadTimer->setInterval(140); connect(frameTimer, &QTimer::timeout, this, [this] { if (stepRuntime() && isVisible()) - frameTimer->start(pbrPreview ? 16 : 1); + frameTimer->start(RuntimeFrameIntervalMs); }); connect(resizeTimer, &QTimer::timeout, this, [this] { resizeRuntime(); }); - connect(environmentReloadTimer, &QTimer::timeout, this, - &ViewportPanel::reloadRuntime); if (auto *app = QCoreApplication::instance()) { connect(app, &QCoreApplication::aboutToQuit, this, [this] { shutdownRuntime(); }); @@ -303,7 +301,7 @@ void ViewportPanel::setRuntimeStartupEnabled(bool enabled) { void ViewportPanel::showEvent(QShowEvent *event) { QWidget::showEvent(event); if (runtimeContext != nullptr) { - frameTimer->start(pbrPreview ? 16 : 1); + frameTimer->start(RuntimeFrameIntervalMs); return; } if (runtimeStartupEnabled) @@ -396,8 +394,6 @@ void ViewportPanel::shutdownRuntime() { runtimeStartQueued = false; if (resizeTimer != nullptr) resizeTimer->stop(); - if (environmentReloadTimer != nullptr) - environmentReloadTimer->stop(); stopRuntime(); } @@ -601,6 +597,7 @@ void ViewportPanel::startRuntime() { const std::string runtimeProjectFile = projectFile.toUtf8().toStdString(); if (runtimeProjectFile.empty()) { qWarning() << "Atlas viewport runtime project file is not configured"; + emit runtimeErrorOccurred("Runtime project file is not configured"); emit runtimeStartupFinished(false, "Runtime project file is not configured"); return; @@ -609,6 +606,7 @@ void ViewportPanel::startRuntime() { void *metalView = reinterpret_cast(static_cast(winId())); if (metalView == nullptr) { qWarning() << "Atlas viewport could not resolve a native Metal view"; + emit runtimeErrorOccurred("Viewport native surface is unavailable"); emit runtimeStartupFinished(false, "Viewport native surface is unavailable"); return; @@ -620,6 +618,14 @@ void ViewportPanel::startRuntime() { QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); runtimeContext = runtime::makeContextForMetalView(runtimeProjectFile, metalView); + QPointer runtimeOwner(this); + runtimeContext->errorReporter = + [runtimeOwner](const std::string &error) { + if (runtimeOwner != nullptr) { + emit runtimeOwner->runtimeErrorOccurred( + QString::fromUtf8(error.c_str())); + } + }; runtimeContext->modelImportProgress = [this]( float value, const std::string &status) { @@ -637,6 +643,19 @@ void ViewportPanel::startRuntime() { runtimeContext->setEditorShadingMode(shadingMode); runtimeContext->setEditorPathTracingPreview(pbrPreview); resizeRuntime(); + emit runtimeAvailabilityChanged(true); + emit cameraFocusChanged(false); + playbackState = 0; + emit playbackStateChanged(playbackState); + emit runtimeLoadingStatusChanged("Preparing viewport..."); + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + if (!stepRuntime()) { + emit runtimeErrorOccurred("The first viewport frame failed"); + emit runtimeLoadingFinished(); + emit runtimeStartupFinished(false, + "The first viewport frame failed"); + return; + } refreshSceneSnapshot(); if (!selectionToRestore.isEmpty()) { const QJsonDocument document = @@ -652,20 +671,8 @@ void ViewportPanel::startRuntime() { emit runtimeObjectActivated(restoredId); } } - emit runtimeAvailabilityChanged(true); - emit cameraFocusChanged(false); - playbackState = 0; - emit playbackStateChanged(playbackState); emit sceneOpened(currentRuntimeScene()); - emit runtimeLoadingStatusChanged("Preparing viewport..."); - QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); - if (!stepRuntime()) { - emit runtimeLoadingFinished(); - emit runtimeStartupFinished(false, - "The first viewport frame failed"); - return; - } - frameTimer->start(pbrPreview ? 16 : 1); + frameTimer->start(RuntimeFrameIntervalMs); emit runtimeLoadingFinished(); emit runtimeStartupFinished(true, {}); if (playAfterRuntimeStart) { @@ -675,15 +682,18 @@ void ViewportPanel::startRuntime() { emit playbackStateChanged(playbackState); } } catch (const std::exception &error) { + const QString message = QString::fromUtf8(error.what()); qWarning().noquote() << QStringLiteral("Failed to start Atlas viewport runtime: %1") - .arg(QString::fromUtf8(error.what())); + .arg(message); + emit runtimeErrorOccurred(message); runtimeContext.reset(); playAfterRuntimeStart = false; emit runtimeLoadingFinished(); - emit runtimeStartupFinished(false, QString::fromUtf8(error.what())); + emit runtimeStartupFinished(false, message); } catch (...) { qWarning() << "Failed to start Atlas viewport runtime"; + emit runtimeErrorOccurred("Runtime initialization failed"); runtimeContext.reset(); playAfterRuntimeStart = false; emit runtimeLoadingFinished(); @@ -691,6 +701,7 @@ void ViewportPanel::startRuntime() { } #else qWarning() << "Atlas viewport runtime embedding requires the Metal backend"; + emit runtimeErrorOccurred("Runtime embedding requires the Metal backend"); emit runtimeStartupFinished(false, "Runtime embedding requires the Metal backend"); #endif @@ -723,6 +734,8 @@ void ViewportPanel::stopRuntime() { runtimeWidth = 0; runtimeHeight = 0; runtimeScale = 0.0f; + snapshotTimer.invalidate(); + frameRateTimer.invalidate(); } bool ViewportPanel::stepRuntime() { @@ -731,20 +744,33 @@ bool ViewportPanel::stepRuntime() { } try { if (!runtimeContext->stepFrame()) { + emit runtimeErrorOccurred( + "The viewport runtime stopped unexpectedly"); stopRuntime(); return false; } - refreshSceneSnapshot(); - emit frameRateChanged(runtimeContext->frameRate()); + if (playbackState == 1 && + (!snapshotTimer.isValid() || + snapshotTimer.elapsed() >= RuntimeSnapshotIntervalMs)) { + refreshSceneSnapshot(); + } + if (!frameRateTimer.isValid() || + frameRateTimer.elapsed() >= RuntimeFrameRateIntervalMs) { + emit frameRateChanged(runtimeContext->frameRate()); + frameRateTimer.restart(); + } return true; } catch (const std::exception &error) { + const QString message = QString::fromUtf8(error.what()); qWarning().noquote() << QStringLiteral("Atlas viewport runtime frame failed: %1") - .arg(QString::fromUtf8(error.what())); + .arg(message); + emit runtimeErrorOccurred(message); stopRuntime(); return false; } catch (...) { qWarning() << "Atlas viewport runtime frame failed"; + emit runtimeErrorOccurred("The viewport runtime frame failed"); stopRuntime(); return false; } @@ -767,11 +793,14 @@ void ViewportPanel::resizeRuntime() { runtimeHeight = nextHeight; runtimeScale = nextScale; } catch (const std::exception &error) { + const QString message = QString::fromUtf8(error.what()); qWarning().noquote() << QStringLiteral("Atlas viewport resize failed: %1") - .arg(QString::fromUtf8(error.what())); + .arg(message); + emit runtimeErrorOccurred(message); } catch (...) { qWarning() << "Atlas viewport resize failed"; + emit runtimeErrorOccurred("The viewport runtime could not be resized"); } } @@ -898,8 +927,6 @@ bool ViewportPanel::setRuntimeSceneProperty(const QString §ion, int index, runtimeContext->saveCurrentScene(); refreshSceneSnapshot(); setSceneDirty(true); - if (section.compare("environment", Qt::CaseInsensitive) == 0) - environmentReloadTimer->start(); return true; } @@ -1362,6 +1389,7 @@ void ViewportPanel::stepRuntimeOnce() { stepRuntime(); if (runtimeContext != nullptr) { runtimeContext->setEditorSimulationEnabled(false); + refreshSceneSnapshot(); playbackState = 2; emit playbackStateChanged(playbackState); } @@ -1419,7 +1447,7 @@ void ViewportPanel::setPathTracingPreview(bool enabled) { frameTimer->stop(); const bool frameReady = stepRuntime(); if (frameReady && isVisible()) - frameTimer->start(pbrPreview ? 16 : 1); + frameTimer->start(RuntimeFrameIntervalMs); emit runtimeLoadingFinished(); if (!enabled && runtimeContext != nullptr) { const std::string error = runtimeContext->getPathTracingError(); @@ -1442,7 +1470,7 @@ bool ViewportPanel::applyPathTracingSettings( internalScale); const bool frameReady = applied && stepRuntime(); if (frameReady && isVisible()) { - frameTimer->start(pbrPreview ? 16 : 1); + frameTimer->start(RuntimeFrameIntervalMs); } return applied; } @@ -1594,6 +1622,7 @@ void ViewportPanel::refreshSceneSnapshot() { } const QString snapshot = QString::fromStdString(runtimeContext->sceneObjectsJson()); + snapshotTimer.restart(); if (snapshot == lastSceneSnapshot) { return; } diff --git a/editor/views/editor/viewportTools.cpp b/editor/views/editor/viewportTools.cpp index 457f1e35..d22a09dd 100644 --- a/editor/views/editor/viewportTools.cpp +++ b/editor/views/editor/viewportTools.cpp @@ -4,19 +4,70 @@ #include #include +#include #include #include #include #include #include #include +#include #include +#include #include #include #include #include #include +class ViewportHost : public QWidget { + public: + ViewportHost(ViewportPanel *viewport, qreal cameraAspect, QWidget *parent) + : QWidget(parent), viewport(viewport), cameraAspect(cameraAspect) { + viewport->setParent(this); + setAutoFillBackground(true); + QPalette background = palette(); + background.setColor(QPalette::Window, QColor("#08090B")); + setPalette(background); + } + + void setCameraView(bool enabled) { + cameraView = enabled; + updateViewportGeometry(); + } + + protected: + void resizeEvent(QResizeEvent *event) override { + QWidget::resizeEvent(event); + updateViewportGeometry(); + } + + private: + void updateViewportGeometry() { + if (!cameraView || cameraAspect <= 0.0) { + viewport->setGeometry(rect()); + return; + } + const qreal availableAspect = + height() > 0 ? static_cast(width()) / height() + : cameraAspect; + int targetWidth = width(); + int targetHeight = height(); + if (availableAspect > cameraAspect) { + targetWidth = qRound(targetHeight * cameraAspect); + } else { + targetHeight = qRound(targetWidth / cameraAspect); + } + viewport->setGeometry((width() - targetWidth) / 2, + (height() - targetHeight) / 2, targetWidth, + targetHeight); + } + + ViewportPanel *viewport; + qreal cameraAspect; + bool cameraView = false; +}; + ViewportTools::ViewportTools(ViewportPanel *viewport, const QString &projectFile, QWidget *parent) : QWidget(parent), viewport(viewport), @@ -122,11 +173,22 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, QFile manifest(projectFile); bool pathTracingProject = false; + qreal cameraAspect = 16.0 / 9.0; if (manifest.open(QIODevice::ReadOnly | QIODevice::Text)) { const QString contents = QString::fromUtf8(manifest.readAll()); pathTracingProject = contents.contains(QRegularExpression( QStringLiteral(R"(default\s*=\s*["']path[\s_-]*tracing["'])"), QRegularExpression::CaseInsensitiveOption)); + const auto dimensions = QRegularExpression( + QStringLiteral( + R"(dimensions\s*=\s*\[\s*(\d+)\s*,\s*(\d+)\s*\])")) + .match(contents); + if (dimensions.hasMatch()) { + const int width = dimensions.captured(1).toInt(); + const int height = dimensions.captured(2).toInt(); + if (width > 0 && height > 0) + cameraAspect = static_cast(width) / height; + } } auto *shadingGroup = new QActionGroup(toolbar); @@ -176,7 +238,8 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, tools->addWidget(fpsLabel); layout->addWidget(toolbar); - layout->addWidget(viewport, 1); + viewportHost = new ViewportHost(viewport, cameraAspect, this); + layout->addWidget(viewportHost, 1); shortcutHint = new QLabel("Tab Frame · Num 0 Camera · Shift+Middle/Right Pan · " "Middle/Right Orbit · G Move · R Rotate · S Scale · X " @@ -225,6 +288,7 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, ? "Main Camera view active · Esc or Numpad 0 to exit" : "Look through Main Camera · Numpad 0"); cameraLabel->setVisible(focused); + viewportHost->setCameraView(focused); }); connect(viewport, &ViewportPanel::transformSpaceChanged, this, [this](bool local) { diff --git a/editorExample.png b/editorExample.png new file mode 100644 index 00000000..3f83c710 Binary files /dev/null and b/editorExample.png differ diff --git a/include/atlas/core/default_shaders.h b/include/atlas/core/default_shaders.h index 34d95b48..a370c282 100644 --- a/include/atlas/core/default_shaders.h +++ b/include/atlas/core/default_shaders.h @@ -2738,7 +2738,6 @@ static inline __attribute__((always_inline)) float4 applyColorEffects(thread float4& color, constant PushConstants& _372, device EffectBuffer& _381, device EffectFloat1Buffer& _394, device EffectFloat2Buffer& _403, device EffectFloat3Buffer& _411, device EffectFloat4Buffer& _419, device EffectFloat5Buffer& _426, constant Uniforms& _849, device EffectFloat6Buffer& _1049, thread float4& gl_FragCoord) { ColorCorrection cc; - float3 _noise; for (int i = 0; i < _372.EffectCount; i++) { if (_381.Effects[i] == 0) @@ -2790,19 +2789,15 @@ float4 applyColorEffects(thread float4& color, constant PushConstants& _372, dev float amount = _394.EffectFloat1[i]; float3 seed = float3(gl_FragCoord.xy, _849.deltaTime * 100.0); float n = dot(seed, float3(12.98980045318603515625, 78.233001708984375, 45.16400146484375)); - _noise.x = fract(sin(n) * 43758.546875); - n = dot(seed, float3(93.9889984130859375, 67.345001220703125, 12.9890003204345703125)); - _noise.y = fract(sin(n) * 28001.123046875); - n = dot(seed, float3(39.34600067138671875, 11.1350002288818359375, 83.154998779296875)); - _noise.z = fract(sin(n) * 19283.45703125); - float3 grain = ((_noise - float3(0.5)) * 2.0) * amount; - float luminance = dot(color.xyz)", -R"(, float3(0.2989999949932098388671875, 0.58700001239776611328125, 0.114000000059604644775390625)); + float noise = fract(sin(n) * 43758.546875); + float grain = ((noise - 0.5) * 2.0) * amount; + float luminance = dot(color.xyz, float3(0.2989999949932098388671875, 0.58700001239776611328125, 0.114000000059604644775390625)); float visibility = 1.0 - (abs(luminance - 0.5) * 0.5); float4 _1210 = color; - float3 _1212 = _1210.xyz + (grain * visibility); + float3 _1212 = _1210.xyz + float3(grain * visibility); color.x = _1212.x; - color.y = _1212.y; + color.y = _12)", +R"(12.y; color.z = _1212.z; float4 _1219 = color; float3 _1223 = fast::clamp(_1219.xyz, float3(0.0), float3(1.0)); @@ -2986,8 +2981,7 @@ float4 cloudRendering(thread const float4& inColor, thread float2& TexCoord, con return inColor; } float dstLimit = fast::min(sceneDistance - distToContainer, distInContainer); - dstLimit = fast::)", -R"(max(dstLimit, 0.0); + dstLimit = fast::max(dstLimit, 0.0); if (dstLimit <= 9.9999997473787516355514526367188e-05) { return inColor; @@ -2997,7 +2991,8 @@ R"(max(dstLimit, 0.0); float stepSize = fast::max(baseStep, _1929.cloudMinStepLength); float3 param_4 = float3(TexCoord, _849.time); float jitter = hashNoise(param_4) - 0.5; - float travelled = fast::clamp(jitter, -0.3499999940395355224609375, 0.3499999940395355224609375) * stepSize; + float travelled = f)", +R"(ast::clamp(jitter, -0.3499999940395355224609375, 0.3499999940395355224609375) * stepSize; travelled = fast::max(travelled, 0.0); float3 accumulatedLight = float3(0.0); float transmittance = 1.0; @@ -3159,8 +3154,8 @@ float4 composeLighting(thread const float2& uv, thread const float4& baseColor, } static inline __attribute__((always_inline)) -float4 applyMotionBlur(thr)", -R"(ead const float2& texCoord, thread const float& size, thread const float& separation, thread const float4& color, constant PushConstants& _372, device EffectBuffer& _381, device EffectFloat1Buffer& _394, device EffectFloat2Buffer& _403, device EffectFloat3Buffer& _411, device EffectFloat4Buffer& _419, device EffectFloat5Buffer& _426, texture2d Texture, sampler TextureSmplr, texture2d BrightTexture, sampler BrightTextureSmplr, constant Uniforms& _849, texture2d VolumetricLightTexture, sampler VolumetricLightTextureSmplr, texture2d SSRTexture, sampler SSRTextureSmplr, texture2d PositionTexture, sampler PositionTextureSmplr, texture2d DepthTexture, sampler DepthTextureSmplr) +float4 applyMotionBlur(thread const float2& texCoord, thread const float& size, thread const float& separation, thread const float4& color, constant PushConstants& _372, device EffectBuffer& _381, device EffectFloat1Buffer& _394, device EffectFloat2Buffer& _403, device EffectFloat3Buffer& _411, device EffectFloat4Buffer& _419, device EffectFloat5Buffer& _426, texture2d Texture, sampler TextureSmplr, texture2d BrightTexture, sampler BrightTextureSmplr, constant Uniforms& _849, texture2d VolumetricLightTexture, sampler VolumetricLightTextureSmplr, texture2d SSRTexture, sampler SSRTextureSmplr, texture2d PositionTexture, sampler PositionTextureSmplr, texture2d DepthTexture, sampler DepthTextureSmplr) { float4 fallbackColor = composeLighting(texCoord, color, _372, _381, _394, _403, _411, _419, _426, BrightTexture, BrightTextureSmplr, VolumetricLightTexture, VolumetricLightTextureSmplr, SSRTexture, SSRTextureSmplr); if ((size <= 0.0) || (separation <= 0.0)) @@ -3343,8 +3338,8 @@ float3 acesToneMapping(thread const float3& color) return fast::clamp((color * ((color * a) + float3(b))) / ((color * ((color * c) + float3(d))) + float3(e)), float3(0.0), float3(1.0)); } -fragment main0_out main0(main0_in in [[stage_in]], constant PushConstants& _372 [[buffer(0)]], device EffectBuffer& _381 [[buffer(1)]], device EffectFloat1Buffer& _394 [[buffer(2)]], device EffectFloat2Buffer& _403 [[buffer(3)]], device EffectFloat3Buffer& _411 [[buffer(4)]], device EffectFloat4Buffer& _419 [[buffer(5)]], device EffectFloat5Buffer& _426 [[buffer(6)]], constant Uniforms& _849 [[buffer(7)]], device EffectFloat6Bu)", -R"(ffer& _1049 [[buffer(8)]], constant Clouds& _1929 [[buffer(9)]], constant Environment& environment [[buffer(10)]], texture2d Texture [[texture(0)]], texture2d BrightTexture [[texture(1)]], texture2d VolumetricLightTexture [[texture(2)]], texture2d SSRTexture [[texture(3)]], texture2d PositionTexture [[texture(4)]], texture2d LUTTexture [[texture(5)]], texture3d cloudsTexture [[texture(6)]], texture2d DepthTexture [[texture(7)]], sampler TextureSmplr [[sampler(0)]], sampler BrightTextureSmplr [[sampler(1)]], sampler VolumetricLightTextureSmplr [[sampler(2)]], sampler SSRTextureSmplr [[sampler(3)]], sampler PositionTextureSmplr [[sampler(4)]], sampler LUTTextureSmplr [[sampler(5)]], sampler cloudsTextureSmplr [[sampler(6)]], sampler DepthTextureSmplr [[sampler(7)]], float4 gl_FragCoord [[position]]) +fragment main0_out main0(main0_in in [[stage_in]], constant PushConstants& _372 [[buffer(0)]], device EffectBuffer& _381 [[buffer(1)]], device EffectFloat1Buffer& _394 [[buffer(2)]], device EffectFloat2Buffer& _403 [[buffer(3)]], device EffectFloat3Buffer& _411 [[buffer(4)]], device EffectFloat4Buffer& _419 [[buffer(5)]], device EffectFloat5Buffer& _426 [[buffer(6)]], constant Uniforms& _849 [[buffer(7)]], device EffectFloat6Buffer& _1049 [[buffer(8)]], constant Clouds& _1929 [[buffer(9)]], constant Environment& environment [[buffer(10)]], texture2d Texture [[texture(0)]], texture2d BrightTexture [[texture(1)]], texture2d VolumetricLightTexture [[texture(2)]], texture2d SSRTexture [[texture(3)]], texture2d PositionTexture [[texture(4)]], texture2d LUTTexture [[texture(5)]], )", +R"(texture3d cloudsTexture [[texture(6)]], texture2d DepthTexture [[texture(7)]], sampler TextureSmplr [[sampler(0)]], sampler BrightTextureSmplr [[sampler(1)]], sampler VolumetricLightTextureSmplr [[sampler(2)]], sampler SSRTextureSmplr [[sampler(3)]], sampler PositionTextureSmplr [[sampler(4)]], sampler LUTTextureSmplr [[sampler(5)]], sampler cloudsTextureSmplr [[sampler(6)]], sampler DepthTextureSmplr [[sampler(7)]], float4 gl_FragCoord [[position]]) { main0_out out = {}; float2 param = in.TexCoord; @@ -3693,6 +3688,7 @@ struct UBO { struct Environment { float rimLightIntensity; float3 rimLightColor; + float bloomThreshold; }; struct PushConstants { @@ -3821,9 +3817,9 @@ constant spvUnsafeArray _660 = spvUnsafeArray( {float2(-0.3260000050067901611328125, -0.4059999883174896240234375), float2(-0.839999973773956298828125, -0.07400000095367431640625), float2(-0.69599997997283935546875, 0.4569999873638153076171875), - float2(-0.20299999415874481201171875, 0.620999991893768310546875), - float2(0.96200001)", -R"(239776611328125, -0.194999992847442626953125), + float2(-0.20299999415874481201171875, 0.62099999189376831054687)", +R"(5), + float2(0.96200001239776611328125, -0.194999992847442626953125), float2(0.472999989986419677734375, -0.4799999892711639404296875), float2(0.518999993801116943359375, 0.767000019550323486328125), float2(0.185000002384185791015625, -0.89300000667572021484375), @@ -4005,9 +4001,9 @@ static inline __attribute__((always_inline)) float4 sampleTextureAt( } else { if (textureIndex == 1) { return texture2.sample(texture2Smplr, uv); - } else { - if (textureIn)", -R"(dex == 2) { + } else {)", +R"( + if (textureIndex == 2) { return texture3.sample(texture3Smplr, uv); } else { if (textureIndex == 3) { @@ -4241,8 +4237,8 @@ static inline __attribute__((always_inline)) float3 calcDirectionalLight( thread const float3 &albedo, thread const float &metallic, thread const float &roughness) { float3 L = fast::normalize(-light.direction); - float3 radiance = light.diffuse * fast:)", -R"(:max(light.intensity, 0.0); + float3 radian)", +R"(ce = light.diffuse * fast::max(light.intensity, 0.0); float3 param = L; float3 param_1 = radiance; float3 param_2 = N; @@ -4455,8 +4451,8 @@ static inline float4 sampleProbeDirectionalRadiance(texture2d ddgiTexture, constant ProbeSpace &ps, uint probeIndex, uint atlasW, uint atlasH, float3 dirWS) { - float2 uv = ddgiAtlasUV(probeIndex, dirWS, ps, atlasW)", -R"(, atlasH); + float2 uv = ddgiAtlasUV(pro)", +R"(beIndex, dirWS, ps, atlasW, atlasH); return sampleDDGITextureBilinear(ddgiTexture, uv); } @@ -4628,9 +4624,9 @@ static inline float3 sampleDDGIIrradiance(texture2d ddgiTexture, fragment main0_out main0( main0_in in [[stage_in]], constant UBO &_526 [[buffer(0)]], - constant Environment &environment [[buffer(1)]], - )", -R"( constant PushConstants &_1355 [[buffer(2)]], + constant Environment &envi)", +R"(ronment [[buffer(1)]], + constant PushConstants &_1355 [[buffer(2)]], device ShadowParams &_1372 [[buffer(3)]], device DirectionalLights &_1422 [[buffer(4)]], device PointLights &_1465 [[buffer(5)]], @@ -4797,8 +4793,8 @@ R"( constant PushConstants &_1355 [[buffer(2)]], float3 param_4 = shadowNormal; spotShadow = fast::max( spotShadow, - calculateShadow(param_2, param_3, pa)", -R"(ram_4, texture1, + calculateS)", +R"(hadow(param_2, param_3, param_4, texture1, texture1Smplr, texture2, texture2Smplr, texture3, texture3Smplr, texture4, texture4Smplr, texture5, texture5Smplr, _526)); @@ -4959,8 +4955,8 @@ R"(ram_4, texture1, float attenuation = 1.0 / ((1.0 + (dist / range)) + ((dist * dist) / (range * range))); float fade = 1.0 - smoothstep(range * 0.89999997615814208984375, - )", -R"( range, dist); + )", +R"( range, dist); float3 radiance = (((float3(_1552.areaLights[i_4].diffuse) * fast::max(_1552.areaLights[i_4].intensity, 0.0)) * @@ -5076,7 +5072,7 @@ R"( range, dist); dot(out.FragColor.xyz, float3(0.2125999927520751953125, 0.715200006961822509765625, 0.072200000286102294921875)); - if (brightness > 0.75) { + if (brightness > environment.bloomThreshold) { out.BrightColor = float4(out.FragColor.xyz, 1.0); } else { out.BrightColor = float4(0.0, 0.0, 0.0, 1.0); @@ -6753,9 +6749,10 @@ struct SceneData { uint accumulationFrameLimit; float fireflyClamp; uint numEmissiveTriangles; + float bloomThreshold; }; -static_assert(sizeof(SceneData) == 144); +static_assert(sizeof(SceneData) == 160); static_assert(__builtin_offsetof(SceneData, atmosphereSunDirection) == 48); static_assert(__builtin_offsetof(SceneData, atmosphereSunIntensity) == 64); static_assert(__builtin_offsetof(SceneData, atmosphereSunColor) == 80); @@ -6763,6 +6760,7 @@ static_assert(__builtin_offsetof(SceneData, pixelStride) == 96); static_assert(__builtin_offsetof(SceneData, ambientColor) == 112); static_assert(__builtin_offsetof(SceneData, accumulationFrameLimit) == 132); static_assert(__builtin_offsetof(SceneData, numEmissiveTriangles) == 140); +static_assert(__builtin_offsetof(SceneData, bloomThreshold) == 144); float pow5(float x) { float x2 = x * x; @@ -6900,13 +6898,13 @@ float2 encodeNormal(float3 normal) { float2 encoded = normal.xy; if (normal.z < 0.0) { float2 signValue = select(float2(-1.0), float2(1.0), encoded >= 0.0); - encoded = (1.0 - abs(encoded.yx)) * signValue; + encoded = (1.0 - a)", +R"(bs(encoded.yx)) * signValue; } return encoded; } -constexpr sampler materialTexSampler()", -R"(coord::normalized, address::repeat, +constexpr sampler materialTexSampler(coord::normalized, address::repeat, filter::linear, mip_filter::linear); #define PT_MATERIAL_TEXTURE_PARAMS \ @@ -7010,9 +7008,9 @@ R"(coord::normalized, address::repeat, texture2d materialTexture35 [[texture(47)]], \ texture2d materialTexture36 [[texture(48)]], \ texture2d materialTexture37 [[texture(49)]], \ - texture2d materialTexture38 [[texture(50)]], \ - texture2d)", -R"( materialTexture39 [[texture(51)]], \ + )", +R"( texture2d materialTexture38 [[texture(50)]], \ + texture2d materialTexture39 [[texture(51)]], \ texture2d materialTexture40 [[texture(52)]], \ texture2d materialTexture41 [[texture(53)]], \ texture2d materialTexture42 [[texture(54)]], \ @@ -7205,9 +7203,9 @@ void resolveMaterialParameters(Material mat, float2 uv, uint textureCount, } roughness *= clamp(roughnessValue, 0.0, 1.0); } - if (mat.aoTextureIndex >= 0 && uint(mat.aoTextureIndex) < textureCount) { - ao *= clamp(sampleMaterialTexture(mat.aoTextureIndex, )", -R"(uv, + if (mat.aoTextureIndex >= 0 && uint(mat.a)", +R"(oTextureIndex) < textureCount) { + ao *= clamp(sampleMaterialTexture(mat.aoTextureIndex, uv, PT_MATERIAL_TEXTURE_ARGS) .x, 0.0, 1.0); @@ -7393,10 +7391,10 @@ float3 materialF0(float3 albedo, float metallic, float reflectivity, float G_Smith(float NdotV, float NdotL, float roughness) { float r = roughness + 1.0; - float k = (r * r) / 8.0; + float k = (r * r))", +R"( / 8.0; float gV = NdotV / (NdotV * (1.0 - k) + k); - float gL = NdotL / (NdotL * (1.0 - )", -R"(k) + k); + float gL = NdotL / (NdotL * (1.0 - k) + k); return gV * gL; } @@ -7577,10 +7575,10 @@ float3 evalEmissiveTriangleLighting( // --------------------------------------------------------------------------- float3 evalDirectLightingPBR(intersector isect, - primitive_acceleration_structure sceneAS, float3 P, + primitive_acceleration_structure )", +R"(sceneAS, float3 P, float3 N, float3 Ng, float3 V, float3 albedo, - )", -R"( float metallic, float roughness, float reflectivity, + float metallic, float roughness, float reflectivity, float ior, float transmittance, float sssStrength, float sssThickness, thread uint &rng, @@ -7730,10 +7728,10 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, constant EmissiveTriangle *emissiveTriangles, PT_MATERIAL_TEXTURE_PARAMS, texturecube skybox, thread float3 &primaryAlbedo, - thread float3 &primaryNormal, + )", +R"(thread float3 &primaryNormal, thread float3 &primaryPosition, - )", -R"( thread float &primaryDepth, + thread float &primaryDepth, thread float &primaryRoughness, thread float &primaryHitDistance, thread uint &primaryObjectId) { @@ -7905,10 +7903,10 @@ R"( thread float &primaryDepth, (1.0 - fresnelProbability); float diffuseProb = (1.0 - metallic) * (1.0 - transmittance) * (1.0 - fresnelProbability); - float eta = frontFace ? 1.0 / ior : ior; + float eta = )", +R"(frontFace ? 1.0 / ior : ior; float3 idealRefractedDirection = refract(-V, N, eta); - )", -R"( bool totalInternalReflection = + bool totalInternalReflection = dot(idealRefractedDirection, idealRefractedDirection) < 1e-8; if (totalInternalReflection) { specProb += transmitProb; @@ -8076,10 +8074,10 @@ R"( bool totalInternalReflection = if (depth >= 2) { float survival = clamp(max(throughput.x, - max(throughput.y, throughput.z)), + )", +R"( max(throughput.y, throughput.z)), 0.05, 0.95); - )", -R"( if (rand(rng) > survival) { + if (rand(rng) > survival) { break; } throughput /= survival; @@ -8254,10 +8252,10 @@ kernel void main0(texture2d outTex [[texture(0)]], historyValid ? min(prevColor.w, max(historyLimit - 1.0, 0.0)) : 0.0; float newHistoryLength = min(previousWeight + 1.0, historyLimit); float accumulationDenominator = max(previousWeight + 1.0, 1.0); - float3 accum = - (prevColor.xyz * previousWeight + color) / accumulationDenominator; )", -R"( float moment = luminance(color); +R"( float3 accum = + (prevColor.xyz * previousWeight + color) / accumulationDenominator; + float moment = luminance(color); float accumulatedMoment = (previousMoments.x * previousWeight + moment) / accumulationDenominator; @@ -8268,14 +8266,13 @@ R"( float moment = luminance(color); accumulatedMoment * accumulatedMoment, 0.0); - constexpr float bloomThreshold = 0.8; constexpr float bloomKnee = 0.35; float brightness = luminance(accum); - float soft = clamp(brightness - bloomThreshold + bloomKnee, 0.0, + float soft = clamp(brightness - sceneData.bloomThreshold + bloomKnee, 0.0, bloomKnee * 2.0); soft = soft * soft / max(bloomKnee * 4.0, 0.00001); - float contribution = max(brightness - bloomThreshold, soft) / + float contribution = max(brightness - sceneData.bloomThreshold, soft) / max(brightness, 0.00001); float3 brightColor = accum * contribution; float2 motion = previousUvValid ? uv - previousUv : float2(0.0); @@ -8311,6 +8308,7 @@ using namespace metal; struct DenoiseParameters { int stepWidth; + float bloomThreshold; }; kernel void main0(texture2d inputTexture [[texture(0)]], @@ -8419,12 +8417,11 @@ kernel void main0(texture2d inputTexture [[texture(0)]], } float3 result = mix(center, spatialResult, filterStrength); float brightness = dot(result, float3(0.2126, 0.7152, 0.0722)); - constexpr float bloomThreshold = 0.8; constexpr float bloomKnee = 0.35; - float soft = clamp(brightness - bloomThreshold + bloomKnee, 0.0, + float soft = clamp(brightness - parameters.bloomThreshold + bloomKnee, 0.0, bloomKnee * 2.0); soft = soft * soft / max(bloomKnee * 4.0, 0.00001); - float contribution = max(brightness - bloomThreshold, soft) / + float contribution = max(brightness - parameters.bloomThreshold, soft) / max(brightness, 0.00001); outputTexture.write(float4(result, 1.0), gid); brightTexture.write(float4(result * contribution, 1.0), gid); diff --git a/include/atlas/runtime/atlasScripts.h b/include/atlas/runtime/atlasScripts.h index d90e95d0..4fbff232 100644 --- a/include/atlas/runtime/atlasScripts.h +++ b/include/atlas/runtime/atlasScripts.h @@ -13,91 +13,85 @@ struct AtlasRuntimeScriptModule { AtlasPackedScriptSource source; }; -static const char* const ATLAS_PARTS[] = { +static const char* const SCRIPTS_ATLAS_PARTS[] = { "import { AxisTrigger, InputAction, Key, MouseButton, Trigger } from \"atlas/input\";\nimport { Color, Position2d, Position3d } from \"atlas/units\";\n\nconst windowConstants = globalThis.__atlasGetWindowConstants?.() ?? {};\n\nexport const ControllerAxis = Object.freeze(windowConstants.ControllerAxis ?? {});\nexport const ControllerButton = Object.freeze(\n windowConstants.ControllerButton ?? {},\n);\nexport const NintendoControllerButton = Object.freeze(\n windowConstants.NintendoControllerButton ?? {},\n);\nexport const SonyControllerButton = Object.freeze(\n windowConstants.SonyControllerButton ?? {},\n);\nexport const CONTROLLER_UNDEFINED =\n windowConstants.CONTROLLER_UNDEFINED ?? -2;\n\nfunction makeControllerAxisTrigger(controllerId, axis) {\n switch (axis) {\n case ControllerAxis.LeftStick:\n return AxisTrigger.fromControllerAxis(controllerId, 0, false, 1);\n case ControllerAxis.LeftStickX:\n return AxisTrigger.fromControllerAxis(controllerId, 0, true);\n case ControllerAxis.LeftStickY:\n return AxisTrigger.fromControllerAxis(controllerId, 1, true);\n case ControllerAxis.RightStick:\n return AxisTrigger.fromControllerAxis(controllerId, 2, false, 3);\n case ControllerAxis.RightStickX:\n return AxisTrigger.fromControllerAxis(controllerId, 2, true);\n case ControllerAxis.RightStickY:\n return AxisTrigger.fromControllerAxis(controllerId, 3, true);\n case ControllerAxis.Trigger:\n return AxisTrigger.fromControllerAxis(controllerId, 4, false, 5);\n case ControllerAxis.TriggerLeft:\n case ControllerAxis.LeftTrigger:\n return AxisTrigger.fromControllerAxis(controllerId, 4, true);\n case ControllerAxis.TriggerRight:\n case ControllerAxis.RightTrigger:\n return AxisTrigger.fromControllerAxis(controllerId, 5, true);\n default:\n return new AxisTrigger();\n }\n}\n\nexport class Scene {\n constructor() {\n this.name = \"\";\n this.atmosphere = null;\n return globalThis.__atlasGetScene() ?? this;\n }\n\n setAmbientIntensity(intensity) {\n return globalThis.__atlasSetSceneAmbientIntensity(this, intensity);\n }\n\n setAutomaticAmbient(enabled) {\n return globalThis.__atlasSetSceneAutomaticAmbient(this, enabled);\n }\n\n setSkybox(skybox) {\n return globalThis.__atlasSetSceneSkybox(this, skybox);\n }\n\n useAtmosphereSkybox(enabled) {\n return globalThis.__atlasUseAtmosphereSkybox(this, enabled);\n }\n\n setEnvironment(environment) {\n return globalThis.__atlasSetSceneEnvironment(this, environment);\n }\n\n setAmbientColor(color) {\n return globalThis.__atlasSetSceneAmbientColor(this, color);\n }\n\n addDirectionalLight(light) {\n return globalThis.__atlasSceneAddDirectionalLight(this, light);\n }\n\n addLight(light) {\n return globalThis.__atlasSceneAddLight(this, light);\n }\n\n addSpotLight(light) {\n return globalThis.__atlasSceneAddSpotLight(this, light);\n }\n\n addAreaLight(light) {\n return globalThis.__atlasSceneAddAreaLight(this, light);\n }\n\n getCamera() {\n return globalThis.__atlasGetCamera();\n }\n\n getWindow() {\n return globalThis.__atlasGetWindow();\n }\n}\n\nexport class Component {\n init() {}\n beforePhysics() {}\n atAttach() {}\n update(deltaTime) {}\n onCollisionEnter(other) {}\n onCollisionStay(other) {}\n onCollisionExit(other) {}\n onSignalRecieve(signal, sender) {}\n onSignalEnd(signal, sender) {}\n onQueryRecieve(query, sender) {}\n onQueryReceive(query, sender) {}\n\n getParent(type) {\n const parent = globalThis.__atlasGetObjectById(this.parentId);\n if (type == null) {\n return parent;\n }\n return parent?.getComponent(type) ?? null;\n }\n\n getObject(identifier) {\n if (typeof identifier === \"number\") {\n return globalThis.__atlasGetObjectById(identifier);\n }\n if (typeof identifier === \"string\") {\n return globalThis.__atlasGetObjectByName(identifier);\n }\n return null;\n }\n\n getScene() {\n return globalThis.__atlasGetScene();\n }\n\n getWindow() {\n return globalThis.__atlasGetWindow();\n }\n\n getCamera() {\n return this.getScene()?.getCamera() ?? null;\n }\n}\n\nexport class Material {\n constructor() {\n this.albedo = Color.white();\n this.metallic = 0;\n this.roughness = 0.5;\n this.ao = 1;\n this.reflectivity = 0;\n this.emissiveColor = Color.black();\n this.emissiveIntensity = 0;\n this.normalMapStrength = 1;\n this.useNormalMap = true;\n this.transmittance = 0;\n this.ior = 1;\n }\n}\n\nexport class CoreVertex {\n constructor(\n position = Position3d.zero(),\n color = Color.white(),\n textureCoord = Position2d.zero(),\n normal = Position3d.zero(),\n tangent = Position3d.zero(),\n bitangent = Position3d.zero(),\n ) {\n this.position = position;\n this.color = color;\n this.textureCoord = textureCoord;\n this.normal = normal;\n this.tangent = tangent;\n this.bitangent = bitangent;\n }\n}\n\nexport class Instance {\n move(position) {\n this.position.x += position.x;\n this.position.y += position.y;\n this.position.z += position.z;\n globalThis.__atlasCommitInstance(this);\n }\n\n setPosition(position) {\n this.position = position;\n globalThis.__atlasCommitInstance(this);\n }\n\n setRotation(rotation) {\n this.rotation = rotation;\n globalThis.__atlasCommitInstance(this);\n }\n\n rotate(rotation) {\n this.rotation.x += rotation.x;\n this.rotation.y += rotation.y;\n this.rotation.z += rotation.z;\n globalThis.__atlasCommitInstance(this);\n }\n\n setScale(scale) {\n this.scale = scale;\n globalThis.__atlasCommitInstance(this);\n }\n\n scaleBy(scale) {\n this.scale.x *= scale.x;\n this.scale.y *= scale.y;\n this.scale.z *= scale.z;\n globalThis.__atlasCommitInstance(this);\n }\n\n equals(other) {\n return (\n this.position.is(other.position) &&\n this.rotation.is(other.rotation) &&\n this.scale.is(other.scale)\n );\n }\n}\n\nexport class GameObject {\n constructor() {\n this.id = -1;\n this.components = [];\n this.position = Position3d.zero();\n this.rotation = Position3d.zero();\n this.scale = new Position3d(1, 1, 1);\n this.name = \"\";\n }\n\n attachTexture(texture) {\n return globalThis.__atlasAttachTexture(this, texture);\n }\n\n setPosition(position) {\n this.position = position;\n globalThis.__atlasUpdateObject(this);\n }\n\n move(position) {\n this.position.x += position.x;\n this.position.y += position.y;\n this.position.z += position.z;\n globalThis.__atlasUpdateObject(this);\n }\n\n setRotation(rotation) {\n this.rotation = rotation;\n globalThis.__atlasUpdateObject(this);\n }\n\n lookAt(target, up = Position3d.up()) {\n return globalThis.__atlasLookAtObject(this, target, up);\n }\n\n rotate(rotation) {\n this.rotation.x += rotation.x;\n this.rotation.y += rotation.y;\n this.rotation.z += rotation.z;\n globalThis.__atlasUpdateObject(this);\n }\n\n setScale(scale) {\n this.scale = scale;\n globalThis.__atlasUpdateObject(this);\n }\n\n scaleBy(scale) {\n this.scale.x *= scale.x;\n this.scale.y *= scale.y;\n this.scale.z *= scale.z;\n globalThis.__atlasUpdateObject(this);\n }\n\n show() {\n globalThis.__atlasShowObject(this.id);\n }\n\n hide() {\n globalThis.__atlasHideObject(this.id);\n }\n\n as(type) {\n if (type == null) {\n return null;\n }\n const object = globalThis.__atlasGetObjectById(this.id) ?? this;\n return object instanceof type ? object : null;\n }\n\n addComponent(component) {\n return globalThis.__atlasAddComponent(this.i", "d, component);\n }\n}\n\nexport class UIObject extends GameObject {\n getSize() {\n return globalThis.__graphiteGetUISize(this);\n }\n\n getScreenPosition() {\n return globalThis.__graphiteGetUIScreenPosition(this);\n }\n\n setScreenPosition(position) {\n this.position = new Position3d(position.x, position.y, 0);\n return globalThis.__graphiteSetUIScreenPosition(this, position);\n }\n\n setPosition(position) {\n this.position = position;\n return globalThis.__graphiteSetUIScreenPosition(\n this,\n new Position2d(position.x, position.y),\n );\n }\n\n move(position) {\n const screenPosition = this.getScreenPosition();\n return this.setScreenPosition(\n new Position2d(\n screenPosition.x + position.x,\n screenPosition.y + position.y,\n ),\n );\n }\n}\n\nexport class CoreObject extends GameObject {\n constructor() {\n super();\n this.vertices = [];\n this.indices = [];\n this.textures = [];\n this.material = new Material();\n this.instances = [];\n this.castsShadows = true;\n globalThis.__atlasCreateCoreObject(this);\n }\n\n makeEmissive(color, intensity) {\n globalThis.__atlasMakeEmissive(this.id, color, intensity);\n }\n\n attachVertices(vertices) {\n this.vertices = vertices;\n globalThis.__atlasUpdateCoreObject(this, this.id);\n }\n\n attachIndices(indices) {\n this.indices = indices;\n globalThis.__atlasUpdateCoreObject(this, this.id);\n }\n\n setPosition(position) {\n this.position = position;\n globalThis.__atlasUpdateCoreObject(this, this.id);\n }\n\n move(position) {\n this.position.x += position.x;\n this.position.y += position.y;\n this.position.z += position.z;\n globalThis.__atlasUpdateCoreObject(this, this.id);\n }\n\n setRotation(rotation) {\n this.rotation = rotation;\n globalThis.__atlasUpdateCoreObject(this, this.id);\n }\n\n setRotationQuaternion(rotation) {\n return globalThis.__atlasSetRotationQuaternion(this.id, rotation);\n }\n\n lookAt(target, up = Position3d.up()) {\n return globalThis.__atlasLookAtObject(this, target, up);\n }\n\n rotate(rotation) {\n this.rotation.x += rotation.x;\n this.rotation.y += rotation.y;\n this.rotation.z += rotation.z;\n globalThis.__atlasUpdateCoreObject(this, this.id);\n }\n\n setScale(scale) {\n this.scale = scale;\n globalThis.__atlasUpdateCoreObject(this, this.id);\n }\n\n scaleBy(scale) {\n this.scale.x *= scale.x;\n this.scale.y *= scale.y;\n this.scale.z *= scale.z;\n globalThis.__atlasUpdateCoreObject(this, this.id);\n }\n\n clone() {\n return globalThis.__atlasCloneCoreObject(this);\n }\n\n enableDeferredRendering() {\n globalThis.__atlasEnableDeferredRendering(this.id);\n }\n\n disableDeferredRendering() {\n globalThis.__atlasDisableDeferredRendering(this.id);\n }\n\n createInstance() {\n return globalThis.__atlasCreateInstance(this.id);\n }\n\n getComponent(type) {\n if (type == null) {\n return null;\n }\n return globalThis.__atlasGetComponentByName(this.id, type.name);\n }\n\n static box(size) {\n return globalThis.__atlasCreateBox(size);\n }\n\n static plane(size) {\n return globalThis.__atlasCreatePlane(size);\n }\n\n static pyramid(size) {\n return globalThis.__atlasCreatePyramid(size);\n }\n\n static sphere(radius, sectorCount = 36, stackCount = 18) {\n return globalThis.__atlasCreateSphere(radius, sectorCount, stackCount);\n }\n}\n\nexport class Model extends GameObject {\n static fromResource(path) {\n return globalThis.__atlasCreateModel(path);\n }\n\n getObjects() {\n return globalThis.__atlasGetModelObjects(this);\n }\n\n move(position) {\n this.position.x += position.x;\n this.position.y += position.y;\n this.position.z += position.z;\n return globalThis.__atlasMoveModel(this, position);\n }\n\n setPosition(position) {\n this.position = position;\n return globalThis.__atlasSetModelPosition(this, position);\n }\n\n setRotation(rotation) {\n this.rotation = rotation;\n return globalThis.__atlasSetModelRotation(this, rotation);\n }\n\n lookAt(target, up = Position3d.up()) {\n return globalThis.__atlasLookAtModel(this, target, up);\n }\n\n rotate(rotation) {\n this.rotation.x += rotation.x;\n this.rotation.y += rotation.y;\n this.rotation.z += rotation.z;\n return globalThis.__atlasRotateModel(this, rotation);\n }\n\n setScale(scale) {\n this.scale = scale;\n return globalThis.__atlasSetModelScale(this, scale);\n }\n\n scaleBy(scale) {\n this.scale.x *= scale.x;\n this.scale.y *= scale.y;\n this.scale.z *= scale.z;\n return globalThis.__atlasScaleModelBy(this, scale);\n }\n\n show() {\n return globalThis.__atlasShowModel(this.id);\n }\n\n hide() {\n return globalThis.__atlasHideModel(this.id);\n }\n\n attachTexture(texture) {\n return globalThis.__atlasAttachTexture(this, texture);\n }\n}\n\nexport const ResourceType = Object.freeze({\n File: 0,\n Texture: 1,\n SpecularMap: 2,\n Audio: 3,\n Font: 4,\n Model: 5,\n});\n\nexport class Resource {\n constructor(type, path, name) {\n this.type = type;\n this.path = path;\n this.name = name;\n }\n\n static fromAssetPath(path, type, name) {\n return globalThis.__atlasLoadResource(path, type, name);\n }\n\n static fromName(name, type) {\n return globalThis.__atlasGetResourceByName(name, type);\n }\n}\n\nexport class ResourceGroup {\n constructor(resources, name) {\n this.resources = resources;\n this.name = name;\n }\n\n addResource(resource) {\n this.resources.push(resource);\n }\n\n getResourceByName(name) {\n return this.resources.find((r) => r.name === name) || null;\n }\n}\n\nexport class Monitor {\n constructor() {\n this.monitorId = -1;\n this.primary = false;\n }\n\n queryVideoModes() {\n return globalThis.__atlasMonitorQueryVideoModes(this);\n }\n\n getCurrentVideoMode() {\n return globalThis.__atlasMonitorGetCurrentVideoMode(this);\n }\n\n getPhysicalSize() {\n return globalThis.__atlasMonitorGetPhysicalSize(this);\n }\n\n getPosition() {\n return globalThis.__atlasMonitorGetPosition(this);\n }\n\n getContentScale() {\n return globalThis.__atlasMonitorGetContentScale(this);\n }\n\n getName() {\n return globalThis.__atlasMonitorGetName(this);\n }\n}\n\nexport class Gamepad {\n constructor() {\n this.controllerId = CONTROLLER_UNDEFINED;\n this.name = \"\";\n this.connected = false;\n }\n\n getAxisTrigger(axis) {\n return makeControllerAxisTrigger(this.controllerId, axis);\n }\n\n static getGlobalAxisTrigger(axis) {\n return makeControllerAxisTrigger(CONTROLLER_UNDEFINED, axis);\n }\n\n getButtonTrigger(button) {\n return Trigger.fromControllerButton(this.controllerId, button);\n }\n\n static getGlobalButtonTrigger(button) {\n return Trigger.fromControllerButton(CONTROLLER_UNDEFINED, button);\n }\n\n runble(strength, duration) {\n globalThis.__atlasGamepadRumble(this, strength, duration);\n }\n\n rumble(strength, duration) {\n this.runble(strength, duration);\n }\n}\n\nexport class Joystick {\n constructor() {\n this.joystickId = -1;\n this.name = \"\";\n this.connected = false;\n }\n\n getSingleAxisTrigger(axisIndex) {\n return AxisTrigger.fromControllerAxis(this.joystickId, axisIndex, true);\n }\n\n getDualAxisTrigger(axisIndexX, axisIndexY) {\n return AxisTrigger.fromControllerAxis(\n this.joystickId,\n axisIndexX,\n false,\n axisIndexY,\n );\n }\n\n getButtonTrigger(buttonIndex) {\n return Trigger.fromControllerButton(this.joystickId, buttonIndex);\n }\n\n getAxisCount() {", "\n return globalThis.__atlasJoystickGetAxisCount(this);\n }\n\n getButtonCount() {\n return globalThis.__atlasJoystickGetButtonCount(this);\n }\n}\n\nexport class Window {\n constructor() {\n this._title = \"\";\n this._width = 0;\n this._height = 0;\n this._currentFrame = 0;\n this._gravity = 9.81;\n this._usesDeferred = false;\n this.audioEngine = null;\n return globalThis.__atlasGetWindow() ?? this;\n }\n\n get title() {\n return this._title;\n }\n\n set title(value) {\n this._title = value;\n }\n\n get width() {\n return this._width;\n }\n\n set width(value) {\n this._width = value;\n }\n\n get height() {\n return this._height;\n }\n\n set height(value) {\n this._height = value;\n }\n\n get currentFrame() {\n return this._currentFrame;\n }\n\n set currentFrame(value) {\n this._currentFrame = value;\n }\n\n get main() {\n return globalThis.__atlasGetWindow() ?? this;\n }\n\n get gravity() {\n return this._gravity;\n }\n\n set gravity(value) {\n this._gravity = value;\n globalThis.__atlasSetWindowGravity?.(this, value);\n }\n\n get usesDeferred() {\n return this._usesDeferred;\n }\n\n set usesDeferred(value) {\n this._usesDeferred = value;\n }\n\n setClearColor(color) {\n globalThis.__atlasSetWindowClearColor(this, color);\n }\n\n close() {\n globalThis.__atlasCloseWindow(this);\n }\n\n setFullscreen(value = true) {\n if (value != null && typeof value === \"object\") {\n globalThis.__atlasSetWindowFullscreenMonitor(this, value);\n return;\n }\n globalThis.__atlasSetWindowFullscreen(this, value !== false);\n }\n\n setWindowed(config) {\n globalThis.__atlasSetWindowed(this, config);\n }\n\n enumerateMonitors() {\n return globalThis.__atlasEnumerateMonitors(this);\n }\n\n getControllers() {\n return globalThis.__atlasGetControllers(this);\n }\n\n getController(id) {\n return globalThis.__atlasGetController(this, id);\n }\n\n getJoystick(id) {\n return globalThis.__atlasGetJoystick(this, id);\n }\n\n instantiate(object) {\n globalThis.__atlasInstantiateObject(this, object);\n }\n\n destroy(object) {\n globalThis.__atlasDestroyObject(this, object);\n }\n\n addUIObject(object) {\n globalThis.__atlasAddUIObject(this, object);\n }\n\n setCamera(camera) {\n globalThis.__atlasSetWindowCamera(this, camera);\n }\n\n setScene(scene) {\n globalThis.__atlasSetWindowScene(this, scene);\n }\n\n getTime() {\n return globalThis.__atlasGetWindowTime(this);\n }\n\n isKeyActive(key) {\n return globalThis.__atlasIsKeyActive(key);\n }\n\n isMouseButtonActive(button) {\n return globalThis.__atlasIsMouseButtonActive(button);\n }\n\n isMouseButtonPressed(button) {\n return globalThis.__atlasIsMouseButtonPressed(button);\n }\n\n getTextInput() {\n return globalThis.__atlasGetTextInput();\n }\n\n startTextInput() {\n globalThis.__atlasStartTextInput();\n }\n\n stopTextInput() {\n globalThis.__atlasStopTextInput();\n }\n\n isTextInputActive() {\n return globalThis.__atlasIsTextInputActive();\n }\n\n isControllerButtonPressed(controllerID, buttonIndex) {\n return globalThis.__atlasIsControllerButtonPressed(\n controllerID,\n buttonIndex,\n );\n }\n\n getControllerAxisValue(controllerID, axisIndex) {\n return globalThis.__atlasGetControllerAxisValue(controllerID, axisIndex);\n }\n\n getControllerAxisPairValue(controllerID, axisIndexX, axisIndexY) {\n return globalThis.__atlasGetControllerAxisPairValue(\n controllerID,\n axisIndexX,\n axisIndexY,\n );\n }\n\n releaseMouse() {\n globalThis.__atlasReleaseMouse();\n }\n\n captureMouse() {\n globalThis.__atlasCaptureMouse();\n }\n\n getCursorPosition() {\n return globalThis.__atlasGetMousePosition();\n }\n\n getCurrentScene() {\n return globalThis.__atlasGetScene();\n }\n\n getCamera() {\n return globalThis.__atlasGetCamera();\n }\n\n addRenderTarget(target) {\n const renderTarget =\n target ?? globalThis.__atlasCreateRenderTarget(1, 1024);\n return globalThis.__atlasAddWindowRenderTarget(this, renderTarget);\n }\n\n getSize() {\n return globalThis.__atlasGetWindowSize(this);\n }\n\n activateDebug() {\n globalThis.__atlasActivateWindowDebug(this);\n }\n\n desactivateDebug() {\n globalThis.__atlasDeactivateWindowDebug(this);\n }\n\n deactivateDebug() {\n this.desactivateDebug();\n }\n\n getDeltaTime() {\n return globalThis.__atlasGetWindowDeltaTime(this);\n }\n\n getFramesPerSecond() {\n return globalThis.__atlasGetWindowFramesPerSecond(this);\n }\n\n useAtlasTracer(enabled) {\n globalThis.__atlasUseWindowTracer(this, enabled);\n }\n\n setLogOutput(showLogs, showWarnings, showErrors) {\n globalThis.__atlasSetWindowLogOutput(\n this,\n showLogs,\n showWarnings,\n showErrors,\n );\n }\n\n getRenderScale() {\n return globalThis.__atlasGetWindowRenderScale(this);\n }\n\n useMetalUpscaling(ratio) {\n globalThis.__atlasUseWindowMetalUpscaling(this, ratio);\n }\n\n isMetalUpscalingEnabled() {\n return globalThis.__atlasIsWindowMetalUpscalingEnabled(this);\n }\n\n getMetalUpscalingRatio() {\n return globalThis.__atlasGetWindowMetalUpscalingRatio(this);\n }\n\n getSSAORenderScale() {\n return globalThis.__atlasGetWindowSSAORenderScale(this);\n }\n\n addInputAction(action) {\n return globalThis.__atlasRegisterInputAction(action);\n }\n\n resetInputActions() {\n globalThis.__atlasResetInputActions();\n }\n\n getInputAction(name) {\n return globalThis.__atlasGetWindowInputAction(this, name);\n }\n\n isActionTriggered(name) {\n return globalThis.__atlasIsActionTriggered(name);\n }\n\n isActionCurrentlyActive(name) {\n return globalThis.__atlasIsActionCurrentlyActive(name);\n }\n\n getActionAxisValue(name) {\n return globalThis.__atlasGetAxisActionValue(name);\n }\n}\n\nexport class Camera {\n constructor() {\n this.position = new Position3d(0, 0, 3);\n this.target = Position3d.zero();\n this.fov = 45;\n this.nearClip = 0.5;\n this.farClip = 1000;\n this.orthographicSize = 5;\n this.movementSpeed = 2;\n this.mouseSensitivity = 0.1;\n this.controllerLookSensitivity = 180;\n this.lookSmoothness = 0.15;\n this.useOrthographic = false;\n this.focusDepth = 20;\n this.focusRange = 10;\n return globalThis.__atlasGetCamera() ?? this;\n }\n\n move(position) {\n this.position.x += position.x;\n this.position.y += position.y;\n this.position.z += position.z;\n return globalThis.__atlasUpdateCamera(this);\n }\n\n setPosition(position) {\n this.position = position;\n return globalThis.__atlasUpdateCamera(this);\n }\n\n setPositionKeepingOrientation(position) {\n return globalThis.__atlasSetPositionKeepingOrientation(this, position);\n }\n\n lookAt(target, up = Position3d.up()) {\n return globalThis.__atlasLookAtCamera(this, target, up);\n }\n\n moveTo(position, speed) {\n return globalThis.__atlasMoveCameraTo(this, position, speed);\n }\n\n getDirection() {\n return globalThis.__atlasGetCameraDirection(this);\n }\n}\n", }; -static const AtlasPackedScriptSource ATLAS = {ATLAS_PARTS, 3}; +static const AtlasPackedScriptSource SCRIPTS_ATLAS = {SCRIPTS_ATLAS_PARTS, 3}; -static const char* const ATLAS_AUDIO_PARTS[] = { +static const char* const SCRIPTS_ATLAS_AUDIO_PARTS[] = { "import { Component } from \"atlas\";\nimport { Position3d } from \"atlas/units\";\n\nexport class AudioPlayer extends Component {\n constructor() {\n super();\n this.id = -1;\n this.source = null;\n this.volume = 1;\n this.loop = false;\n this.position = Position3d.zero();\n this.spatialAudio = false;\n globalThis.__atlasCreateAudioPlayer(this);\n }\n\n init() {\n globalThis.__atlasInitAudioPlayer(this.id);\n }\n\n play() {\n globalThis.__atlasPlayAudioPlayer(this.id);\n }\n\n pause() {\n globalThis.__atlasPauseAudioPlayer(this.id);\n }\n\n stop() {\n globalThis.__atlasStopAudioPlayer(this.id);\n }\n\n setVolume(volume) {\n this.volume = volume;\n globalThis.__atlasSetAudioPlayerVolume(this.id, volume);\n }\n\n setLoop(loop) {\n this.loop = loop;\n globalThis.__atlasSetAudioPlayerLoop(this.id, loop);\n }\n\n setSource(resource) {\n globalThis.__atlasSetAudioPlayerSource(this.id, resource);\n }\n\n update(dt) {\n globalThis.__atlasUpdateAudioPlayer(this.id, dt);\n }\n\n setPosition(position) {\n this.position = position;\n globalThis.__atlasSetAudioPlayerPosition(this.id, position);\n }\n\n useSpatialAudio(enabled) {\n this.spatialAudio = enabled;\n globalThis.__atlasUseSpatialAudio(this.id, enabled);\n }\n}\n", }; -static const AtlasPackedScriptSource ATLAS_AUDIO = {ATLAS_AUDIO_PARTS, 1}; +static const AtlasPackedScriptSource SCRIPTS_ATLAS_AUDIO = {SCRIPTS_ATLAS_AUDIO_PARTS, 1}; -static const char* const ATLAS_GRAPHICS_PARTS[] = { +static const char* const SCRIPTS_ATLAS_GRAPHICS_PARTS[] = { "import { Resource, ResourceType } from \"atlas\";\nimport { Color, Position3d, Size2d } from \"atlas/units\";\n\nexport const TextureType = Object.freeze({\n Color: 0,\n Specular: 1,\n Cubemap: 2,\n Depth: 3,\n DepthCube: 4,\n Normal: 5,\n Parallax: 6,\n SSAONoise: 7,\n SSAO: 8,\n Metallic: 9,\n Roughness: 10,\n AO: 11,\n Opacity: 12,\n HDR: 13,\n PBRPack: 14,\n});\n\nexport const RenderTargetType = Object.freeze({\n Scene: 0,\n Multisampled: 1,\n Shadow: 2,\n CubeShadow: 3,\n GBuffer: 4,\n SSAO: 5,\n SSAOBlur: 6,\n SSR: 7,\n});\n\nexport const RenderPassType = Object.freeze({\n Deferred: 0,\n Forward: 1,\n PathTracing: 2,\n});\n\nexport const Effects = Object.freeze({\n Inversion: { type: \"Inversion\" },\n Grayscale: { type: \"Grayscale\" },\n Sharpen: { type: \"Sharpen\" },\n Blur: { type: \"Blur\", magnitude: 16 },\n EdgeDetection: { type: \"EdgeDetection\" },\n ColorCorrection: {\n type: \"ColorCorrection\",\n exposure: 0,\n contrast: 1,\n saturation: 1,\n gamma: 1,\n temperature: 0,\n tint: 0,\n },\n MotionBlur: { type: \"MotionBlur\", size: 8, separation: 1 },\n ChromaticAberration: {\n type: \"ChromaticAberration\",\n red: 0.01,\n green: 0.006,\n blue: -0.006,\n direction: { x: 0, y: 0 },\n },\n Posterization: { type: \"Posterization\", levels: 5 },\n Pixelation: { type: \"Pixelation\", pixelSize: 8 },\n Dialation: { type: \"Dilation\", size: 8, separation: 1 },\n Dilation: { type: \"Dilation\", size: 8, separation: 1 },\n FilmGrain: { type: \"FilmGrain\", amount: 0.05 },\n});\n\nexport class Texture {\n constructor() {\n this.type = TextureType.Color;\n this.resource = new Resource(ResourceType.File, \"\", \"\");\n this.width = 0;\n this.height = 0;\n this.channels = 0;\n this.id = 0;\n this.borderColor = Color.black();\n }\n\n static fromResource(resource, type = TextureType.Color) {\n return globalThis.__atlasCreateTextureFromResource(resource, type);\n }\n\n static createEmpty(\n width,\n height,\n type = TextureType.Color,\n borderColor = new Color(0, 0, 0, 0),\n ) {\n return globalThis.__atlasCreateEmptyTexture(\n width,\n height,\n type,\n borderColor,\n );\n }\n\n static createColor(color, type = TextureType.Color, width = 1, height = 1) {\n return globalThis.__atlasCreateColorTexture(color, type, width, height);\n }\n\n createCheckerboard(width, height, checkSize, color1, color2) {\n return globalThis.__atlasCreateCheckerboardTexture(\n this,\n width,\n height,\n checkSize,\n color1,\n color2,\n );\n }\n\n createDoubleCheckerboard(\n width,\n height,\n checkSizeBig,\n checkSizeSmall,\n color1,\n color2,\n color3,\n ) {\n return globalThis.__atlasCreateDoubleCheckerboardTexture(\n this,\n width,\n height,\n checkSizeBig,\n checkSizeSmall,\n color1,\n color2,\n color3,\n );\n }\n\n displayToWindow() {\n return globalThis.__atlasDisplayTexture(this);\n }\n}\n\nexport class Cubemap {\n constructor(resources) {\n this.resources = resources;\n this.id = 0;\n return globalThis.__atlasCreateCubemap(resources);\n }\n\n getAverageColor() {\n return globalThis.__atlasGetCubemapAverageColor(this);\n }\n\n static fromResourceGroup(resourceGroup) {\n if (resourceGroup == null) {\n return null;\n }\n return globalThis.__atlasCreateCubemapFromGroup(resourceGroup.resources);\n }\n\n updateWithColors(colors) {\n return globalThis.__atlasUpdateCubemapWithColors(this, colors);\n }\n}\n\nexport class RenderTarget {\n constructor(type = RenderTargetType.Scene, resolution = 1024) {\n this.type = type;\n this.resolution = resolution;\n this.outTextures = [];\n this.depthTexture = null;\n return globalThis.__atlasCreateRenderTarget(type, resolution);\n }\n\n addEffect(effect) {\n return globalThis.__atlasAddRenderTargetEffect(this, effect);\n }\n\n addToPassQueue(type) {\n return globalThis.__atlasAddRenderTargetToPassQueue(this, type);\n }\n\n addToPass(type) {\n return this.addToPassQueue(type);\n }\n\n display() {\n return globalThis.__atlasDisplayRenderTarget(this);\n }\n}\n\nexport class Skybox {\n constructor(cubemap) {\n this.cubemap = cubemap;\n return globalThis.__atlasCreateSkybox(cubemap);\n }\n}\n\nexport class AmbientLight {\n constructor(color = Color.white(), intensity = 0.125) {\n this.color = color;\n this.intensity = intensity;\n }\n}\n\nexport class Light {\n constructor(\n position = Position3d.zero(),\n color = Color.white(),\n distance = 50,\n shineColor = Color.white(),\n intensity = 1,\n ) {\n this.position = position;\n this.color = color;\n this.shineColor = shineColor;\n this.intensity = intensity;\n this.distance = distance;\n return globalThis.__atlasCreatePointLight(this);\n }\n\n setColor(color) {\n this.color = color;\n return globalThis.__atlasUpdatePointLight(this);\n }\n\n createDebugObject() {\n return globalThis.__atlasCreatePointLightDebugObject(this);\n }\n\n castShadows(resolution = 2048) {\n return globalThis.__atlasCastPointLightShadows(this, resolution);\n }\n}\n\nexport class DirectionalLight {\n constructor(\n direction = Position3d.down(),\n color = Color.white(),\n shineColor = Color.white(),\n intensity = 1,\n ) {\n this.direction = direction;\n this.color = color;\n this.shineColor = shineColor;\n this.intensity = intensity;\n return globalThis.__atlasCreateDirectionalLight(this);\n }\n\n setColor(color) {\n this.color = color;\n return globalThis.__atlasUpdateDirectionalLight(this);\n }\n\n castShadows(resolution = 2048) {\n return globalThis.__atlasCastDirectionalLightShadows(\n this,\n resolution,\n );\n }\n}\n\nexport class SpotLight {\n constructor(\n position = Position3d.zero(),\n direction = Position3d.down(),\n color = Color.white(),\n cutOff = 35,\n outerCutOff = 40,\n shineColor = Color.white(),\n intensity = 1,\n range = 50,\n ) {\n this.position = position;\n this.direction = direction;\n this.color = color;\n this.shineColor = shineColor;\n this.range = range;\n this.cutOff = cutOff;\n this.outerCutOff = outerCutOff;\n this.intensity = intensity;\n return globalThis.__atlasCreateSpotLight(this);\n }\n\n setColor(color) {\n this.color = color;\n return globalThis.__atlasUpdateSpotLight(this);\n }\n\n createDebugObject() {\n return globalThis.__atlasCreateSpotLightDebugObject(this);\n }\n\n lookAt(target) {\n return globalThis.__atlasLookAtSpotLight(this, target);\n }\n\n castShadows(resolution = 2048) {\n return globalThis.__atlasCastSpotLightShadows(this, resolution);\n }\n}\n\nexport class AreaLight {\n constructor(\n position = Position3d.zero(),\n right = Position3d.right(),\n up = Position3d.up(),\n size = new Size2d(1, 1),\n color = Color.white(),\n shineColor = Color.white(),\n intensity = 1,\n range = 50,\n angle = 90,\n castsBothSides = false,\n rotation = Position3d.zero(),\n ) {\n this.position = position;\n this.right = right;\n this.up = up;\n this.size = size;\n this.color = color;\n this.shineColor = shineColor;\n this.intensity = intensity;\n this.range = range;\n this.angle = angle;\n this.castsBothSides = castsBothSides;\n this.rotation = rotation;\n return globalThis.__atlasCreateAreaLight(this);\n }\n\n getNormal() {\n return globalThis.__atlasGetA", "reaLightNormal(this);\n }\n\n setColor(color) {\n this.color = color;\n return globalThis.__atlasUpdateAreaLight(this);\n }\n\n setRotation(rotation) {\n this.rotation = rotation;\n return globalThis.__atlasSetAreaLightRotation(this, rotation);\n }\n\n rotate(delta) {\n return globalThis.__atlasRotateAreaLight(this, delta);\n }\n\n createDebugObject() {\n return globalThis.__atlasCreateAreaLightDebugObject(this);\n }\n\n castShadows(resolution = 2048) {\n return globalThis.__atlasCastAreaLightShadows(this, resolution);\n }\n}\n", }; -static const AtlasPackedScriptSource ATLAS_GRAPHICS = {ATLAS_GRAPHICS_PARTS, 2}; +static const AtlasPackedScriptSource SCRIPTS_ATLAS_GRAPHICS = {SCRIPTS_ATLAS_GRAPHICS_PARTS, 2}; -static const char* const ATLAS_INPUT_PARTS[] = { +static const char* const SCRIPTS_ATLAS_INPUT_PARTS[] = { "import { Position2d } from \"atlas/units\";\n\nconst constants = globalThis.__atlasGetInputConstants?.() ?? {};\n\nexport const Key = Object.freeze(constants.Key ?? {});\nexport const MouseButton = Object.freeze(constants.MouseButton ?? {});\nexport const TriggerType = Object.freeze(constants.TriggerType ?? {});\nexport const AxisTriggerType = Object.freeze(constants.AxisTriggerType ?? {});\n\nexport class Trigger {\n constructor() {\n this.type = TriggerType.Key ?? 0;\n this.mouseButton = undefined;\n this.key = undefined;\n this.controllerButton = undefined;\n }\n\n static fromKey(key) {\n const trigger = new Trigger();\n trigger.type = TriggerType.Key ?? 1;\n trigger.key = key;\n return trigger;\n }\n\n static fromMouseButton(mouseButton) {\n const trigger = new Trigger();\n trigger.type = TriggerType.MouseButton ?? 0;\n trigger.mouseButton = mouseButton;\n return trigger;\n }\n\n static fromControllerButton(controllerID, buttonIndex) {\n const trigger = new Trigger();\n trigger.type = TriggerType.ControllerButton ?? 2;\n trigger.controllerButton = { controllerID, buttonIndex };\n return trigger;\n }\n}\n\nexport class AxisTrigger {\n constructor() {\n this.type = AxisTriggerType.MouseAxis ?? 0;\n this.positiveX = Trigger.fromKey(Key.Unknown ?? 0);\n this.negativeX = Trigger.fromKey(Key.Unknown ?? 0);\n this.positiveY = Trigger.fromKey(Key.Unknown ?? 0);\n this.negativeY = Trigger.fromKey(Key.Unknown ?? 0);\n this.controllerId = undefined;\n this.controllerAxisSingle = false;\n this.axisIndex = undefined;\n this.axisIndexY = -1;\n this.isJoystick = false;\n }\n\n static fromMouse() {\n const trigger = new AxisTrigger();\n trigger.type = AxisTriggerType.MouseAxis ?? 0;\n return trigger;\n }\n\n static fromKeys(positiveX, negativeX, positiveY, negativeY) {\n const trigger = new AxisTrigger();\n trigger.type = AxisTriggerType.KeyCustom ?? 1;\n trigger.positiveX = Trigger.fromKey(positiveX);\n trigger.negativeX = Trigger.fromKey(negativeX);\n trigger.positiveY = Trigger.fromKey(positiveY);\n trigger.negativeY = Trigger.fromKey(negativeY);\n return trigger;\n }\n\n static fromControllerAxis(\n controllerId,\n axisIndex,\n single,\n axisIndexY = -1,\n ) {\n const trigger = new AxisTrigger();\n trigger.type = AxisTriggerType.ControllerAxis ?? 2;\n trigger.controllerId = controllerId;\n trigger.controllerAxisSingle = single;\n trigger.axisIndex = axisIndex;\n trigger.axisIndexY = axisIndexY;\n return trigger;\n }\n}\n\nexport class InputAction {\n constructor() {\n this.triggers = [];\n this.axisTriggers = [];\n this.name = \"\";\n this.isAxis = false;\n this.isAxisSingle = false;\n this.normalized = false;\n this.invertY = false;\n this.controllerDeadzone = 0.2;\n }\n\n static createButtonAction(name, triggers) {\n const action = new InputAction();\n action.name = name;\n action.triggers = triggers;\n return action;\n }\n\n static createAxisAction(name, axisTriggers) {\n const action = new InputAction();\n action.name = name;\n action.isAxis = true;\n action.axisTriggers = axisTriggers;\n return action;\n }\n\n static createSingleAxisAction(name, positiveTrigger, negativeTrigger) {\n const action = new InputAction();\n action.name = name;\n action.isAxis = true;\n action.isAxisSingle = true;\n action.axisTriggers = [\n {\n type: AxisTriggerType.KeyCustom ?? 1,\n positiveX: positiveTrigger,\n negativeX: negativeTrigger,\n positiveY: Trigger.fromKey(Key.Unknown ?? 0),\n negativeY: Trigger.fromKey(Key.Unknown ?? 0),\n controllerId: undefined,\n controllerAxisSingle: false,\n axisIndex: undefined,\n axisIndexY: -1,\n isJoystick: false,\n },\n ];\n return action;\n }\n}\n\nexport const Input = {\n addAction(action) {\n return globalThis.__atlasRegisterInputAction(action) ?? action;\n },\n\n resetActions() {\n globalThis.__atlasResetInputActions();\n },\n\n isKeyActive(key) {\n return globalThis.__atlasIsKeyActive(key);\n },\n\n isKeyPressed(key) {\n return globalThis.__atlasIsKeyPressed(key);\n },\n\n isMouseButtonActive(button) {\n return globalThis.__atlasIsMouseButtonActive(button);\n },\n\n isMouseButtonPressed(button) {\n return globalThis.__atlasIsMouseButtonPressed(button);\n },\n\n getTextInput() {\n return globalThis.__atlasGetTextInput();\n },\n\n startTextInput() {\n globalThis.__atlasStartTextInput();\n },\n\n stopTextInput() {\n globalThis.__atlasStopTextInput();\n },\n\n isTextInputActive() {\n return globalThis.__atlasIsTextInputActive();\n },\n\n isControllerButtonPressed(controllerID, buttonIndex) {\n return globalThis.__atlasIsControllerButtonPressed(\n controllerID,\n buttonIndex,\n );\n },\n\n getControllerAxisValue(controllerID, axisIndex) {\n return globalThis.__atlasGetControllerAxisValue(controllerID, axisIndex);\n },\n\n getControllerAxisPairValue(controllerID, axisIndexX, axisIndexY) {\n const value = globalThis.__atlasGetControllerAxisPairValue(\n controllerID,\n axisIndexX,\n axisIndexY,\n );\n return new Position2d(value?.x ?? 0, value?.y ?? 0);\n },\n\n captureMouse() {\n globalThis.__atlasCaptureMouse();\n },\n\n releaseMouse() {\n globalThis.__atlasReleaseMouse();\n },\n\n getMousePosition() {\n const position = globalThis.__atlasGetMousePosition();\n return new Position2d(position?.x ?? 0, position?.y ?? 0);\n },\n\n isActionTriggered(name) {\n return globalThis.__atlasIsActionTriggered(name);\n },\n\n isActionCurrentlyActive(name) {\n return globalThis.__atlasIsActionCurrentlyActive(name);\n },\n\n getAxisActionValue(name) {\n return globalThis.__atlasGetAxisActionValue(name);\n },\n};\n\nexport class Interactive {\n constructor() {\n globalThis.__atlasRegisterInteractive(this);\n }\n\n onKeyPress(key, dt) {}\n onKeyRelease(key, dt) {}\n onMouseMove(packet, dt) {}\n onMouseButtonPress(button, dt) {}\n onMouseScroll(packet, dt) {}\n onEachFrame(dt) {}\n}\n", }; -static const AtlasPackedScriptSource ATLAS_INPUT = {ATLAS_INPUT_PARTS, 1}; +static const AtlasPackedScriptSource SCRIPTS_ATLAS_INPUT = {SCRIPTS_ATLAS_INPUT_PARTS, 1}; -static const char* const ATLAS_LOG_PARTS[] = { +static const char* const SCRIPTS_ATLAS_LOG_PARTS[] = { "\nexport const Debug = {\n print(message) {\n globalThis.print(message);\n },\n warning(message) {\n globalThis.print(`[WARNING] ${message}`);\n },\n error(message) {\n globalThis.print(`[ERROR] ${message}`);\n }\n};", }; -static const AtlasPackedScriptSource ATLAS_LOG = {ATLAS_LOG_PARTS, 1}; +static const AtlasPackedScriptSource SCRIPTS_ATLAS_LOG = {SCRIPTS_ATLAS_LOG_PARTS, 1}; -static const char* const ATLAS_PARTICLES_PARTS[] = { +static const char* const SCRIPTS_ATLAS_PARTICLES_PARTS[] = { "import { GameObject } from \"atlas\";\nimport { Position3d } from \"atlas/units\";\n\nexport const ParticleEmissionType = Object.freeze({\n Fountain: 0,\n Ambient: 1,\n});\n\nexport class ParticleEmitter extends GameObject {\n constructor(maxParticles = 100) {\n super();\n this.settings = {\n minLifetime: 1,\n maxLifetime: 3,\n minSize: 0.02,\n maxSize: 0.01,\n fadeSpeed: 0.5,\n gravity: -9.81,\n spread: 1,\n speedVariation: 1,\n };\n this.position = Position3d.zero();\n this.emissionType = ParticleEmissionType.Fountain;\n this.direction = Position3d.up();\n this.spawnRadius = 0.1;\n this.spawnRate = 10;\n globalThis.__atlasCreateParticleEmitter(this, maxParticles);\n }\n\n attachTexture(texture) {\n return globalThis.__atlasAttachParticleEmitterTexture(this, texture);\n }\n\n setColor(color) {\n return globalThis.__atlasSetParticleEmitterColor(this, color);\n }\n\n enableTexture() {\n return globalThis.__atlasSetParticleEmitterUseTexture(this, true);\n }\n\n disableTexture() {\n return globalThis.__atlasSetParticleEmitterUseTexture(this, false);\n }\n\n setPosition(position) {\n this.position = position;\n return globalThis.__atlasSetParticleEmitterPosition(this, position);\n }\n\n move(position) {\n this.position.x += position.x;\n this.position.y += position.y;\n this.position.z += position.z;\n return globalThis.__atlasMoveParticleEmitter(this, position);\n }\n\n getPosition() {\n return globalThis.__atlasGetParticleEmitterPosition(this);\n }\n\n setEmissionType(type) {\n this.emissionType = type;\n return globalThis.__atlasSetParticleEmitterEmissionType(this, type);\n }\n\n setDirection(direction) {\n this.direction = direction;\n return globalThis.__atlasSetParticleEmitterDirection(this, direction);\n }\n\n setSpawnRadius(radius) {\n this.spawnRadius = radius;\n return globalThis.__atlasSetParticleEmitterSpawnRadius(this, radius);\n }\n\n setSpawnRate(rate) {\n this.spawnRate = rate;\n return globalThis.__atlasSetParticleEmitterSpawnRate(this, rate);\n }\n\n setParticleSettings(settings) {\n this.settings = settings;\n return globalThis.__atlasSetParticleEmitterSettings(this, settings);\n }\n\n emitOnce() {\n return globalThis.__atlasParticleEmitterEmitOnce(this);\n }\n\n emitContinuous() {\n return globalThis.__atlasParticleEmitterEmitContinuous(this);\n }\n\n startEmission() {\n return globalThis.__atlasParticleEmitterStartEmission(this);\n }\n\n stopEmission() {\n return globalThis.__atlasParticleEmitterStopEmission(this);\n }\n\n emitBurst(count) {\n return globalThis.__atlasParticleEmitterEmitBurst(this, count);\n }\n\n setRotation() {\n return undefined;\n }\n\n setScale() {\n return undefined;\n }\n\n lookAt() {\n return undefined;\n }\n\n rotate() {\n return undefined;\n }\n\n scaleBy() {\n return undefined;\n }\n\n show() {\n return globalThis.__atlasShowParticleEmitter(this.id);\n }\n\n hide() {\n return globalThis.__atlasHideParticleEmitter(this.id);\n }\n}\n", }; -static const AtlasPackedScriptSource ATLAS_PARTICLES = {ATLAS_PARTICLES_PARTS, 1}; +static const AtlasPackedScriptSource SCRIPTS_ATLAS_PARTICLES = {SCRIPTS_ATLAS_PARTICLES_PARTS, 1}; -static const char* const ATLAS_UNITS_PARTS[] = { +static const char* const SCRIPTS_ATLAS_UNITS_PARTS[] = { "export class Position3d {\n constructor(x = 0, y = 0, z = 0) {\n this.x = x;\n this.y = y;\n this.z = z;\n }\n\n static zero() {\n return new Position3d(0, 0, 0);\n }\n static down() {\n return new Position3d(0, -1, 0);\n }\n static up() {\n return new Position3d(0, 1, 0);\n }\n static forward() {\n return new Position3d(0, 0, 1);\n }\n static back() {\n return new Position3d(0, 0, -1);\n }\n static right() {\n return new Position3d(1, 0, 0);\n }\n static left() {\n return new Position3d(-1, 0, 0);\n }\n static invalid() {\n return new Position3d(Number.NaN, Number.NaN, Number.NaN);\n }\n\n add(other) {\n if (typeof other === \"number\") {\n return new Position3d(\n this.x + other,\n this.y + other,\n this.z + other,\n );\n }\n return new Position3d(\n this.x + other.x,\n this.y + other.y,\n this.z + other.z,\n );\n }\n\n subtract(other) {\n if (typeof other === \"number\") {\n return new Position3d(\n this.x - other,\n this.y - other,\n this.z - other,\n );\n }\n return new Position3d(\n this.x - other.x,\n this.y - other.y,\n this.z - other.z,\n );\n }\n\n multiply(other) {\n if (typeof other === \"number\") {\n return new Position3d(\n this.x * other,\n this.y * other,\n this.z * other,\n );\n }\n return new Position3d(\n this.x * other.x,\n this.y * other.y,\n this.z * other.z,\n );\n }\n\n divide(other) {\n if (typeof other === \"number\") {\n return new Position3d(\n this.x / other,\n this.y / other,\n this.z / other,\n );\n }\n return new Position3d(\n this.x / other.x,\n this.y / other.y,\n this.z / other.z,\n );\n }\n\n is(other) {\n return this.x === other.x && this.y === other.y && this.z === other.z;\n }\n\n normalized() {\n const length = Math.hypot(this.x, this.y, this.z);\n if (length === 0) {\n return Position3d.zero();\n }\n return new Position3d(\n this.x / length,\n this.y / length,\n this.z / length,\n );\n }\n\n toString() {\n return `Position3d(${this.x}, ${this.y}, ${this.z})`;\n }\n}\n\nexport class BoundingBox {\n constructor(min = Position3d.zero(), max = Position3d.zero()) {\n this.min = min;\n this.max = max;\n }\n\n toString() {\n return `BoundingBox(min: ${this.min.toString()}, max: ${this.max.toString()})`;\n }\n\n contains(point) {\n return (\n point.x >= this.min.x &&\n point.x <= this.max.x &&\n point.y >= this.min.y &&\n point.y <= this.max.y &&\n point.z >= this.min.z &&\n point.z <= this.max.z\n );\n }\n\n intersects(other) {\n return !(\n this.max.x < other.min.x ||\n this.min.x > other.max.x ||\n this.max.y < other.min.y ||\n this.min.y > other.max.y ||\n this.max.z < other.min.z ||\n this.min.z > other.max.z\n );\n }\n}\n\nexport class Color {\n constructor(r = 0, g = 0, b = 0, a = 1) {\n this.r = r;\n this.g = g;\n this.b = b;\n this.a = a;\n }\n\n add(other) {\n if (typeof other === \"number\") {\n return new Color(\n this.r + other,\n this.g + other,\n this.b + other,\n this.a + other,\n );\n }\n return new Color(\n this.r + other.r,\n this.g + other.g,\n this.b + other.b,\n this.a + other.a,\n );\n }\n\n subtract(other) {\n if (typeof other === \"number\") {\n return new Color(\n this.r - other,\n this.g - other,\n this.b - other,\n this.a - other,\n );\n }\n\n return new Color(\n this.r - other.r,\n this.g - other.g,\n this.b - other.b,\n this.a - other.a,\n );\n }\n\n multiply(other) {\n if (typeof other === \"number\") {\n return new Color(\n this.r * other,\n this.g * other,\n this.b * other,\n this.a * other,\n );\n }\n return new Color(\n this.r * other.r,\n this.g * other.g,\n this.b * other.b,\n this.a * other.a,\n );\n }\n\n divide(other) {\n if (typeof other === \"number\") {\n return new Color(\n this.r / other,\n this.g / other,\n this.b / other,\n this.a / other,\n );\n }\n return new Color(\n this.r / other.r,\n this.g / other.g,\n this.b / other.b,\n this.a / other.a,\n );\n }\n\n static white() {\n return new Color(1, 1, 1, 1);\n }\n\n static black() {\n return new Color(0, 0, 0, 1);\n }\n\n static red() {\n return new Color(1, 0, 0, 1);\n }\n\n static green() {\n return new Color(0, 1, 0, 1);\n }\n\n static blue() {\n return new Color(0, 0, 1, 1);\n }\n\n static transparent() {\n return new Color(0, 0, 0, 0);\n }\n\n static yellow() {\n return new Color(1, 1, 0, 1);\n }\n\n static cyan() {\n return new Color(0, 1, 1, 1);\n }\n\n static magenta() {\n return new Color(1, 0, 1, 1);\n }\n\n static gray() {\n return new Color(0.5, 0.5, 0.5, 1);\n }\n\n static orange() {\n return new Color(1, 0.5, 0, 1);\n }\n\n static purple() {\n return new Color(0.5, 0, 0.5, 1);\n }\n\n static brown() {\n return new Color(0.6, 0.3, 0, 1);\n }\n\n static pink() {\n return new Color(1, 0.75, 0.8, 1);\n }\n\n static lime() {\n return new Color(0, 1, 0, 1);\n }\n\n static navy() {\n return new Color(0, 0, 0.5, 1);\n }\n\n static teal() {\n return new Color(0, 0.5, 0.5, 1);\n }\n\n static olive() {\n return new Color(0.5, 0.5, 0, 1);\n }\n\n static maroon() {\n return new Color(0.5, 0, 0, 1);\n }\n\n static fromHex(hex) {\n if (hex.startsWith(\"#\")) {\n hex = hex.slice(1);\n }\n if (hex.length === 3) {\n hex = hex\n .split(\"\")\n .map((c) => c + c)\n .join(\"\");\n }\n if (hex.length !== 6) {\n throw new Error(\"Invalid hex color\");\n }\n const r = parseInt(hex.slice(0, 2), 16) / 255;\n const g = parseInt(hex.slice(2, 4), 16) / 255;\n const b = parseInt(hex.slice(4, 6), 16) / 255;\n return new Color(r, g, b, 1);\n }\n\n static mix(color1, color2, t) {\n return new Color(\n color1.r * (1 - t) + color2.r * t,\n color1.g * (1 - t) + color2.g * t,\n color1.b * (1 - t) + color2.b * t,\n color1.a * (1 - t) + color2.a * t,\n );\n }\n}\n\nexport class Position2d {\n constructor(x = 0, y = 0) {\n this.x = x;\n this.y = y;\n }\n\n static zero() {\n return new Position2d(0, 0);\n }\n\n static up() {\n return new Position2d(0, 1);\n }\n\n static down() {\n return new Position2d(0, -1);\n }\n\n static left() {\n return new Position2d(-1, 0);\n }\n\n static right() {\n return new Position2d(1, 0);\n }\n\n static invalid() {\n return new Position2d(NaN, NaN);\n }\n\n add(other) {\n if (typeof other === \"number\") {\n return new Position2d(this.x + other, this.y + other);\n }\n return new Position2d(this.x + other.x, this.y + other.y);\n }\n\n subtract(other) {\n if (typeof other === \"number\") {\n return new Position2d(this.x - other, this.y - other);\n }\n ", " return new Position2d(this.x - other.x, this.y - other.y);\n }\n\n multiply(other) {\n if (typeof other === \"number\") {\n return new Position2d(this.x * other, this.y * other);\n }\n return new Position2d(this.x * other.x, this.y * other.y);\n }\n\n divide(other) {\n if (typeof other === \"number\") {\n return new Position2d(this.x / other, this.y / other);\n }\n return new Position2d(this.x / other.x, this.y / other.y);\n }\n\n is(other) {\n return this.x === other.x && this.y === other.y;\n }\n}\n\nexport class Radians {\n constructor(value = 0) {\n this.value = value;\n }\n\n add(other) {\n return new Radians(this.value + other.value);\n }\n\n subtract(other) {\n return new Radians(this.value - other.value);\n }\n\n multiply(other) {\n if (typeof other === \"number\") {\n return new Radians(this.value * other);\n }\n return new Radians(this.value * other.value);\n }\n\n divide(other) {\n if (typeof other === \"number\") {\n return new Radians(this.value / other);\n }\n return new Radians(this.value / other.value);\n }\n\n toNumber() {\n return this.value;\n }\n\n static fromDegrees(degrees) {\n return new Radians((degrees * Math.PI) / 180);\n }\n\n toDegrees() {\n return (this.value * 180) / Math.PI;\n }\n}\n\nexport class Size2d {\n constructor(width = 0, height = 0) {\n this.width = width;\n this.height = height;\n }\n\n toString() {\n return `Size2d(${this.width}, ${this.height})`;\n }\n\n add(other) {\n if (typeof other === \"number\") {\n return new Size2d(this.width + other, this.height + other);\n }\n return new Size2d(this.width + other.width, this.height + other.height);\n }\n\n subtract(other) {\n if (typeof other === \"number\") {\n return new Size2d(this.width - other, this.height - other);\n }\n return new Size2d(this.width - other.width, this.height - other.height);\n }\n\n multiply(other) {\n if (typeof other === \"number\") {\n return new Size2d(this.width * other, this.height * other);\n }\n return new Size2d(this.width * other.width, this.height * other.height);\n }\n\n divide(other) {\n if (typeof other === \"number\") {\n return new Size2d(this.width / other, this.height / other);\n }\n return new Size2d(this.width / other.width, this.height / other.height);\n }\n\n is(other) {\n return this.width === other.width && this.height === other.height;\n }\n\n static zero() {\n return new Size2d(0, 0);\n }\n}\n", }; -static const AtlasPackedScriptSource ATLAS_UNITS = {ATLAS_UNITS_PARTS, 2}; +static const AtlasPackedScriptSource SCRIPTS_ATLAS_UNITS = {SCRIPTS_ATLAS_UNITS_PARTS, 2}; -static const char* const AURORA_PARTS[] = { +static const char* const SCRIPTS_AURORA_PARTS[] = { "import { GameObject, Resource, ResourceType } from \"atlas\";\nimport { Texture } from \"atlas/graphics\";\nimport { Color, Position3d } from \"atlas/units\";\n\nfunction nextSeed() {\n const seed = Math.floor(Date.now() % 2147483647);\n Noise.seed = seed;\n Noise.initializedSeed = true;\n return seed;\n}\n\nfunction activeSeed() {\n if (!Noise.initializedSeed) {\n return nextSeed();\n }\n return Noise.seed;\n}\n\nexport class PerlinNoise {\n constructor(seed = 0) {\n this.seed = seed;\n }\n\n noise(x, y) {\n return globalThis.__auroraPerlinNoise(this.seed, x, y);\n }\n}\n\nexport class SimplexNoise {\n static noise(xin, yin) {\n return globalThis.__auroraSimplexNoise(xin, yin);\n }\n}\n\nexport class WorleyNoise {\n constructor(numPoints, seed = 0) {\n this.numPoints = numPoints;\n this.seed = seed;\n }\n\n noise(x, y) {\n return globalThis.__auroraWorleyNoise(this.numPoints, this.seed, x, y);\n }\n}\n\nexport class FractalNoise {\n constructor(o, p) {\n this.octaves = o;\n this.persistence = p;\n }\n\n noise(x, y) {\n return globalThis.__auroraFractalNoise(\n this.octaves,\n this.persistence,\n x,\n y,\n );\n }\n}\n\nexport class Noise {\n static seed = 0;\n static initializedSeed = false;\n\n static perlin(x, y) {\n return new PerlinNoise(activeSeed()).noise(x, y);\n }\n\n static simplex(x, y) {\n activeSeed();\n return SimplexNoise.noise(x, y);\n }\n\n static worley(x, y) {\n return new WorleyNoise(10, activeSeed()).noise(x, y);\n }\n\n static fractal(x, y, octaves, persistence) {\n return new FractalNoise(octaves, persistence).noise(x, y);\n }\n}\n\nexport class Biome {\n constructor(\n name = \"\",\n texture = null,\n color = Color.white(),\n useTexture = false,\n ) {\n this.name = name;\n this.texture = texture;\n this.color = color;\n this.useTexture = useTexture;\n this.minHeight = -1;\n this.maxHeight = -1;\n this.minMoisture = -1;\n this.maxMoisture = -1;\n this.minTemperature = -1;\n this.maxTemperature = -1;\n this.condition = () => {};\n }\n\n attachTexture(texture) {\n this.texture = texture;\n this.useTexture = true;\n }\n}\n\nexport class Terrain extends GameObject {\n constructor(source = null, createdWithMap = false) {\n super();\n this.heightmap =\n createdWithMap && source instanceof Resource\n ? source\n : new Resource(ResourceType.Texture, \"\", \"\");\n this.moistureTexture = null;\n this.temperatureTexture = null;\n this.generator = createdWithMap ? null : source;\n this.createdWithMap = createdWithMap;\n this.width = 512;\n this.height = 512;\n this.length = 512;\n this.biomes = [];\n this.maxPeak = 48;\n this.seaLevel = 16;\n this.resolution = 20;\n return globalThis.__auroraCreateTerrain(this) ?? this;\n }\n\n _sync() {\n this.height = this.length ?? this.height;\n this.length = this.height;\n return globalThis.__auroraUpdateTerrain(this);\n }\n\n attachTexture(texture) {\n if (!(texture instanceof Texture)) {\n return;\n }\n if (this.biomes.length === 0) {\n this.biomes.push(new Biome(\"Default\", texture, Color.white(), true));\n } else {\n this.biomes[0].attachTexture(texture);\n }\n this._sync();\n }\n\n setPosition(position) {\n this.position = position;\n this._sync();\n }\n\n move(position) {\n this.position.x += position.x;\n this.position.y += position.y;\n this.position.z += position.z;\n this._sync();\n }\n\n setRotation(rotation) {\n this.rotation = rotation;\n this._sync();\n }\n\n lookAt(target, up = Position3d.up()) {\n return globalThis.__atlasLookAtObject(this, target, up);\n }\n\n rotate(rotation) {\n this.rotation.x += rotation.x;\n this.rotation.y += rotation.y;\n this.rotation.z += rotation.z;\n this._sync();\n }\n\n setScale(scale) {\n this.scale = scale;\n this._sync();\n }\n\n scaleBy(scale) {\n this.scale.x *= scale.x;\n this.scale.y *= scale.y;\n this.scale.z *= scale.z;\n this._sync();\n }\n\n show() {\n globalThis.__atlasShowObject(this.id);\n }\n\n hide() {\n globalThis.__atlasHideObject(this.id);\n }\n\n addBiome(biome) {\n if (biome == null) {\n return;\n }\n if (typeof biome.condition === \"function\") {\n biome.condition(biome);\n }\n this.biomes.push(biome);\n this._sync();\n }\n\n static fromGenerator(generator) {\n return new Terrain(generator, false);\n }\n\n static fromHeightmap(heightmap) {\n const resource =\n heightmap instanceof Resource\n ? heightmap\n : Resource.fromAssetPath(\n heightmap,\n ResourceType.Texture,\n \"terrain-heightmap\",\n );\n return new Terrain(resource, true);\n }\n}\n\nexport class TerrainGenerator {\n constructor() {\n this.algorithm = \"generator\";\n this.type = \"generator\";\n }\n\n generateHeight(x, y) {\n return 0;\n }\n\n applyTo(terrain) {\n if (!(terrain instanceof Terrain)) {\n return;\n }\n terrain.generator = this;\n terrain.createdWithMap = false;\n terrain._sync();\n }\n}\n\nexport class HillGenerator extends TerrainGenerator {\n constructor(scale, amplitude) {\n super();\n this.algorithm = \"hill\";\n this.type = \"hill\";\n this.scale = scale;\n this.amplitude = amplitude;\n }\n\n generateHeight(x, y) {\n const noise = Noise.perlin(x / this.scale, y / this.scale);\n return ((noise + 1) * 0.5 * this.amplitude) / 10;\n }\n}\n\nexport class MountainGenerator extends TerrainGenerator {\n constructor(scale, amplitude, octaves, persistance) {\n super();\n this.algorithm = \"mountain\";\n this.type = \"mountain\";\n this.scale = scale;\n this.amplitude = amplitude;\n this.octaves = octaves;\n this.persistance = persistance;\n this.persistence = persistance;\n }\n\n generateHeight(x, y) {\n const noise = Noise.fractal(\n x * this.scale,\n y * this.scale,\n this.octaves,\n this.persistence,\n );\n return noise * this.amplitude;\n }\n}\n\nexport class PlainGenerator extends TerrainGenerator {\n constructor(scale, amplitude) {\n super();\n this.algorithm = \"plain\";\n this.type = \"plain\";\n this.scale = scale;\n this.amplitude = amplitude;\n }\n\n generateHeight(x, y) {\n const noise = Noise.perlin(x * this.scale, y * this.scale);\n return ((noise + 1) * 0.5 * this.amplitude) / 2;\n }\n}\n\nexport class IslandGenerator extends TerrainGenerator {\n constructor(numFeatures, scale) {\n super();\n this.algorithm = \"island\";\n this.type = \"island\";\n this.numFeatures = numFeatures;\n this.scale = scale;\n }\n\n generateHeight(x, y) {\n const noise = new WorleyNoise(this.numFeatures, activeSeed()).noise(\n x * this.scale,\n y * this.scale,\n );\n return Math.max(0, Math.min(noise, 1));\n }\n}\n\nexport class CompoundGenerator extends TerrainGenerator {\n constructor() {\n super();\n this.algorithm = \"compound\";\n this.type = \"compound\";\n this.generators = [];\n }\n\n addGenerator(generator) {\n if (generator != null) {\n this.generators.push(generator);\n }\n }\n\n generateHeight(x, y) {\n let height = 0;\n for (const generator of this.generators) {\n height += generator.generateHeight(x, y);\n }\n return height;\n }\n}\n", }; -static const AtlasPackedScriptSource AURORA = {AURORA_PARTS, 1}; +static const AtlasPackedScriptSource SCRIPTS_AURORA = {SCRIPTS_AURORA_PARTS, 1}; -static const char* const BEZEL_PARTS[] = { +static const char* const SCRIPTS_BEZEL_PARTS[] = { "import { Component } from \"atlas\";\nimport { Position3d } from \"atlas/units\";\n\nexport const QueryOperation = Object.freeze({\n RaycastAll: 0,\n Raycast: 1,\n RasycastWorld: 2,\n RaycastWorld: 2,\n RaycastWorldAll: 3,\n RaycastTagged: 4,\n RaycastTaggedAll: 5,\n Movement: 6,\n Overlap: 7,\n MovementAll: 8,\n});\n\nexport const SpringMode = Object.freeze({\n FrequencyAndDamping: 0,\n StiffnessAndDamping: 1,\n});\n\nexport const Space = Object.freeze({\n Local: 0,\n Global: 1,\n});\n\nexport const VehicleTransmissionMode = Object.freeze({\n Auto: 0,\n Manual: 1,\n});\n\nfunction emptyRaycastResult() {\n return {\n hits: [],\n hit: null,\n closestDistance: -1,\n };\n}\n\nfunction emptyOverlapResult() {\n return {\n hits: [],\n hitAny: false,\n };\n}\n\nfunction emptySweepResult(endPosition = Position3d.zero()) {\n return {\n hits: [],\n closest: null,\n hitAny: false,\n endPosition,\n };\n}\n\nfunction defaultWheelSettings() {\n return {\n position: Position3d.zero(),\n enableSuspensionForcePoint: false,\n suspensionForcePoint: Position3d.zero(),\n suspensionDirection: Position3d.down(),\n steeringAxis: Position3d.up(),\n wheelUp: Position3d.up(),\n wheelForward: Position3d.forward(),\n suspensionMinLength: 0.3,\n suspensionMaxLength: 0.5,\n suspensionPreloadLength: 0,\n suspensionFrequencyHz: 1.5,\n suspensionDampingRatio: 0.5,\n radius: 0.3,\n width: 0.1,\n inertia: 0.9,\n angularDamping: 0.2,\n maxSteerAngleDegrees: 70,\n maxBrakeTorque: 1500,\n maxHandBrakeTorque: 4000,\n };\n}\n\nfunction defaultVehicleSettings() {\n return {\n up: Position3d.up(),\n forward: Position3d.forward(),\n maxPitchRollAngleDeg: 180,\n wheels: [defaultWheelSettings(), defaultWheelSettings()],\n controller: {\n engine: {\n maxTorque: 500,\n minRPM: 1000,\n maxRPM: 6000,\n inertia: 0.5,\n angularDamping: 0.2,\n },\n transmission: {\n mode: VehicleTransmissionMode.Auto,\n gearRatios: [2.66, 1.78, 1.3, 1.0, 0.74],\n reverseGearRatios: [-2.9],\n switchTime: 0.5,\n clutchReleaseTime: 0.3,\n switchLatency: 0.5,\n shiftUpRPM: 4000,\n shiftDownRPM: 2000,\n clutchStrength: 10,\n },\n differentials: [],\n differentialLimitedSlipRatio: 1.4,\n },\n maxSlopAngleDeg: 80,\n };\n}\n\nexport class Joint extends Component {\n constructor() {\n super();\n this.parent = {};\n this.child = {};\n this.space = Space.Global;\n this.anchor = Position3d.invalid();\n this.breakForce = 0;\n this.breakTorque = 0;\n }\n\n init() {}\n update(deltaTime) {}\n beforePhysics() {}\n breakJoint() {}\n}\n\nexport class FixedJoint extends Joint {\n constructor() {\n super();\n globalThis.__atlasCreateFixedJoint(this);\n }\n\n beforePhysics() {\n return globalThis.__atlasFixedJointBeforePhysics(this);\n }\n\n breakJoint() {\n return globalThis.__atlasFixedJointBreak(this);\n }\n}\n\nexport class HingeJoint extends Joint {\n constructor() {\n super();\n this.axis1 = Position3d.up();\n this.axis2 = Position3d.up();\n this.angleLimits = {\n enabled: false,\n minAngle: 0,\n maxAngle: 0,\n };\n this.motor = {\n enabled: false,\n maxForce: 0,\n maxTorque: 0,\n };\n globalThis.__atlasCreateHingeJoint(this);\n }\n\n beforePhysics() {\n return globalThis.__atlasHingeJointBeforePhysics(this);\n }\n\n breakJoint() {\n return globalThis.__atlasHingeJointBreak(this);\n }\n}\n\nexport class SpringJoint extends Joint {\n constructor() {\n super();\n this.anchorB = Position3d.invalid();\n this.restLength = 1;\n this.useLimits = false;\n this.minLength = 0;\n this.maxLength = 0;\n this.spring = {\n enabled: false,\n mode: SpringMode.FrequencyAndDamping,\n frequency: 0,\n dampingRatio: 0,\n stiffness: 0,\n damping: 0,\n };\n globalThis.__atlasCreateSpringJoint(this);\n }\n\n beforePhysics() {\n return globalThis.__atlasSpringJointBeforePhysics(this);\n }\n\n breakJoint() {\n return globalThis.__atlasSpringJointBreak(this);\n }\n}\n\nexport class Vehicle extends Component {\n constructor() {\n super();\n this.settings = defaultVehicleSettings();\n this.forward = 0;\n this.right = 0;\n this.brake = 0;\n this.handBrake = 0;\n globalThis.__atlasCreateVehicle(this);\n }\n\n atAttach() {}\n\n beforePhysics() {\n return globalThis.__atlasVehicleBeforePhysics(this);\n }\n\n requestRecreate() {\n return globalThis.__atlasVehicleRequestRecreate(this);\n }\n\n init() {}\n update(deltaTime) {}\n}\n\nexport class Rigidbody extends Component {\n constructor() {\n super();\n this.sendSignal = \"\";\n this.isSensor = false;\n globalThis.__atlasCreateRigidbody(this);\n }\n\n init() {\n return globalThis.__atlasInitRigidbody(this);\n }\n\n beforePhysics() {\n return globalThis.__atlasBeforePhysicsRigidbody(this);\n }\n\n update(deltaTime) {\n return globalThis.__atlasUpdateRigidbody(this, deltaTime);\n }\n\n clone() {\n return globalThis.__atlasCloneRigidbody(this);\n }\n\n addCollider(collider) {\n return globalThis.__atlasRigidbodyAddCollider(this, collider);\n }\n\n setFriction(friction) {\n return globalThis.__atlasRigidbodySetFriction(this, friction);\n }\n\n applyForce(force) {\n return globalThis.__atlasRigidbodyApplyForce(this, force);\n }\n\n applyForceAtPoint(force, point) {\n return globalThis.__atlasRigidbodyApplyForceAtPoint(this, force, point);\n }\n\n applyImpulse(impulse) {\n return globalThis.__atlasRigidbodyApplyImpulse(this, impulse);\n }\n\n setLinearVelocity(velocity) {\n return globalThis.__atlasRigidbodySetLinearVelocity(this, velocity);\n }\n\n addLinearVelocity(velocity) {\n return globalThis.__atlasRigidbodyAddLinearVelocity(this, velocity);\n }\n\n setAngularVelocity(velocity) {\n return globalThis.__atlasRigidbodySetAngularVelocity(this, velocity);\n }\n\n addAngularVelocity(velocity) {\n return globalThis.__atlasRigidbodyAddAngularVelocity(this, velocity);\n }\n\n setMaxLinearVelocity(velocity) {\n return globalThis.__atlasRigidbodySetMaxLinearVelocity(this, velocity);\n }\n\n setMaxAngularVelocity(velocity) {\n return globalThis.__atlasRigidbodySetMaxAngularVelocity(this, velocity);\n }\n\n getLinearVelocity() {\n return globalThis.__atlasRigidbodyGetLinearVelocity(this);\n }\n\n getAngularVelocity() {\n return globalThis.__atlasRigidbodyGetAngularVelocity(this);\n }\n\n getVelocity() {\n return globalThis.__atlasRigidbodyGetVelocity(this);\n }\n\n raycast(direction, maxDistance) {\n return (\n globalThis.__atlasRigidbodyRaycast(this, direction, maxDistance)\n ?.raycastResult ?? emptyRaycastResult()\n );\n }\n\n raycastAll(direction, maxDistance) {\n return (\n globalThis.__atlasRigidbodyRaycastAll(this, direction, maxDistance)\n ?.raycastResult ?? emptyRaycastResult()\n );\n }\n\n raycastWorld(origin, direction, maxDistance) {\n return (\n globalThis.__atlasRigidbodyRaycastWorld(\n this,\n origin,\n direction,\n maxDistance,\n )?.raycastResult ?? emptyRaycastResult()\n );\n }\n\n raycastWorldAll(origin, direction, maxDistance) {\n return (\n globalThis.__atlasRigidbodyRaycastWorldAll(\n this,\n origin,\n direction,\n ", " maxDistance,\n )?.raycastResult ?? emptyRaycastResult()\n );\n }\n\n raycastTagged(tags, direction, maxDistance) {\n return (\n globalThis.__atlasRigidbodyRaycastTagged(\n this,\n tags,\n direction,\n maxDistance,\n )?.raycastResult ?? emptyRaycastResult()\n );\n }\n\n raycastTaggedAll(tags, direction, maxDistance) {\n return (\n globalThis.__atlasRigidbodyRaycastTaggedAll(\n this,\n tags,\n direction,\n maxDistance,\n )?.raycastResult ?? emptyRaycastResult()\n );\n }\n\n overlap() {\n return (\n globalThis.__atlasRigidbodyOverlap(this)?.overlapResult ??\n emptyOverlapResult()\n );\n }\n\n overlapWithCollider(collider) {\n return (\n globalThis.__atlasRigidbodyOverlapWithCollider(this, collider)\n ?.overlapResult ?? emptyOverlapResult()\n );\n }\n\n overlapWithColliderWorld(collider, position) {\n return (\n globalThis.__atlasRigidbodyOverlapWithColliderWorld(\n this,\n collider,\n position,\n )?.overlapResult ?? emptyOverlapResult()\n );\n }\n\n predictMovementWithCollider(endPosition, collider) {\n return (\n globalThis.__atlasRigidbodyPredictMovementWithCollider(\n this,\n endPosition,\n collider,\n )?.sweepResult ?? emptySweepResult(endPosition)\n );\n }\n\n predictMovement(endPosition) {\n return (\n globalThis.__atlasRigidbodyPredictMovement(this, endPosition)\n ?.sweepResult ?? emptySweepResult(endPosition)\n );\n }\n\n predictMovementWithColliderWorld(startPosition, endPosition, collider) {\n return (\n globalThis.__atlasRigidbodyPredictMovementWithColliderWorld(\n this,\n startPosition,\n endPosition,\n collider,\n )?.sweepResult ?? emptySweepResult(endPosition)\n );\n }\n\n predictMovementWorld(startPosition, endPosition) {\n return (\n globalThis.__atlasRigidbodyPredictMovementWorld(\n this,\n startPosition,\n endPosition,\n )?.sweepResult ?? emptySweepResult(endPosition)\n );\n }\n\n predictMovementWithColliderAll(endPosition, collider) {\n return (\n globalThis.__atlasRigidbodyPredictMovementWithColliderAll(\n this,\n endPosition,\n collider,\n )?.sweepResult ?? emptySweepResult(endPosition)\n );\n }\n\n predictMovementAll(endPosition) {\n return (\n globalThis.__atlasRigidbodyPredictMovementAll(this, endPosition)\n ?.sweepResult ?? emptySweepResult(endPosition)\n );\n }\n\n predictMovementWithColliderAllWorld(startPosition, endPosition, collider) {\n return (\n globalThis.__atlasRigidbodyPredictMovementWithColliderAllWorld(\n this,\n startPosition,\n endPosition,\n collider,\n )?.sweepResult ?? emptySweepResult(endPosition)\n );\n }\n\n predictMovementAllWorld(startPosition, endPosition) {\n return (\n globalThis.__atlasRigidbodyPredictMovementAllWorld(\n this,\n startPosition,\n endPosition,\n )?.sweepResult ?? emptySweepResult(endPosition)\n );\n }\n\n hasTag(tag) {\n return globalThis.__atlasRigidbodyHasTag(this, tag);\n }\n\n addTag(tag) {\n return globalThis.__atlasRigidbodyAddTag(this, tag);\n }\n\n removeTag(tag) {\n return globalThis.__atlasRigidbodyRemoveTag(this, tag);\n }\n\n setDamping(linearDamping, angularDamping) {\n return globalThis.__atlasRigidbodySetDamping(\n this,\n linearDamping,\n angularDamping,\n );\n }\n\n setMass(mass) {\n return globalThis.__atlasRigidbodySetMass(this, mass);\n }\n\n setRestituition(restitution) {\n return globalThis.__atlasRigidbodySetRestitution(this, restitution);\n }\n\n setRestitution(restitution) {\n return this.setRestituition(restitution);\n }\n\n setMotionType(motionType) {\n return globalThis.__atlasRigidbodySetMotionType(this, motionType);\n }\n}\n\nexport class Sensor extends Rigidbody {\n constructor() {\n super();\n this.isSensor = true;\n }\n\n setSignal(signal) {\n this.sendSignal = signal;\n return globalThis.__atlasSensorSetSignal(this, signal);\n }\n}\n", }; -static const AtlasPackedScriptSource BEZEL = {BEZEL_PARTS, 2}; +static const AtlasPackedScriptSource SCRIPTS_BEZEL = {SCRIPTS_BEZEL_PARTS, 2}; -static const char* const FINEWAVE_PARTS[] = { +static const char* const SCRIPTS_FINEWAVE_PARTS[] = { "import { Resource, ResourceType } from \"atlas\";\n\nexport class AudioEngine {\n constructor() {\n this.deviceName = \"\";\n return globalThis.__finewaveGetAudioEngine() ?? this;\n }\n\n setListenerPosition(position) {\n globalThis.__finewaveAudioEngineSetListenerPosition(this, position);\n }\n\n setListenerOrientation(forward, up) {\n globalThis.__finewaveAudioEngineSetListenerOrientation(\n this,\n forward,\n up,\n );\n }\n\n setListenerVelocity(velocity) {\n globalThis.__finewaveAudioEngineSetListenerVelocity(this, velocity);\n }\n\n setMasterVolume(volume) {\n globalThis.__finewaveAudioEngineSetMasterVolume(this, volume);\n }\n}\n\nexport class AudioData {\n constructor() {\n this.isMono = false;\n this.resource = new Resource(ResourceType.Audio, \"\", \"\");\n }\n\n static fromResource(resource) {\n return globalThis.__finewaveCreateAudioData(resource);\n }\n}\n\nexport class AudioSource {\n constructor() {\n return globalThis.__finewaveCreateAudioSource() ?? this;\n }\n\n setData(data) {\n globalThis.__finewaveAudioSourceSetData(this, data);\n }\n\n fromFile(resource) {\n globalThis.__finewaveAudioSourceFromFile(this, resource);\n }\n\n play() {\n globalThis.__finewaveAudioSourcePlay(this);\n }\n\n pause() {\n globalThis.__finewaveAudioSourcePause(this);\n }\n\n stop() {\n globalThis.__finewaveAudioSourceStop(this);\n }\n\n setLoop(loop) {\n globalThis.__finewaveAudioSourceSetLoop(this, loop);\n }\n\n setVolume(volume) {\n globalThis.__finewaveAudioSourceSetVolume(this, volume);\n }\n\n setPitch(pitch) {\n globalThis.__finewaveAudioSourceSetPitch(this, pitch);\n }\n\n setPosition(position) {\n globalThis.__finewaveAudioSourceSetPosition(this, position);\n }\n\n setVelocity(velocity) {\n globalThis.__finewaveAudioSourceSetVelocity(this, velocity);\n }\n\n isPlaying() {\n return globalThis.__finewaveAudioSourceIsPlaying(this);\n }\n\n playFrom(position) {\n globalThis.__finewaveAudioSourcePlayFrom(this, position);\n }\n\n disableSpatialization() {\n globalThis.__finewaveAudioSourceDisableSpatialization(this);\n }\n\n applyEffect(effect) {\n globalThis.__finewaveAudioSourceApplyEffect(this, effect);\n }\n\n getPosition() {\n return globalThis.__finewaveAudioSourceGetPosition(this);\n }\n\n getListenerPosition() {\n return globalThis.__finewaveAudioSourceGetListenerPosition(this);\n }\n\n useSpatialization() {\n globalThis.__finewaveAudioSourceUseSpatialization(this);\n }\n}\n\nexport class AudioEffect {}\n\nexport class Reverb extends AudioEffect {\n constructor() {\n super();\n return globalThis.__finewaveCreateReverb() ?? this;\n }\n\n setRoomSize(size) {\n globalThis.__finewaveReverbSetRoomSize(this, size);\n }\n\n setDamping(damping) {\n globalThis.__finewaveReverbSetDamping(this, damping);\n }\n\n setWetLevel(level) {\n globalThis.__finewaveReverbSetWetLevel(this, level);\n }\n\n setDryLevel(level) {\n globalThis.__finewaveReverbSetDryLevel(this, level);\n }\n\n setWidth(width) {\n globalThis.__finewaveReverbSetWidth(this, width);\n }\n}\n\nexport class Echo extends AudioEffect {\n constructor() {\n super();\n return globalThis.__finewaveCreateEcho() ?? this;\n }\n\n setDelay(delay) {\n globalThis.__finewaveEchoSetDelay(this, delay);\n }\n\n setDecay(decay) {\n globalThis.__finewaveEchoSetDecay(this, decay);\n }\n\n setWetLevel(level) {\n globalThis.__finewaveEchoSetWetLevel(this, level);\n }\n\n setDryLevel(level) {\n globalThis.__finewaveEchoSetDryLevel(this, level);\n }\n}\n\nexport class Distortion extends AudioEffect {\n constructor() {\n super();\n return globalThis.__finewaveCreateDistortion() ?? this;\n }\n\n setEdge(edge) {\n globalThis.__finewaveDistortionSetEdge(this, edge);\n }\n\n setGain(gain) {\n globalThis.__finewaveDistortionSetGain(this, gain);\n }\n\n setLowpassCutoff(cutoff) {\n globalThis.__finewaveDistortionSetLowpassCutoff(this, cutoff);\n }\n}\n", }; -static const AtlasPackedScriptSource FINEWAVE = {FINEWAVE_PARTS, 1}; +static const AtlasPackedScriptSource SCRIPTS_FINEWAVE = {SCRIPTS_FINEWAVE_PARTS, 1}; -static const char* const GRAPHITE_PARTS[] = { +static const char* const SCRIPTS_GRAPHITE_PARTS[] = { "import { Resource, ResourceType, UIObject } from \"atlas\";\nimport { Color, Position2d, Position3d, Size2d } from \"atlas/units\";\n\nexport const ElementAlignment = Object.freeze({\n Top: 0,\n Center: 1,\n Bottom: 2,\n});\n\nexport const LayoutAnchor = Object.freeze({\n TopLeft: 0,\n TopCenter: 1,\n TopRight: 2,\n CenterLeft: 3,\n Center: 4,\n CenterRight: 5,\n BottomLeft: 6,\n BottomCenter: 7,\n BottomRight: 8,\n});\n\nexport const UIStyleState = Object.freeze({\n Normal: 0,\n Hovered: 1,\n Pressed: 2,\n Disabled: 3,\n Focused: 4,\n Checked: 5,\n});\n\nfunction commitObject(object) {\n const updated = globalThis.__atlasUpdateObject(object);\n return updated == null ? object : updated;\n}\n\nfunction toPosition3d(position = Position2d.zero()) {\n return new Position3d(position.x, position.y, 0);\n}\n\nfunction makeDefaultStyle() {\n return new UIStyle();\n}\n\nfunction isPosition2dLike(value) {\n return (\n value != null &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n typeof value.x === \"number\" &&\n typeof value.y === \"number\"\n );\n}\n\nfunction resolveCreated(value, fallback) {\n return value == null ? fallback : value;\n}\n\nexport class UIStyleVariant {\n constructor() {\n this.paddingValue = undefined;\n this.cornerRadiusValue = undefined;\n this.borderWidthValue = undefined;\n this.backgroundColorValue = undefined;\n this.borderColorValue = undefined;\n this.foregroundColorValue = undefined;\n this.tintColorValue = undefined;\n this.fontValue = undefined;\n this.fontSizeValue = undefined;\n }\n\n padding(value) {\n this.paddingValue = value;\n return this;\n }\n\n cornerRadius(value) {\n this.cornerRadiusValue = value;\n return this;\n }\n\n borderWidth(value) {\n this.borderWidthValue = value;\n return this;\n }\n\n backgroundColor(value) {\n this.backgroundColorValue = value;\n return this;\n }\n\n borderColor(value) {\n this.borderColorValue = value;\n return this;\n }\n\n foregroundColor(value) {\n this.foregroundColorValue = value;\n return this;\n }\n\n tintColor(value) {\n this.tintColorValue = value;\n return this;\n }\n\n font(value) {\n this.fontValue = value;\n return this;\n }\n\n fontSize(value) {\n this.fontSizeValue = value;\n return this;\n }\n}\n\nexport class UIStyle {\n constructor() {\n this.__normal = new UIStyleVariant();\n this.__hovered = new UIStyleVariant();\n this.__pressed = new UIStyleVariant();\n this.__disabled = new UIStyleVariant();\n this.__focused = new UIStyleVariant();\n this.__checked = new UIStyleVariant();\n }\n\n normal() {\n return this.__normal;\n }\n\n hovered() {\n return this.__hovered;\n }\n\n pressed() {\n return this.__pressed;\n }\n\n disabled() {\n return this.__disabled;\n }\n\n focused() {\n return this.__focused;\n }\n\n checked() {\n return this.__checked;\n }\n\n variant(state) {\n switch (state) {\n case UIStyleState.Normal:\n return this.__normal;\n case UIStyleState.Hovered:\n return this.__hovered;\n case UIStyleState.Pressed:\n return this.__pressed;\n case UIStyleState.Disabled:\n return this.__disabled;\n case UIStyleState.Focused:\n return this.__focused;\n case UIStyleState.Checked:\n return this.__checked;\n default:\n return this.__normal;\n }\n }\n}\n\nexport class Theme {\n constructor() {\n this.text = new UIStyle();\n this.image = new UIStyle();\n this.textField = new UIStyle();\n this.button = new UIStyle();\n this.checkbox = new UIStyle();\n this.row = new UIStyle();\n this.column = new UIStyle();\n this.stack = new UIStyle();\n }\n\n static current() {\n return globalThis.__graphiteGetTheme();\n }\n\n static set(theme) {\n globalThis.__graphiteSetTheme(theme);\n }\n\n static reset() {\n globalThis.__graphiteResetTheme();\n }\n}\n\nexport class Font {\n constructor() {\n this.name = \"\";\n this.atlas = null;\n this.size = 0;\n this.resource = new Resource(ResourceType.Font, \"\", \"\");\n this.texture = null;\n }\n\n static fromResource(resource) {\n return globalThis.__graphiteCreateFont(resource);\n }\n\n static getFont(name) {\n return globalThis.__graphiteGetFont(name);\n }\n\n changeSize(size) {\n return resolveCreated(globalThis.__graphiteChangeFontSize(this, size), this);\n }\n}\n\nexport class Image extends UIObject {\n constructor(\n texture = null,\n size = Size2d.zero(),\n position = Position2d.zero(),\n tint = Color.white(),\n ) {\n super();\n this.texture = texture;\n this.position = toPosition3d(position);\n this.size = size;\n this.tint = tint;\n this.__style = null;\n return resolveCreated(globalThis.__graphiteCreateImage(this), this);\n }\n\n style() {\n if (this.__style == null) {\n this.__style = makeDefaultStyle();\n }\n return this.__style;\n }\n\n setStyle(style) {\n this.__style = style;\n return resolveCreated(globalThis.__graphiteSetUIObjectStyle(this, style), this);\n }\n\n setTexture(texture) {\n this.texture = texture;\n return commitObject(this);\n }\n\n setSize(size) {\n this.size = size;\n return commitObject(this);\n }\n}\n\nexport class TextField extends UIObject {\n constructor(\n font = null,\n maximumWidth = 320,\n position = Position2d.zero(),\n text = \"\",\n placeholder = \"\",\n ) {\n super();\n this.text = text;\n this.placeholder = placeholder;\n this.font = font;\n this.position = toPosition3d(position);\n this.fontSize = 0;\n this.padding = new Size2d(14, 10);\n this.maximumWidth = maximumWidth;\n this.textColor = Color.white();\n this.placeholderColor = new Color(1, 1, 1, 0.45);\n this.backgroundColor = new Color(0.08, 0.09, 0.12, 0.94);\n this.borderColor = new Color(1, 1, 1, 0.15);\n this.focusedBorderColor = new Color(1, 0.55, 0.14, 1);\n this.cursorColor = Color.white();\n this.__style = null;\n return resolveCreated(globalThis.__graphiteCreateTextField(this), this);\n }\n\n getText() {\n return this.text;\n }\n\n isFocused() {\n return globalThis.__graphiteTextFieldIsFocused(this);\n }\n\n getCursorIndex() {\n return globalThis.__graphiteTextFieldGetCursorIndex(this);\n }\n\n style() {\n if (this.__style == null) {\n this.__style = makeDefaultStyle();\n }\n return this.__style;\n }\n\n setText(text) {\n this.text = text;\n return commitObject(this);\n }\n\n setPlaceholder(placeholder) {\n this.placeholder = placeholder;\n return commitObject(this);\n }\n\n setPadding(padding) {\n this.padding = padding;\n return commitObject(this);\n }\n\n setMaximumWidth(width) {\n this.maximumWidth = width;\n return commitObject(this);\n }\n\n setFontSize(size) {\n this.fontSize = size;\n return commitObject(this);\n }\n\n setStyle(style) {\n this.__style = style;\n return resolveCreated(globalThis.__graphiteSetUIObjectStyle(this, style), this);\n }\n\n setOnChange(callback) {\n this.__graphiteOnChange = callback;\n return this;\n }\n\n focus() {\n globalThis.__graphiteTextFieldFocus(this);\n }\n\n blur() {\n globalThis.__graphiteTextFieldBlur(this);\n }\n}\n\nexport class Button extends UIObject {\n constructor(font = null, label = \"\", position = Position2d.zero()) {\n super();\n this.label = label;\n this.font = font;\n this.position = toPosition3d(position);\n this.fontSize = 0;\n this.padding =", " new Size2d(18, 12);\n this.minimumSize = Size2d.zero();\n this.textColor = Color.white();\n this.backgroundColor = new Color(0.15, 0.16, 0.2, 0.96);\n this.hoverBackgroundColor = new Color(0.2, 0.22, 0.27, 0.98);\n this.pressedBackgroundColor = new Color(1, 0.55, 0.14, 0.96);\n this.borderColor = new Color(1, 1, 1, 0.16);\n this.hoverBorderColor = new Color(1, 0.55, 0.14, 1);\n this.enabled = true;\n this.__style = null;\n return resolveCreated(globalThis.__graphiteCreateButton(this), this);\n }\n\n getLabel() {\n return this.label;\n }\n\n isHovered() {\n return globalThis.__graphiteButtonIsHovered(this);\n }\n\n isEnabled() {\n return this.enabled;\n }\n\n style() {\n if (this.__style == null) {\n this.__style = makeDefaultStyle();\n }\n return this.__style;\n }\n\n setLabel(label) {\n this.label = label;\n return commitObject(this);\n }\n\n setPadding(padding) {\n this.padding = padding;\n return commitObject(this);\n }\n\n setMinimumSize(size) {\n this.minimumSize = size;\n return commitObject(this);\n }\n\n setFontSize(size) {\n this.fontSize = size;\n return commitObject(this);\n }\n\n setStyle(style) {\n this.__style = style;\n return resolveCreated(globalThis.__graphiteSetUIObjectStyle(this, style), this);\n }\n\n setOnClick(callback) {\n this.__graphiteOnClick = callback;\n return this;\n }\n\n setEnabled(enabled) {\n this.enabled = enabled;\n commitObject(this);\n }\n}\n\nexport class Checkbox extends UIObject {\n constructor(font = null, label = \"\", position = Position2d.zero()) {\n super();\n this.label = label;\n this.font = font;\n this.position = toPosition3d(position);\n this.fontSize = 0;\n this.padding = new Size2d(8, 8);\n this.boxSize = 18;\n this.spacing = 10;\n this.checked = false;\n this.enabled = true;\n this.textColor = Color.white();\n this.boxBackgroundColor = new Color(0.12, 0.13, 0.17, 0.95);\n this.hoverBoxBackgroundColor = new Color(0.18, 0.19, 0.24, 0.98);\n this.borderColor = new Color(1, 1, 1, 0.16);\n this.activeBorderColor = new Color(1, 0.55, 0.14, 1);\n this.checkColor = new Color(1, 0.55, 0.14, 1);\n this.__style = null;\n return resolveCreated(globalThis.__graphiteCreateCheckbox(this), this);\n }\n\n getLabel() {\n return this.label;\n }\n\n isChecked() {\n return this.checked;\n }\n\n isHovered() {\n return globalThis.__graphiteCheckboxIsHovered(this);\n }\n\n isEnabled() {\n return this.enabled;\n }\n\n style() {\n if (this.__style == null) {\n this.__style = makeDefaultStyle();\n }\n return this.__style;\n }\n\n setLabel(label) {\n this.label = label;\n return commitObject(this);\n }\n\n setPadding(padding) {\n this.padding = padding;\n return commitObject(this);\n }\n\n setFontSize(size) {\n this.fontSize = size;\n return commitObject(this);\n }\n\n setBoxSize(size) {\n this.boxSize = size;\n return commitObject(this);\n }\n\n setSpacing(spacing) {\n this.spacing = spacing;\n return commitObject(this);\n }\n\n setStyle(style) {\n this.__style = style;\n return resolveCreated(globalThis.__graphiteSetUIObjectStyle(this, style), this);\n }\n\n setOnToggle(callback) {\n this.__graphiteOnToggle = callback;\n return this;\n }\n\n setChecked(checked) {\n this.checked = checked;\n commitObject(this);\n }\n\n setEnabled(enabled) {\n this.enabled = enabled;\n commitObject(this);\n }\n\n toggle() {\n return globalThis.__graphiteCheckboxToggle(this);\n }\n}\n\nexport class Column extends UIObject {\n constructor(\n children = [],\n spacing = 0,\n padding = Size2d.zero(),\n position = Position2d.zero(),\n ) {\n super();\n if (isPosition2dLike(children)) {\n position = children;\n children = [];\n }\n this.spacing = spacing;\n this.maxSize = Size2d.zero();\n this.padding = padding;\n this.children = children;\n this.position = toPosition3d(position);\n this.alignment = ElementAlignment.Top;\n this.anchor = LayoutAnchor.TopLeft;\n this.style = makeDefaultStyle();\n return resolveCreated(globalThis.__graphiteCreateColumn(this), this);\n }\n\n addChild(child) {\n this.children.push(child);\n commitObject(this);\n }\n\n setChildren(children) {\n this.children = children;\n commitObject(this);\n }\n\n setStyle(style) {\n this.style = style;\n return commitObject(this);\n }\n}\n\nexport class Row extends UIObject {\n constructor(\n children = [],\n spacing = 0,\n padding = Size2d.zero(),\n position = Position2d.zero(),\n ) {\n super();\n if (isPosition2dLike(children)) {\n position = children;\n children = [];\n }\n this.spacing = spacing;\n this.maxSize = Size2d.zero();\n this.padding = padding;\n this.children = children;\n this.position = toPosition3d(position);\n this.alignment = ElementAlignment.Center;\n this.anchor = LayoutAnchor.TopLeft;\n this.style = makeDefaultStyle();\n return resolveCreated(globalThis.__graphiteCreateRow(this), this);\n }\n\n addChild(child) {\n this.children.push(child);\n commitObject(this);\n }\n\n setChildren(children) {\n this.children = children;\n commitObject(this);\n }\n\n setStyle(style) {\n this.style = style;\n return commitObject(this);\n }\n}\n\nexport class Stack extends UIObject {\n constructor(\n children = [],\n padding = Size2d.zero(),\n position = Position2d.zero(),\n ) {\n super();\n if (isPosition2dLike(children)) {\n position = children;\n children = [];\n }\n this.maxSize = Size2d.zero();\n this.padding = padding;\n this.children = children;\n this.position = toPosition3d(position);\n this.horizontalAlignment = ElementAlignment.Top;\n this.verticalAlignment = ElementAlignment.Top;\n this.anchor = LayoutAnchor.TopLeft;\n this.style = makeDefaultStyle();\n return resolveCreated(globalThis.__graphiteCreateStack(this), this);\n }\n\n addChild(child) {\n this.children.push(child);\n commitObject(this);\n }\n\n setChildren(children) {\n this.children = children;\n commitObject(this);\n }\n\n setStyle(style) {\n this.style = style;\n return commitObject(this);\n }\n}\n\nexport class Text extends UIObject {\n constructor(\n text = \"\",\n font = null,\n color = Color.white(),\n position = Position2d.zero(),\n ) {\n super();\n this.content = text;\n this.font = font;\n this.position = toPosition3d(position);\n this.fontSize = 0;\n this.color = color;\n this.__style = null;\n return resolveCreated(globalThis.__graphiteCreateText(this), this);\n }\n\n style() {\n if (this.__style == null) {\n this.__style = makeDefaultStyle();\n }\n return this.__style;\n }\n\n setStyle(style) {\n this.__style = style;\n return resolveCreated(globalThis.__graphiteSetUIObjectStyle(this, style), this);\n }\n\n setFontSize(size) {\n this.fontSize = size;\n return commitObject(this);\n }\n}\n", }; -static const AtlasPackedScriptSource GRAPHITE = {GRAPHITE_PARTS, 2}; +static const AtlasPackedScriptSource SCRIPTS_GRAPHITE = {SCRIPTS_GRAPHITE_PARTS, 2}; -static const char* const HYDRA_PARTS[] = { +static const char* const SCRIPTS_HYDRA_PARTS[] = { "import { GameObject } from \"atlas\";\nimport { Color, Position3d, Size2d } from \"atlas/units\";\n\nexport const WeatherCondition = Object.freeze({\n Clear: 0,\n Rain: 1,\n Snow: 2,\n Storm: 3,\n});\n\nfunction resolved(value, fallback) {\n return value == null ? fallback : value;\n}\n\nfunction syncClouds(clouds) {\n return resolved(globalThis.__hydraUpdateClouds(clouds), clouds);\n}\n\nfunction syncAtmosphere(atmosphere) {\n return resolved(globalThis.__hydraUpdateAtmosphere(atmosphere), atmosphere);\n}\n\nexport class WorleyNoise3D {\n constructor(frequency, numDivisions) {\n return resolved(\n globalThis.__hydraCreateWorleyNoise(frequency, numDivisions),\n this,\n );\n }\n\n getValue(x, y, z) {\n return globalThis.__hydraWorleyGetValue(this, x, y, z);\n }\n\n get3dTexture(size) {\n return globalThis.__hydraWorleyGet3dTexture(this, size);\n }\n\n getDetailTexture(size) {\n return globalThis.__hydraWorleyGetDetailTexture(this, size);\n }\n\n get3dTextureAtAllChannels(size) {\n return globalThis.__hydraWorleyGetAllChannelsTexture(this, size);\n }\n}\n\nexport class Clouds {\n constructor(frequency, numDivisions) {\n this.position = new Position3d(0, 5, 0);\n this.size = new Position3d(10, 3, 10);\n this.scale = 1.5;\n this.offset = Position3d.zero();\n this.density = 0.45;\n this.densityMultiplier = 1.5;\n this.absorption = 1.1;\n this.scattering = 0.85;\n this.phase = 0.55;\n this.clusterStrength = 0.5;\n this.primaryStepCount = 12;\n this.lightStepCount = 6;\n this.lightStepMultiplier = 1.6;\n this.minStepLength = 0.05;\n this.wind = new Position3d(0.02, 0, 0.01);\n return resolved(\n globalThis.__hydraCreateClouds(frequency, numDivisions),\n this,\n );\n }\n\n getCloudTexture(size) {\n syncClouds(this);\n return globalThis.__hydraCloudsGetTexture(this, size);\n }\n}\n\nexport class Atmosphere {\n constructor() {\n this.timeOfDay = 12;\n this.secondsPerHour = 3600;\n this.wind = Position3d.zero();\n this.weatherDelegate = null;\n this.clouds = null;\n this.sunColor = new Color(1, 0.95, 0.8, 1);\n this.moonColor = new Color(0.5, 0.5, 0.8, 1);\n this.sunSize = 1;\n this.moonSize = 1;\n this.sunTintStrength = 0.3;\n this.moonTintStrength = 0.8;\n this.starIntensity = 3;\n this.cycle = false;\n return resolved(globalThis.__hydraCreateAtmosphere(), this);\n }\n\n enable() {\n syncAtmosphere(this);\n globalThis.__hydraAtmosphereEnable(this);\n }\n\n disable() {\n syncAtmosphere(this);\n globalThis.__hydraAtmosphereDisable(this);\n }\n\n isEnabled() {\n return globalThis.__hydraAtmosphereIsEnabled(this);\n }\n\n enableWeather() {\n syncAtmosphere(this);\n globalThis.__hydraAtmosphereEnableWeather(this);\n }\n\n disableWeather() {\n syncAtmosphere(this);\n globalThis.__hydraAtmosphereDisableWeather(this);\n }\n\n getNormalizedTime() {\n syncAtmosphere(this);\n return globalThis.__hydraAtmosphereGetNormalizedTime(this);\n }\n\n getSunAngle() {\n syncAtmosphere(this);\n return globalThis.__hydraAtmosphereGetSunAngle(this);\n }\n\n getMoonAngle() {\n syncAtmosphere(this);\n return globalThis.__hydraAtmosphereGetMoonAngle(this);\n }\n\n getLightIntensity() {\n syncAtmosphere(this);\n return globalThis.__hydraAtmosphereGetLightIntensity(this);\n }\n\n getLightColor() {\n syncAtmosphere(this);\n return globalThis.__hydraAtmosphereGetLightColor(this);\n }\n\n getSkyboxColors() {\n syncAtmosphere(this);\n return globalThis.__hydraAtmosphereGetSkyboxColors(this);\n }\n\n createSkyCubemap(size) {\n syncAtmosphere(this);\n return globalThis.__hydraAtmosphereCreateSkyCubemap(this, size);\n }\n\n updateSkyCubemap(cubemap) {\n syncAtmosphere(this);\n globalThis.__hydraAtmosphereUpdateSkyCubemap(this, cubemap);\n }\n\n castShadowsFromSunlight(resolution) {\n syncAtmosphere(this);\n globalThis.__hydraAtmosphereCastShadows(this, resolution);\n }\n\n useGlobalLight() {\n syncAtmosphere(this);\n globalThis.__hydraAtmosphereUseGlobalLight(this);\n }\n\n isDaytime() {\n syncAtmosphere(this);\n return globalThis.__hydraAtmosphereIsDaytime(this);\n }\n\n setTime(hours, minutes = 0, seconds = 0) {\n this.timeOfDay = hours + (minutes / 60) + (seconds / 3600);\n globalThis.__hydraAtmosphereSetTime(this, hours, minutes, seconds);\n }\n\n addClouds(frequency = 4, numDivisions = 6) {\n syncAtmosphere(this);\n const updated = resolved(\n globalThis.__hydraAtmosphereAddClouds(this, frequency, numDivisions),\n this,\n );\n this.clouds = updated.clouds;\n }\n\n resetRuntimeState() {\n syncAtmosphere(this);\n globalThis.__hydraAtmosphereResetRuntimeState(this);\n }\n}\n\nexport class Fluid extends GameObject {\n constructor() {\n super();\n this.waveVelocity = 0;\n this.normalTexture = null;\n this.movementTexture = null;\n return resolved(globalThis.__hydraCreateFluid(this), this);\n }\n\n create(extent, color) {\n globalThis.__hydraFluidCreate(this, extent, color);\n }\n\n move(position) {\n this.position.x += position.x;\n this.position.y += position.y;\n this.position.z += position.z;\n globalThis.__atlasUpdateObject(this);\n }\n\n setPosition(position) {\n this.position = position;\n globalThis.__atlasUpdateObject(this);\n }\n\n setRotation(rotation) {\n this.rotation = rotation;\n globalThis.__atlasUpdateObject(this);\n }\n\n rotate(rotation) {\n this.rotation.x += rotation.x;\n this.rotation.y += rotation.y;\n this.rotation.z += rotation.z;\n globalThis.__atlasUpdateObject(this);\n }\n\n setScale(scale) {\n this.scale = scale;\n globalThis.__atlasUpdateObject(this);\n }\n\n setExtent(extent) {\n globalThis.__hydraFluidSetExtent(this, extent);\n }\n\n setWaveVelocity(velocity) {\n this.waveVelocity = velocity;\n globalThis.__hydraFluidSetWaveVelocity(this, velocity);\n }\n\n setWaterColor(color) {\n globalThis.__hydraFluidSetWaterColor(this, color);\n }\n\n getPosition() {\n return this.position;\n }\n\n getScale() {\n return this.scale;\n }\n}\n", }; -static const AtlasPackedScriptSource HYDRA = {HYDRA_PARTS, 1}; +static const AtlasPackedScriptSource SCRIPTS_HYDRA = {SCRIPTS_HYDRA_PARTS, 1}; static const AtlasRuntimeScriptModule ATLAS_RUNTIME_SCRIPT_MODULES[] = { - {"atlas", ATLAS}, - {"atlas/audio", ATLAS_AUDIO}, - {"atlas_audio", ATLAS_AUDIO}, - {"atlas/graphics", ATLAS_GRAPHICS}, - {"atlas_graphics", ATLAS_GRAPHICS}, - {"atlas/input", ATLAS_INPUT}, - {"atlas_input", ATLAS_INPUT}, - {"atlas/log", ATLAS_LOG}, - {"atlas_log", ATLAS_LOG}, - {"atlas/particles", ATLAS_PARTICLES}, - {"atlas_particles", ATLAS_PARTICLES}, - {"atlas/units", ATLAS_UNITS}, - {"atlas_units", ATLAS_UNITS}, - {"aurora", AURORA}, - {"bezel", BEZEL}, - {"finewave", FINEWAVE}, - {"graphite", GRAPHITE}, - {"hydra", HYDRA}, + {"scripts/atlas", SCRIPTS_ATLAS}, + {"scripts/atlas_audio", SCRIPTS_ATLAS_AUDIO}, + {"scripts/atlas_graphics", SCRIPTS_ATLAS_GRAPHICS}, + {"scripts/atlas_input", SCRIPTS_ATLAS_INPUT}, + {"scripts/atlas_log", SCRIPTS_ATLAS_LOG}, + {"scripts/atlas_particles", SCRIPTS_ATLAS_PARTICLES}, + {"scripts/atlas_units", SCRIPTS_ATLAS_UNITS}, + {"scripts/aurora", SCRIPTS_AURORA}, + {"scripts/bezel", SCRIPTS_BEZEL}, + {"scripts/finewave", SCRIPTS_FINEWAVE}, + {"scripts/graphite", SCRIPTS_GRAPHITE}, + {"scripts/hydra", SCRIPTS_HYDRA}, }; static constexpr std::size_t ATLAS_RUNTIME_SCRIPT_MODULE_COUNT = sizeof(ATLAS_RUNTIME_SCRIPT_MODULES) / sizeof(ATLAS_RUNTIME_SCRIPT_MODULES[0]); diff --git a/include/atlas/runtime/context.h b/include/atlas/runtime/context.h index 4a60fd2d..422801d7 100644 --- a/include/atlas/runtime/context.h +++ b/include/atlas/runtime/context.h @@ -90,6 +90,9 @@ class Context { bool editorCameraFocused = false; bool editorRuntime = false; bool materialPreviewRuntime = false; + float materialPreviewYaw = 0.0f; + float materialPreviewPitch = 0.0f; + std::function errorReporter; std::unique_ptr window; std::vector> objects; @@ -167,6 +170,10 @@ class Context { bool setMaterialPreviewMaterial(const std::string &definition, const std::string &baseDir); bool setMaterialPreviewEnvironment(int mode); + bool rotateMaterialPreview(float yawDelta, float pitchDelta); + bool materialPreviewUsesPathTracing() const { + return materialPreviewRuntime && config.renderer == "pathtracing"; + } int addObjectComponent(int id, const json &component); bool removeObjectComponent(int id, int componentIndex); bool controlObjectAudio(int id, int componentIndex, diff --git a/include/atlas/scene.h b/include/atlas/scene.h index f273fd95..4e9977c6 100644 --- a/include/atlas/scene.h +++ b/include/atlas/scene.h @@ -65,6 +65,7 @@ struct VolumetricLighting { * @brief Configuration values for bloom post-processing. */ struct LightBloom { + float threshold = 0.8f; /** * @brief Radius of the blur kernel applied to bright fragments. */ @@ -349,6 +350,8 @@ class Scene { */ void setEnvironment(Environment newEnv) { environment = std::move(newEnv); } + const Environment &getEnvironment() const { return environment; } + /** * @brief Internal update hook used by the renderer to advance scene-wide * effects. diff --git a/include/atlas/window.h b/include/atlas/window.h index bcf64da4..1657ec25 100644 --- a/include/atlas/window.h +++ b/include/atlas/window.h @@ -508,6 +508,7 @@ class Window { void enablePathTracing(); void configurePathTracing(int samplesPerPixel, int bounceLimit, bool denoising, int accumulationFrames); + void resetPathTracingAccumulation(); bool setEditorPathTracingPreview(bool enabled); const std::string &getPathTracingError() const; #endif @@ -534,6 +535,7 @@ class Window { * property is a good idea. */ void addPreferencedObject(Renderable *object); + void removePreferencedObject(Renderable *object); /** * @brief Adds a renderable object to be rendered first. * diff --git a/include/editor/views/editorWindow.h b/include/editor/views/editorWindow.h index c320bf2f..a881f432 100644 --- a/include/editor/views/editorWindow.h +++ b/include/editor/views/editorWindow.h @@ -37,6 +37,7 @@ class QTimer; class QFileSystemWatcher; class QEvent; class SplashScreen; +class QPlainTextEdit; class EditorWindow : public QMainWindow { Q_OBJECT @@ -93,6 +94,7 @@ class EditorWindow : public QMainWindow { QTimer *layoutSaveTimer = nullptr; SplashScreen *assetLoadingSplash = nullptr; QFileSystemWatcher *scriptWatcher = nullptr; + QPlainTextEdit *runtimeErrors = nullptr; QByteArray defaultDockState; QString projectFile; QString projectName; @@ -100,6 +102,7 @@ class EditorWindow : public QMainWindow { bool restoringLayout = false; bool startupQueued = false; bool startupComplete = false; + bool workspaceChangesPending = false; void closeEvent(QCloseEvent *event) override; void showEvent(QShowEvent *event) override; diff --git a/include/editor/views/graphiteEditor.h b/include/editor/views/graphiteEditor.h index 9f33f964..1db3cadc 100644 --- a/include/editor/views/graphiteEditor.h +++ b/include/editor/views/graphiteEditor.h @@ -25,11 +25,13 @@ class GraphiteEditorPanel : public QWidget { void openUI(const QString &path); void saveUI(); + void flushPendingSave(); void undo(); void redo(); signals: void previewRequested(); + void documentSaved(); private: void showEmptyState(); @@ -64,6 +66,7 @@ class GraphiteEditorPanel : public QWidget { QVBoxLayout *inspectorLayout = nullptr; QUndoStack *undoStack = nullptr; bool loading = false; + bool documentDirty = false; int nextElementNumber = 1; QString styleVariant = "normal"; }; diff --git a/include/editor/views/materialEditor.h b/include/editor/views/materialEditor.h index 1fb7a268..2b82b9ad 100644 --- a/include/editor/views/materialEditor.h +++ b/include/editor/views/materialEditor.h @@ -25,6 +25,7 @@ class MaterialEditorPanel : public QWidget { explicit MaterialEditorPanel(ViewportPanel *viewport, QWidget *parent = nullptr); ~MaterialEditorPanel() override; + void flushPendingSave(); public slots: void openMaterial(const QString &path); diff --git a/include/editor/views/postProcessing.h b/include/editor/views/postProcessing.h index 3a3fc128..bc678af0 100644 --- a/include/editor/views/postProcessing.h +++ b/include/editor/views/postProcessing.h @@ -2,6 +2,7 @@ #define ATLAS_POSTPROCESSING_H #include +#include #include #include #include @@ -22,6 +23,9 @@ class PostProcessingPanel : public QWidget { public slots: void applySceneSnapshot(const QString &snapshot); + signals: + void settingsChanged(); + private: void rebuildTargetList(); void rebuildEditor(); @@ -33,6 +37,7 @@ class PostProcessingPanel : public QWidget { void setTargetValue(const QString &path, const QJsonValue &value); void setEffectValue(int effectIndex, const QString &key, const QJsonValue &value); + void setBloomThreshold(double value); void replaceTargets(); ViewportPanel *viewport = nullptr; @@ -42,6 +47,7 @@ class PostProcessingPanel : public QWidget { QWidget *body = nullptr; QVBoxLayout *bodyLayout = nullptr; QJsonArray targets; + QJsonObject environment; int targetIndex = -1; bool applying = false; }; diff --git a/include/editor/views/viewport.h b/include/editor/views/viewport.h index a00f59dc..453b9c0f 100644 --- a/include/editor/views/viewport.h +++ b/include/editor/views/viewport.h @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -118,6 +119,7 @@ class ViewportPanel : public QWidget { void runtimeLoadingStarted(); void runtimeLoadingStatusChanged(const QString &status); void runtimeLoadingFinished(); + void runtimeErrorOccurred(const QString &message); void transformHintChanged(const QString &hint); void sceneOpened(const QString &path); void transformSpaceChanged(bool local); @@ -158,7 +160,6 @@ class ViewportPanel : public QWidget { QTimer *frameTimer = nullptr; QTimer *resizeTimer = nullptr; - QTimer *environmentReloadTimer = nullptr; QUndoStack *undoStack = nullptr; QString projectFile; std::shared_ptr runtimeContext; @@ -169,6 +170,8 @@ class ViewportPanel : public QWidget { QString selectionToRestore; QByteArray objectClipboard; QJsonObject transformUndoBefore; + QElapsedTimer snapshotTimer; + QElapsedTimer frameRateTimer; bool runtimeStartQueued = false; bool runtimeStartupEnabled = false; bool shuttingDown = false; diff --git a/include/editor/views/viewportTools.h b/include/editor/views/viewportTools.h index 9b768d85..5ab90602 100644 --- a/include/editor/views/viewportTools.h +++ b/include/editor/views/viewportTools.h @@ -8,6 +8,7 @@ class QLabel; class QTabBar; class QToolButton; class ViewportPanel; +class ViewportHost; class ViewportTools : public QWidget { Q_OBJECT @@ -36,6 +37,7 @@ class ViewportTools : public QWidget { QLabel *cameraLabel = nullptr; QLabel *shortcutHint = nullptr; QTabBar *sceneTabs = nullptr; + ViewportHost *viewportHost = nullptr; QString projectRoot; QStringList scenePaths; bool runtimeAvailable = false; diff --git a/include/editor/widgets/scrubbableSpinBox.h b/include/editor/widgets/scrubbableSpinBox.h index df07b4cf..463c68d0 100644 --- a/include/editor/widgets/scrubbableSpinBox.h +++ b/include/editor/widgets/scrubbableSpinBox.h @@ -3,12 +3,37 @@ #include #include +#include #include #include #include #include +class FlexibleDoubleSpinBox : public QDoubleSpinBox { + public: + explicit FlexibleDoubleSpinBox(QWidget *parent = nullptr) + : QDoubleSpinBox(parent) {} + + protected: + double valueFromText(const QString &text) const override { + return QDoubleSpinBox::valueFromText(normalizedText(text)); + } + + QValidator::State validate(QString &text, int &position) const override { + QString normalized = normalizedText(text); + return QDoubleSpinBox::validate(normalized, position); + } + + private: + QString normalizedText(QString text) const { + const QString decimalPoint = locale().decimalPoint(); + text.replace('.', decimalPoint); + text.replace(',', decimalPoint); + return text; + } +}; + template class ScrubbableSpinBoxBase : public SpinBox { public: explicit ScrubbableSpinBoxBase(QWidget *parent = nullptr) @@ -71,7 +96,7 @@ template class ScrubbableSpinBoxBase : public SpinBox { bool selectOnRelease = false; }; -using ScrubbableDoubleSpinBox = ScrubbableSpinBoxBase; +using ScrubbableDoubleSpinBox = ScrubbableSpinBoxBase; using ScrubbableSpinBox = ScrubbableSpinBoxBase; #endif diff --git a/justfile b/justfile index b8a19949..2195720d 100644 --- a/justfile +++ b/justfile @@ -60,7 +60,11 @@ frametest: timeout 2 ./build/bin/atlas_test cli: - cargo build + cargo build + +copy-cli: + cargo build --release + cp target/release/atlas /usr/local/bin/atlas package-debug-macos: ./scripts/package_app.py --debug --macOS diff --git a/photon/path_tracing.cpp b/photon/path_tracing.cpp index 77df689a..75677512 100644 --- a/photon/path_tracing.cpp +++ b/photon/path_tracing.cpp @@ -1283,6 +1283,9 @@ bool photon::PathTracing::render( pathTracingPipeline->setUniform1i("sceneData.accumulationFrameLimit", accumulationFrames); pathTracingPipeline->setUniform1f("sceneData.fireflyClamp", fireflyClamp); + pathTracingPipeline->setUniform1f( + "sceneData.bloomThreshold", + std::max(scene->getEnvironment().lightBloom.threshold, 0.0f)); pathTracingPipeline->setUniform1i("sceneData.numEmissiveTriangles", emissiveTriangleCount); @@ -1347,6 +1350,9 @@ bool photon::PathTracing::render( pathTracingHistoryMoments[historyReadIndex]->texture, 5); pathDenoisePipeline->setUniform1i("parameters.stepWidth", denoiseSteps[pass]); + pathDenoisePipeline->setUniform1f( + "parameters.bloomThreshold", + std::max(scene->getEnvironment().lightBloom.threshold, 0.0f)); commandBuffer->dispatch(outputWidth, outputHeight, 1); commandBuffer->computeBarrier(); } diff --git a/runtime/atlas.d.ts b/runtime/atlas.d.ts index e309cbeb..0e8f5eba 100644 --- a/runtime/atlas.d.ts +++ b/runtime/atlas.d.ts @@ -64,6 +64,7 @@ declare module "atlas" { }; export type LightBloomConfiguration = { + threshold: number; radius: number; maxSamples: number; }; diff --git a/runtime/docs/other.md b/runtime/docs/other.md index 9b5836c0..355499b6 100644 --- a/runtime/docs/other.md +++ b/runtime/docs/other.md @@ -109,6 +109,7 @@ The scene can define an `environment` object. This combines the scene `Environme "exposure": 0.7 }, "lightBloom": { + "threshold": 0.8, "radius": 0.01, "maxSamples": 6 }, @@ -178,6 +179,7 @@ Top-level `environment` properties: * `decay`: Falloff per step. * `exposure`: Final intensity multiplier. * `lightBloom`: Controls deferred bloom generation. + * `threshold`: Minimum luminance extracted into the bloom buffer. * `radius`: Bloom blur radius. * `maxSamples`: Number of blur passes. * `rimLight`: Controls rim lighting applied in supported shaders. diff --git a/runtime/lib/context.cpp b/runtime/lib/context.cpp index dd33bb1a..01830ff2 100644 --- a/runtime/lib/context.cpp +++ b/runtime/lib/context.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -114,6 +115,18 @@ class WindowActivationScope { opal::Device *previousDevice; }; +void clearRenderTargets( + Window *window, + std::map> &renderTargets) { + if (window != nullptr) { + for (auto &[_, target] : renderTargets) { + window->removeRenderTarget(target.get()); + window->removePreferencedObject(target.get()); + } + } + renderTargets.clear(); +} + struct PendingComponent { GameObject *object = nullptr; std::string objectType; @@ -335,6 +348,13 @@ std::string normalizeToken(std::string value) { return normalized; } +std::string propertyNameToken(const std::string &path) { + const std::size_t separator = path.find_last_of('/'); + return normalizeToken(separator == std::string::npos + ? path + : path.substr(separator + 1)); +} + std::string resolveRuntimePath(const std::string &baseDir, const std::string &path) { const std::filesystem::path candidate(path); @@ -896,6 +916,8 @@ loadEnvironmentDefinition(const json &sceneData, const std::string &baseDir) { throw std::runtime_error("Environment light bloom must be an " "object"); } + tryReadFloatAny(*bloomNode, {"threshold"}, + loaded.environment.lightBloom.threshold); tryReadFloatAny(*bloomNode, {"radius"}, loaded.environment.lightBloom.radius); tryReadIntAny(*bloomNode, {"maxSamples"}, @@ -4623,6 +4645,7 @@ runtime::makeMaterialPreviewContextForMetalView(std::string projectFile, metalView, nullptr); context->editorRuntime = false; context->materialPreviewRuntime = true; + context->loadProject(); restorePrevious(); return context; } catch (...) { @@ -4762,6 +4785,19 @@ bool Context::resize(int width, int height, float scale) { } WindowActivationScope activeWindow(*window); window->resize(width, height, scale); + if (materialPreviewRuntime && camera != nullptr) { + const float aspect = static_cast(std::max(1, width)) / + static_cast(std::max(1, height)); + const float verticalHalfFov = glm::radians(camera->fov * 0.5f); + const float horizontalHalfFov = + std::atan(std::tan(verticalHalfFov) * aspect); + const float limitingHalfFov = + std::max(glm::radians(5.0f), + std::min(verticalHalfFov, horizontalHalfFov)); + const float distance = 0.82f / std::sin(limitingHalfFov); + camera->setPosition({0.0f, 0.0f, distance}); + camera->lookAt(Position3d::zero()); + } return true; } @@ -5165,7 +5201,7 @@ std::optional propertySyncSourceValue(Context &context, if (component == "bounds") return editorObjectBoundsSize(*object); if (component == "transform") { - const std::string property = normalizeToken(path); + const std::string property = propertyNameToken(path); if (property == "position") return vec3ToJson(object->getPosition()); if (property == "rotation") @@ -5457,7 +5493,7 @@ bool Context::setObjectProperty(int id, const std::string &component, if (!readEditorVec3(value, vector)) { return false; } - const std::string property = normalizeToken(propertyPath); + const std::string property = propertyNameToken(propertyPath); if (property == "position") { object->setPosition(vector); } else if (property == "rotation") { @@ -5552,6 +5588,8 @@ bool Context::setObjectProperty(int id, const std::string &component, } catch (const std::exception &error) { RUNTIME_LOG("Component update is waiting for valid values: " + std::string(error.what())); + if (errorReporter) + errorReporter(error.what()); } applyPropertySyncs(*this, true); return true; @@ -5662,16 +5700,17 @@ bool Context::initializeMaterialPreview(const std::string &definition, WindowActivationScope activeWindow(*window); sceneDir = baseDir; - config.renderer = "deferred"; camera = std::make_unique(); camera->setPosition({0.0f, 0.0f, 2.15f}); camera->lookAt(Position3d::zero()); camera->nearClip = 0.05f; camera->farClip = 50.0f; + materialPreviewYaw = 0.0f; + materialPreviewPitch = 0.0f; window->setCamera(camera.get()); window->setEditorSceneCamera(nullptr); window->setEditorControlsEnabled(false); - window->useDeferredRendering(); + window->setScene(scene.get()); auto sphere = std::make_shared(); *sphere = createSphere(0.72f, 64, 32, Color::white()); @@ -5703,7 +5742,6 @@ bool Context::initializeMaterialPreview(const std::string &definition, scene->addAreaLight(rimLight.get()); areaLights.push_back(std::move(rimLight)); - window->setScene(scene.get()); return setMaterialPreviewEnvironment(environmentMode) && setMaterialPreviewMaterial(definition, baseDir); } @@ -5722,6 +5760,9 @@ bool Context::setMaterialPreviewMaterial(const std::string &definition, WindowActivationScope activeWindow(*window); applyMaterial(*sphere, loadMaterialDefinition(json::parse(definition), baseDir)); +#ifdef METAL + window->resetPathTracingAccumulation(); +#endif return true; } catch (const std::exception &error) { RUNTIME_LOG("Material preview could not be updated: " + @@ -5737,7 +5778,6 @@ bool Context::setMaterialPreviewEnvironment(int mode) { } WindowActivationScope activeWindow(*window); - std::array colors; Color ambient; Color key; Color rim; @@ -5746,42 +5786,58 @@ bool Context::setMaterialPreviewEnvironment(int mode) { float directionalIntensity = 0.0f; float keyIntensity = 0.0f; float rimIntensity = 0.0f; + if (const auto activeSkybox = scene->getSkybox(); activeSkybox != nullptr) { + activeSkybox->hide(); + } if (mode == 1) { - colors = {Color{0.92f, 0.3f, 0.12f, 1.0f}, - Color{0.16f, 0.05f, 0.2f, 1.0f}, - Color{0.34f, 0.12f, 0.32f, 1.0f}, - Color{0.08f, 0.025f, 0.045f, 1.0f}, - Color{0.98f, 0.48f, 0.18f, 1.0f}, - Color{0.12f, 0.04f, 0.18f, 1.0f}}; - ambient = {0.72f, 0.28f, 0.32f, 1.0f}; - key = {1.0f, 0.42f, 0.18f, 1.0f}; - rim = {0.42f, 0.16f, 0.72f, 1.0f}; - background = {0.08f, 0.025f, 0.055f, 1.0f}; - ambientIntensity = 0.7f; - directionalIntensity = 0.8f; - keyIntensity = 5.5f; - rimIntensity = 3.0f; + scene->atmosphere.enable(); + scene->atmosphere.setTime(14.0f); + scene->setUseAtmosphereSkybox(true); + scene->updateScene(0.0f); + if (const auto skybox = scene->getSkybox(); skybox != nullptr) { + skybox->show(); + } + ambient = scene->atmosphere.getLightColor(); + key = scene->atmosphere.getLightColor(); + rim = {0.48f, 0.68f, 1.0f, 1.0f}; + background = {0.3f, 0.55f, 0.82f, 1.0f}; + ambientIntensity = 0.35f; + directionalIntensity = scene->atmosphere.getLightIntensity(); + keyIntensity = 3.5f; + rimIntensity = 1.8f; } else if (mode == 2) { - colors = { - Color{0.52f, 0.76f, 1.0f, 1.0f}, Color{0.42f, 0.68f, 0.96f, 1.0f}, - Color{0.3f, 0.62f, 1.0f, 1.0f}, Color{0.72f, 0.78f, 0.82f, 1.0f}, - Color{0.62f, 0.82f, 1.0f, 1.0f}, Color{0.46f, 0.72f, 0.98f, 1.0f}}; - ambient = {0.58f, 0.74f, 1.0f, 1.0f}; - key = {1.0f, 0.95f, 0.84f, 1.0f}; - rim = {0.42f, 0.7f, 1.0f, 1.0f}; - background = {0.28f, 0.5f, 0.76f, 1.0f}; - ambientIntensity = 0.9f; - directionalIntensity = 1.15f; - keyIntensity = 4.0f; - rimIntensity = 2.2f; + scene->atmosphere.disable(); + scene->setUseAtmosphereSkybox(false); + ambient = {0.18f, 0.18f, 0.18f, 1.0f}; + key = {1.0f, 0.97f, 0.92f, 1.0f}; + rim = {0.5f, 0.62f, 0.82f, 1.0f}; + background = Color::black(); + ambientIntensity = 0.18f; + directionalIntensity = 0.65f; + keyIntensity = 4.5f; + rimIntensity = 2.0f; } else { - colors = {Color{0.9f, 0.9f, 0.88f, 1.0f}, - Color{0.035f, 0.04f, 0.05f, 1.0f}, - Color{0.7f, 0.74f, 0.8f, 1.0f}, - Color{0.025f, 0.025f, 0.03f, 1.0f}, - Color{0.38f, 0.4f, 0.44f, 1.0f}, - Color{0.07f, 0.075f, 0.085f, 1.0f}}; + static std::mt19937 generator(std::random_device{}()); + std::uniform_real_distribution tint(0.82f, 1.0f); + const float warm = tint(generator); + const float cool = tint(generator); + const std::array colors = { + Color{warm, warm * 0.96f, warm * 0.88f, 1.0f}, + Color{0.025f, 0.03f, 0.04f, 1.0f}, + Color{cool * 0.72f, cool * 0.8f, cool, 1.0f}, + Color{0.018f, 0.02f, 0.026f, 1.0f}, + Color{warm * 0.42f, warm * 0.44f, warm * 0.48f, 1.0f}, + Color{cool * 0.06f, cool * 0.072f, cool * 0.09f, 1.0f}}; + scene->atmosphere.disable(); + scene->setUseAtmosphereSkybox(false); + if (const auto skybox = scene->getSkybox(); skybox != nullptr) { + skybox->cubemap.updateWithColors(colors); + skybox->show(); + } else { + scene->setSkybox( + Skybox::create(Cubemap::fromColors(colors, 64), *window)); + } ambient = {0.82f, 0.84f, 0.88f, 1.0f}; key = {1.0f, 0.97f, 0.9f, 1.0f}; rim = {0.52f, 0.65f, 0.88f, 1.0f}; @@ -5804,13 +5860,30 @@ bool Context::setMaterialPreviewEnvironment(int mode) { areaLights[1]->shineColor = rim; areaLights[1]->intensity = rimIntensity; window->setClearColor(background); +#ifdef METAL + window->resetPathTracingAccumulation(); +#endif - if (auto skybox = scene->getSkybox(); skybox != nullptr) { - skybox->cubemap.updateWithColors(colors); - } else { - scene->setSkybox( - Skybox::create(Cubemap::fromColors(colors, 32), *window)); + return true; +} + +bool Context::rotateMaterialPreview(float yawDelta, float pitchDelta) { + if (window == nullptr || !materialPreviewRuntime || objects.empty()) { + return false; + } + auto *sphere = dynamic_cast(objects.front().get()); + if (sphere == nullptr) { + return false; } + materialPreviewYaw = std::fmod(materialPreviewYaw + yawDelta, 360.0f); + materialPreviewPitch = + std::clamp(materialPreviewPitch + pitchDelta, -85.0f, 85.0f); + WindowActivationScope activeWindow(*window); + sphere->setRotation( + Rotation3d{materialPreviewPitch, materialPreviewYaw, 0.0f}); +#ifdef METAL + window->resetPathTracingAccumulation(); +#endif return true; } @@ -6569,11 +6642,11 @@ Context::~Context() { end(); } catch (...) { } + clearRenderTargets(window.get(), renderTargets); if (context != nullptr) { runtime::scripting::clearSceneBindings(context, scriptHost); editorRuntimeComponents.clear(); objects.clear(); - renderTargets.clear(); directionalLights.clear(); pointLights.clear(); spotlights.clear(); @@ -6838,7 +6911,7 @@ void Context::loadScene(Window &window, const json &sceneData) { editorDirectionalLights.clear(); editorLightSourceData.clear(); deletedObjectReferences.clear(); - renderTargets.clear(); + clearRenderTargets(&window, renderTargets); directionalLights.clear(); pointLights.clear(); spotlights.clear(); @@ -6884,6 +6957,12 @@ void Context::loadScene(Window &window, const json &sceneData) { continue; } + std::string name; + JSON_READ_STRING(targetData, "name", name); + if (name.empty()) { + continue; + } + std::unique_ptr target; const std::string normalizedType = normalizeToken(type); if (normalizedType == "multisampled") { @@ -6921,10 +7000,10 @@ void Context::loadScene(Window &window, const json &sceneData) { target->display(window); } - std::string name; - JSON_READ_STRING(targetData, "name", name); - if (name.empty()) { - continue; + if (auto existing = renderTargets.find(name); + existing != renderTargets.end()) { + window.removeRenderTarget(existing->second.get()); + window.removePreferencedObject(existing->second.get()); } renderTargets[name] = std::move(target); } diff --git a/runtime/lib/runtime.cpp b/runtime/lib/runtime.cpp index 0554a760..61dc9127 100644 --- a/runtime/lib/runtime.cpp +++ b/runtime/lib/runtime.cpp @@ -18,7 +18,21 @@ void RuntimeScene::initialize(Window &window) { } if (runtimeContext->materialPreviewRuntime) { - window.useDeferredRendering(); + if (runtimeContext->config.renderer == "pathtracing") { + window.enablePathTracing(); + window.configurePathTracing( + runtimeContext->config.pathTracingSamples, + runtimeContext->config.pathTracingBounces, + runtimeContext->config.pathTracingDenoising, + runtimeContext->config.pathTracingAccumulationFrames); + } else { + window.useDeferredRendering(); + } + if (runtimeContext->config.useUpscaling) { +#ifdef METAL + window.useMetalUpscaling(runtimeContext->config.upscalingRatio); +#endif + } return; } diff --git a/runtime/lib/scripting.cpp b/runtime/lib/scripting.cpp index c55fced9..e6378274 100644 --- a/runtime/lib/scripting.cpp +++ b/runtime/lib/scripting.cpp @@ -1036,6 +1036,10 @@ bool parseEnvironmentValue(JSContext *ctx, ScriptHost &host, JSValueConst value, prop = JS_GetPropertyStr(ctx, value, "lightBloom"); if (!JS_IsException(prop) && !JS_IsUndefined(prop) && JS_IsObject(prop) && !JS_IsNull(prop)) { + double threshold = out.lightBloom.threshold; + readNumberProperty(ctx, prop, "threshold", threshold); + out.lightBloom.threshold = static_cast(threshold); + double radius = out.lightBloom.radius; readNumberProperty(ctx, prop, "radius", radius); out.lightBloom.radius = static_cast(radius); @@ -8803,6 +8807,11 @@ JSValue jsCreateCheckerboardTexture(JSContext *ctx, JSValueConst, int argc, ctx, "Expected texture, width, height, check size, and two colors"); } + if (host->context != nullptr && host->context->window != nullptr && + state->texture->object != nullptr) { + host->context->window->removePreferencedObject( + state->texture->object.get()); + } *state->texture = Texture::createCheckerboard( static_cast(width), static_cast(height), static_cast(checkSize), color1, color2); @@ -8843,6 +8852,11 @@ JSValue jsCreateDoubleCheckerboardTexture(JSContext *ctx, JSValueConst, "check sizes, and three colors"); } + if (host->context != nullptr && host->context->window != nullptr && + state->texture->object != nullptr) { + host->context->window->removePreferencedObject( + state->texture->object.get()); + } *state->texture = Texture::createDoubleCheckerboard( static_cast(width), static_cast(height), static_cast(checkSizeBig), static_cast(checkSizeSmall), @@ -14978,8 +14992,10 @@ JSValue jsSetRotationQuaternion(JSContext *ctx, JSValueConst, int argc, void runtime::scripting::dumpExecution(JSContext *ctx) { JSValue exceptionVal = JS_GetException(ctx); + std::string report; const char *exceptionStr = JS_ToCString(ctx, exceptionVal); if (exceptionStr) { + report = exceptionStr; std::cout << BOLD << RED << "Script execution failed: " << RESET << YELLOW << exceptionStr << RESET << std::endl; JS_FreeCString(ctx, exceptionStr); @@ -14989,11 +15005,19 @@ void runtime::scripting::dumpExecution(JSContext *ctx) { if (!JS_IsUndefined(stack)) { const char *stackStr = JS_ToCString(ctx, stack); if (stackStr) { + if (!report.empty()) + report += '\n'; + report += stackStr; std::cerr << stackStr << "\n"; JS_FreeCString(ctx, stackStr); } } + if (auto *host = getHost(ctx); host != nullptr && host->context != nullptr && + host->context->errorReporter && !report.empty()) { + host->context->errorReporter(report); + } + JS_FreeValue(ctx, stack); JS_FreeValue(ctx, exceptionVal); } @@ -15068,6 +15092,11 @@ void runtime::scripting::clearSceneBindings(JSContext *ctx, ScriptHost &host) { host.springJoints.clear(); for (auto &[_, state] : host.textures) { + if (host.context != nullptr && host.context->window != nullptr && + state.texture != nullptr && state.texture->object != nullptr) { + host.context->window->removePreferencedObject( + state.texture->object.get()); + } JS_FreeValue(ctx, state.value); } host.textures.clear(); @@ -15083,6 +15112,12 @@ void runtime::scripting::clearSceneBindings(JSContext *ctx, ScriptHost &host) { host.skyboxes.clear(); for (auto &[_, state] : host.renderTargets) { + if (host.context != nullptr && host.context->window != nullptr && + state.renderTarget != nullptr) { + host.context->window->removeRenderTarget(state.renderTarget.get()); + host.context->window->removePreferencedObject( + state.renderTarget.get()); + } JS_FreeValue(ctx, state.value); } host.renderTargets.clear(); diff --git a/shaders/metal/deferred/light.frag.metal b/shaders/metal/deferred/light.frag.metal index a59d533f..ee297b2d 100644 --- a/shaders/metal/deferred/light.frag.metal +++ b/shaders/metal/deferred/light.frag.metal @@ -158,6 +158,7 @@ struct UBO { struct Environment { float rimLightIntensity; float3 rimLightColor; + float bloomThreshold; }; struct PushConstants { @@ -1534,7 +1535,7 @@ fragment main0_out main0( dot(out.FragColor.xyz, float3(0.2125999927520751953125, 0.715200006961822509765625, 0.072200000286102294921875)); - if (brightness > 0.75) { + if (brightness > environment.bloomThreshold) { out.BrightColor = float4(out.FragColor.xyz, 1.0); } else { out.BrightColor = float4(0.0, 0.0, 0.0, 1.0); diff --git a/shaders/metal/fullscreen.frag.metal b/shaders/metal/fullscreen.frag.metal index 27642eb9..bab92d6b 100644 --- a/shaders/metal/fullscreen.frag.metal +++ b/shaders/metal/fullscreen.frag.metal @@ -358,7 +358,6 @@ static inline __attribute__((always_inline)) float4 applyColorEffects(thread float4& color, constant PushConstants& _372, device EffectBuffer& _381, device EffectFloat1Buffer& _394, device EffectFloat2Buffer& _403, device EffectFloat3Buffer& _411, device EffectFloat4Buffer& _419, device EffectFloat5Buffer& _426, constant Uniforms& _849, device EffectFloat6Buffer& _1049, thread float4& gl_FragCoord) { ColorCorrection cc; - float3 _noise; for (int i = 0; i < _372.EffectCount; i++) { if (_381.Effects[i] == 0) @@ -410,16 +409,12 @@ float4 applyColorEffects(thread float4& color, constant PushConstants& _372, dev float amount = _394.EffectFloat1[i]; float3 seed = float3(gl_FragCoord.xy, _849.deltaTime * 100.0); float n = dot(seed, float3(12.98980045318603515625, 78.233001708984375, 45.16400146484375)); - _noise.x = fract(sin(n) * 43758.546875); - n = dot(seed, float3(93.9889984130859375, 67.345001220703125, 12.9890003204345703125)); - _noise.y = fract(sin(n) * 28001.123046875); - n = dot(seed, float3(39.34600067138671875, 11.1350002288818359375, 83.154998779296875)); - _noise.z = fract(sin(n) * 19283.45703125); - float3 grain = ((_noise - float3(0.5)) * 2.0) * amount; + float noise = fract(sin(n) * 43758.546875); + float grain = ((noise - 0.5) * 2.0) * amount; float luminance = dot(color.xyz, float3(0.2989999949932098388671875, 0.58700001239776611328125, 0.114000000059604644775390625)); float visibility = 1.0 - (abs(luminance - 0.5) * 0.5); float4 _1210 = color; - float3 _1212 = _1210.xyz + (grain * visibility); + float3 _1212 = _1210.xyz + float3(grain * visibility); color.x = _1212.x; color.y = _1212.y; color.z = _1212.z; diff --git a/shaders/metal/path_tracing/path.metal b/shaders/metal/path_tracing/path.metal index 3320bad0..c86ac3ae 100644 --- a/shaders/metal/path_tracing/path.metal +++ b/shaders/metal/path_tracing/path.metal @@ -133,9 +133,10 @@ struct SceneData { uint accumulationFrameLimit; float fireflyClamp; uint numEmissiveTriangles; + float bloomThreshold; }; -static_assert(sizeof(SceneData) == 144); +static_assert(sizeof(SceneData) == 160); static_assert(__builtin_offsetof(SceneData, atmosphereSunDirection) == 48); static_assert(__builtin_offsetof(SceneData, atmosphereSunIntensity) == 64); static_assert(__builtin_offsetof(SceneData, atmosphereSunColor) == 80); @@ -143,6 +144,7 @@ static_assert(__builtin_offsetof(SceneData, pixelStride) == 96); static_assert(__builtin_offsetof(SceneData, ambientColor) == 112); static_assert(__builtin_offsetof(SceneData, accumulationFrameLimit) == 132); static_assert(__builtin_offsetof(SceneData, numEmissiveTriangles) == 140); +static_assert(__builtin_offsetof(SceneData, bloomThreshold) == 144); float pow5(float x) { float x2 = x * x; @@ -1639,14 +1641,13 @@ kernel void main0(texture2d outTex [[texture(0)]], accumulatedMoment * accumulatedMoment, 0.0); - constexpr float bloomThreshold = 0.8; constexpr float bloomKnee = 0.35; float brightness = luminance(accum); - float soft = clamp(brightness - bloomThreshold + bloomKnee, 0.0, + float soft = clamp(brightness - sceneData.bloomThreshold + bloomKnee, 0.0, bloomKnee * 2.0); soft = soft * soft / max(bloomKnee * 4.0, 0.00001); - float contribution = max(brightness - bloomThreshold, soft) / + float contribution = max(brightness - sceneData.bloomThreshold, soft) / max(brightness, 0.00001); float3 brightColor = accum * contribution; float2 motion = previousUvValid ? uv - previousUv : float2(0.0); diff --git a/shaders/metal/path_tracing/path_denoise.metal b/shaders/metal/path_tracing/path_denoise.metal index 73987eb3..319c30e6 100644 --- a/shaders/metal/path_tracing/path_denoise.metal +++ b/shaders/metal/path_tracing/path_denoise.metal @@ -3,6 +3,7 @@ using namespace metal; struct DenoiseParameters { int stepWidth; + float bloomThreshold; }; kernel void main0(texture2d inputTexture [[texture(0)]], @@ -111,12 +112,11 @@ kernel void main0(texture2d inputTexture [[texture(0)]], } float3 result = mix(center, spatialResult, filterStrength); float brightness = dot(result, float3(0.2126, 0.7152, 0.0722)); - constexpr float bloomThreshold = 0.8; constexpr float bloomKnee = 0.35; - float soft = clamp(brightness - bloomThreshold + bloomKnee, 0.0, + float soft = clamp(brightness - parameters.bloomThreshold + bloomKnee, 0.0, bloomKnee * 2.0); soft = soft * soft / max(bloomKnee * 4.0, 0.00001); - float contribution = max(brightness - bloomThreshold, soft) / + float contribution = max(brightness - parameters.bloomThreshold, soft) / max(brightness, 0.00001); outputTexture.write(float4(result, 1.0), gid); brightTexture.write(float4(result * contribution, 1.0), gid); diff --git a/shaders/opengl/deferred/light.frag b/shaders/opengl/deferred/light.frag index 26901ff5..f80b7ee2 100644 --- a/shaders/opengl/deferred/light.frag +++ b/shaders/opengl/deferred/light.frag @@ -83,6 +83,7 @@ struct ShadowParameters { struct Environment { float rimLightIntensity; vec3 rimLightColor; + float bloomThreshold; }; uniform AmbientLight ambientLight; @@ -479,7 +480,7 @@ void main() { FragColor = vec4(finalColor, 1.0); float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722)); - if (brightness > 1.0) { + if (brightness > environment.bloomThreshold) { BrightColor = vec4(FragColor.rgb, 1.0); } else { BrightColor = vec4(0.0, 0.0, 0.0, 1.0); diff --git a/shaders/opengl/fullscreen.frag b/shaders/opengl/fullscreen.frag index f98d215f..31b90f4f 100644 --- a/shaders/opengl/fullscreen.frag +++ b/shaders/opengl/fullscreen.frag @@ -374,24 +374,14 @@ vec4 applyColorEffects(vec4 color) { vec3 seed = vec3(gl_FragCoord.xy, deltaTime * 100.0); - vec3 noise; - float n; - - n = dot(seed, vec3(12.9898, 78.233, 45.164)); - noise.r = fract(sin(n) * 43758.5453); - - n = dot(seed, vec3(93.989, 67.345, 12.989)); - noise.g = fract(sin(n) * 28001.1234); - - n = dot(seed, vec3(39.346, 11.135, 83.155)); - noise.b = fract(sin(n) * 19283.4567); - - vec3 grain = (noise - 0.5) * 2.0 * amount; + float n = dot(seed, vec3(12.9898, 78.233, 45.164)); + float noise = fract(sin(n) * 43758.5453); + float grain = (noise - 0.5) * 2.0 * amount; float luminance = dot(color.rgb, vec3(0.299, 0.587, 0.114)); float visibility = 1.0 - abs(luminance - 0.5) * 0.5; - color.rgb += grain * visibility; + color.rgb += vec3(grain * visibility); color.rgb = clamp(color.rgb, 0.0, 1.0); } } diff --git a/shaders/vulkan/deferred/light.frag b/shaders/vulkan/deferred/light.frag index 6d1fc011..bc13ffc1 100644 --- a/shaders/vulkan/deferred/light.frag +++ b/shaders/vulkan/deferred/light.frag @@ -95,6 +95,7 @@ struct ShadowParameters { layout(set = 1, binding = 1) uniform Environment { float rimLightIntensity; vec3 rimLightColor; + float bloomThreshold; } environment; layout(set = 4, binding = 0) buffer DirectionalLights { @@ -527,7 +528,7 @@ void main() { FragColor = vec4(finalColor, 1.0); float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722)); - if (brightness > 1.0) { + if (brightness > environment.bloomThreshold) { BrightColor = vec4(FragColor.rgb, 1.0); } else { BrightColor = vec4(0.0, 0.0, 0.0, 1.0); diff --git a/shaders/vulkan/fullscreen.frag b/shaders/vulkan/fullscreen.frag index 4e9962d6..543afc35 100644 --- a/shaders/vulkan/fullscreen.frag +++ b/shaders/vulkan/fullscreen.frag @@ -400,24 +400,14 @@ vec4 applyColorEffects(vec4 color) { vec3 seed = vec3(gl_FragCoord.xy, deltaTime * 100.0); - vec3 noise; - float n; - - n = dot(seed, vec3(12.9898, 78.233, 45.164)); - noise.r = fract(sin(n) * 43758.5453); - - n = dot(seed, vec3(93.989, 67.345, 12.989)); - noise.g = fract(sin(n) * 28001.1234); - - n = dot(seed, vec3(39.346, 11.135, 83.155)); - noise.b = fract(sin(n) * 19283.4567); - - vec3 grain = (noise - 0.5) * 2.0 * amount; + float n = dot(seed, vec3(12.9898, 78.233, 45.164)); + float noise = fract(sin(n) * 43758.5453); + float grain = (noise - 0.5) * 2.0 * amount; float luminance = dot(color.rgb, vec3(0.299, 0.587, 0.114)); float visibility = 1.0 - abs(luminance - 0.5) * 0.5; - color.rgb += grain * visibility; + color.rgb += vec3(grain * visibility); color.rgb = clamp(color.rgb, 0.0, 1.0); } } diff --git a/tests/path-tracing/.atlas/project-settings.ini b/tests/path-tracing/.atlas/project-settings.ini index 61978377..03e12d50 100644 --- a/tests/path-tracing/.atlas/project-settings.ini +++ b/tests/path-tracing/.atlas/project-settings.ini @@ -19,7 +19,7 @@ internalScale=67 pathTracingAccumulation=512 pathTracingBounces=8 pathTracingDenoising=true -pathTracingSamples=50 +pathTracingSamples=10 renderer=Path Tracing runCommand=atlas run project.atlas snapIncrement=0.5 diff --git a/tests/path-tracing/assets/materials/Emissive Ball.amat b/tests/path-tracing/assets/materials/Emissive Ball.amat index 86fe7044..0383a580 100644 --- a/tests/path-tracing/assets/materials/Emissive Ball.amat +++ b/tests/path-tracing/assets/materials/Emissive Ball.amat @@ -1,16 +1,16 @@ { "material": { "albedo": [ - 0.8, - 0.8, - 0.8, + 0.8419623374938965, + 0.8796826004981995, + 0.8955062031745911, 1 ], "ao": 1, "emissiveColor": [ 1, - 1, - 1, + 0.9635767340660095, + 0.2377660721540451, 1 ], "emissiveIntensity": 10, @@ -18,7 +18,7 @@ "metallic": 0, "normalMapStrength": 1, "reflectivity": 0.5, - "roughness": 1.0, + "roughness": 1, "textureOffset": [ 0, 0 diff --git a/tests/path-tracing/main.ascene b/tests/path-tracing/main.ascene index de3cfac1..8dcef3a5 100644 --- a/tests/path-tracing/main.ascene +++ b/tests/path-tracing/main.ascene @@ -34,7 +34,10 @@ } }, "atmosphereSky": false, - "automaticAmbient": false + "automaticAmbient": false, + "lightBloom": { + "threshold": 2.108 + } }, "id": "main_scene", "lights": [], diff --git a/tests/path-tracing/project.atlas b/tests/path-tracing/project.atlas index d7240baf..8fcbc65f 100644 --- a/tests/path-tracing/project.atlas +++ b/tests/path-tracing/project.atlas @@ -20,7 +20,7 @@ default = "pathtracing" denoising = true global_illumination = false max_bounces = 8 -samples_per_pixel = 50 +samples_per_pixel = 10 upscaling_ratio = 0.67 use_upscaling = true