diff --git a/atlas/application/window.cpp b/atlas/application/window.cpp index 01f6a7db..c2296b50 100644 --- a/atlas/application/window.cpp +++ b/atlas/application/window.cpp @@ -924,6 +924,28 @@ computeShadowCasterSignature(const std::vector &shadowCasters) { return signature; } + +class RenderingContextScope { + public: + explicit RenderingContextScope(Window &window) + : previousWindow(Window::mainWindow), + previousDevice(opal::Device::globalInstance) { + window.activateRenderingContext(); + } + + ~RenderingContextScope() { + if (previousWindow != nullptr) { + previousWindow->activateRenderingContext(); + return; + } + Window::mainWindow = nullptr; + opal::Device::globalInstance = previousDevice; + } + + private: + Window *previousWindow; + opal::Device *previousDevice; +}; } // namespace Window::Window(const WindowConfiguration &config) @@ -1034,7 +1056,7 @@ Window::Window(const WindowConfiguration &config) this->setEditorControlsEnabled(config.editorControls); this->metalUpscalingRatio = this->renderScale; - Window::mainWindow = this; + activateRenderingContext(); float initialMouseX = 0.0f; float initialMouseY = 0.0f; @@ -1404,6 +1426,7 @@ void Window::pollEvents() { } bool Window::stepFrame() { + RenderingContextScope renderingContext(*this); this->initializeRunLoop(); if (this->shouldClose) { return false; @@ -1572,7 +1595,9 @@ bool Window::stepFrame() { DebugTimer gpuTimer("Gpu Data"); - renderLightsToShadowMaps(commandBuffer); + if (!this->usePathTracing) { + renderLightsToShadowMaps(commandBuffer); + } std::vector activeRenderTargets = this->renderTargets; bool usesModeScreenTarget = false; @@ -1632,8 +1657,13 @@ bool Window::stepFrame() { } #ifdef METAL pathTracer->resizeOutput(target->getWidth(), target->getHeight()); - pathTracer->render(commandBuffer, target->texture.texture, - target->brightTexture.texture); + if (!pathTracer->render(commandBuffer, target->texture.texture, + target->brightTexture.texture)) { + commandBuffer->beginPass(newRenderPass); + commandBuffer->clearColor(0.08f, 0.01f, 0.01f, 1.0f); + commandBuffer->clearDepth(1.0f); + commandBuffer->endPass(); + } #endif continue; @@ -1968,6 +1998,14 @@ bool Window::stepFrame() { return !this->shouldClose; } +void Window::activateRenderingContext() { + if (device != nullptr && device->context != nullptr) { + device->context->makeCurrent(); + } + Window::mainWindow = this; + opal::Device::globalInstance = device.get(); +} + void Window::resize(int width, int height, float scale) { const int clampedWidth = std::max(1, width); const int clampedHeight = std::max(1, height); @@ -1995,6 +2033,31 @@ void Window::resize(int width, int height, float scale) { device->getDefaultFramebuffer()->setViewport(0, 0, pixelWidth, pixelHeight); setViewportState(0, 0, pixelWidth, pixelHeight); + const int targetWidth = std::max( + 1, static_cast(pixelWidth * this->getRenderScale())); + const int targetHeight = std::max( + 1, static_cast(pixelHeight * this->getRenderScale())); + for (RenderTarget *target : renderTargets) { + if (target != nullptr && + (target->type == RenderTargetType::Scene || + target->type == RenderTargetType::Multisampled) && + (target->getWidth() != targetWidth || + target->getHeight() != targetHeight)) { + target->resize(*this); + } + } + const std::array *, 7> internalTargets = { + &gBuffer, &ssaoBuffer, &ssaoBlurBuffer, + &volumetricBuffer, &lightBuffer, &ssrFramebuffer, + &ssrHistoryFramebuffer}; + for (auto *target : internalTargets) { + if (target != nullptr && *target != nullptr) { + (*target)->resize(*this); + } + } + if (bloomBuffer != nullptr) { + bloomBuffer->destroy(); + } this->editorGridInitialized = false; this->shadowMapsDirty = true; this->ssaoMapsDirty = true; @@ -2301,8 +2364,13 @@ void Window::editorPointerEvent(int action, float x, float y, int button, updateEditorCameraDrag(x, y, effectiveScale); } else if (action == 2) { editorCameraDragging = false; - editorOrbitVelocityX *= 0.65f; - editorOrbitVelocityY *= 0.65f; + if (usePathTracing) { + editorOrbitVelocityX = 0.0f; + editorOrbitVelocityY = 0.0f; + } else { + editorOrbitVelocityX *= 0.65f; + editorOrbitVelocityY *= 0.65f; + } } return; } @@ -2370,8 +2438,12 @@ void Window::editorScrollEvent(float delta, float scale) { } applyEditorZoomDelta(scrollAmount); - editorZoomVelocity += scrollAmount * 0.01f; - editorZoomVelocity = std::clamp(editorZoomVelocity, -80.0f, 80.0f); + if (usePathTracing) { + editorZoomVelocity = 0.0f; + } else { + editorZoomVelocity += scrollAmount * 0.01f; + editorZoomVelocity = std::clamp(editorZoomVelocity, -80.0f, 80.0f); + } } void Window::editorKeyEvent(int key, bool pressed) { @@ -2888,8 +2960,13 @@ void Window::updateEditorCameraDrag(float x, float y, float scale) { float yawDelta = dx * 0.22f; float pitchDelta = -dy * 0.22f; applyEditorOrbitDelta(yawDelta, pitchDelta); - editorOrbitVelocityX = yawDelta * 45.0f; - editorOrbitVelocityY = pitchDelta * 45.0f; + if (usePathTracing) { + editorOrbitVelocityX = 0.0f; + editorOrbitVelocityY = 0.0f; + } else { + editorOrbitVelocityX = yawDelta * 45.0f; + editorOrbitVelocityY = pitchDelta * 45.0f; + } } void Window::updateEditorCameraPan(float x, float y, float scale) { @@ -3056,6 +3133,13 @@ void Window::updateEditorCameraInertia(float deltaTime) { return; } + if (usePathTracing) { + editorOrbitVelocityX = 0.0f; + editorOrbitVelocityY = 0.0f; + editorZoomVelocity = 0.0f; + return; + } + float dt = std::clamp(deltaTime, 1.0f / 240.0f, 1.0f / 30.0f); if (!editorCameraDragging) { if (std::abs(editorOrbitVelocityX) > 0.0001f || @@ -4729,7 +4813,9 @@ void Window::renderPingpong(RenderTarget *target) { blurPipeline->setUniform1i("image", 0); target->object->vao->bind(); - target->object->ebo->bind(); + if (target->object->ebo != nullptr) { + target->object->ebo->bind(); + } for (unsigned int i = 0; i < blurIterations; ++i) { this->pingpongFramebuffers.at(horizontal)->bind(); @@ -5544,4 +5630,27 @@ void Window::enablePathTracing() { this->pathTracer = std::make_shared(); pathTracer->init(); } + +bool Window::setEditorPathTracingPreview(bool enabled) { + if (pathTracer == nullptr) { + return false; + } + if (enabled) { + if (gBuffer == nullptr) { + useDeferredRendering(); + } else { + usePathTracing = false; + usesDeferred = true; + } + } else { + usesDeferred = false; + usePathTracing = true; + } + return true; +} + +const std::string &Window::getPathTracingError() const { + static const std::string noError; + return pathTracer != nullptr ? pathTracer->getLastError() : noError; +} #endif diff --git a/atlas/graphics/render_target.cpp b/atlas/graphics/render_target.cpp index 1bcea882..80d8d781 100644 --- a/atlas/graphics/render_target.cpp +++ b/atlas/graphics/render_target.cpp @@ -26,6 +26,7 @@ RenderTarget::RenderTarget(Window &window, RenderTargetType type, int resolution) { + creationResolution = resolution; atlas_log("Creating render target (type: " + std::to_string(static_cast(type)) + ")"); Size2d drawableSize = window.getSize(); @@ -472,29 +473,54 @@ RenderTarget::RenderTarget(Window &window, RenderTargetType type, packet.send(); } +void RenderTarget::resize(Window &window) { + if (type == RenderTargetType::Shadow || + type == RenderTargetType::CubeShadow) { + return; + } + auto displayedObject = object; + auto savedEffects = std::move(effects); + const RenderTargetType savedType = type; + RenderTarget replacement(window, savedType, creationResolution); + replacement.object = std::move(displayedObject); + replacement.effects = std::move(savedEffects); + *this = std::move(replacement); + if (object != nullptr) { + object->textures.clear(); + object->attachTexture(texture); + } +} + void RenderTarget::display(Window &window, float zindex) { if (object == nullptr) { CoreObject obj; std::vector vertices = { #ifdef METAL - {{1.0f, 1.0f, zindex}, Color::white(), {1.0f, 0.0f}}, // top right + {{1.0f, 1.0f, zindex}, Color::white(), {1.0f, 0.0f}}, + {{-1.0f, 1.0f, zindex}, Color::white(), {0.0f, 0.0f}}, + {{1.0f, -1.0f, zindex}, + Color::white(), + {1.0f, 1.0f}}, {{1.0f, -1.0f, zindex}, Color::white(), - {1.0f, 1.0f}}, // bottom right + {1.0f, 1.0f}}, + {{-1.0f, 1.0f, zindex}, Color::white(), {0.0f, 0.0f}}, {{-1.0f, -1.0f, zindex}, Color::white(), - {0.0f, 1.0f}}, // bottom left - {{-1.0f, 1.0f, zindex}, Color::white(), {0.0f, 0.0f}} // top left + {0.0f, 1.0f}} #else - // positions // texture coords - {{1.0f, 1.0f, zindex}, Color::white(), {1.0f, 1.0f}}, // top right + {{1.0f, 1.0f, zindex}, Color::white(), {1.0f, 1.0f}}, {{1.0f, -1.0f, zindex}, Color::white(), - {1.0f, 0.0f}}, // bottom right + {1.0f, 0.0f}}, + {{-1.0f, 1.0f, zindex}, Color::white(), {0.0f, 1.0f}}, + {{1.0f, -1.0f, zindex}, + Color::white(), + {1.0f, 0.0f}}, {{-1.0f, -1.0f, zindex}, Color::white(), - {0.0f, 0.0f}}, // bottom left - {{-1.0f, 1.0f, zindex}, Color::white(), {0.0f, 1.0f}} // top left + {0.0f, 0.0f}}, + {{-1.0f, 1.0f, zindex}, Color::white(), {0.0f, 1.0f}} #endif }; VertexShader vertexShader = @@ -506,15 +532,8 @@ void RenderTarget::display(Window &window, float zindex) { obj.createAndAttachProgram(vertexShader, fragmentShader); -#ifdef METAL - std::vector indices = {0, 3, 1, 1, 3, 2}; -#else - std::vector indices = {0, 1, 3, 1, 2, 3}; -#endif - obj.attachTexture(this->texture); obj.attachVertices(vertices); - obj.attachIndices(indices); obj.renderOnlyTexture(); obj.show(); obj.initialize(); @@ -762,8 +781,10 @@ void RenderTarget::render(float dt, renderTargetPipeline->setUniform1i("hasBrightTexture", blurredTexture.id != 0 ? 1 : 0); - uint depthTextureId = depthTexture.id; - bool hasDepth = depthTexture.id != 0; + const bool hasDepth = depthTexture.id != 0 && + (Window::mainWindow == nullptr || + !Window::mainWindow->usePathTracing); + uint depthTextureId = hasDepth ? depthTexture.id : 0; renderTargetPipeline->bindTexture2D("DepthTexture", depthTextureId, 2, obj->id); renderTargetPipeline->setUniform1i("hasDepthTexture", hasDepth ? 1 : 0); @@ -917,14 +938,8 @@ void RenderTarget::render(float dt, commandBuffer->bindDrawingState(obj->vao); commandBuffer->bindPipeline(renderTargetPipeline); - if (!obj->indices.empty()) { - commandBuffer->drawIndexed( - static_cast(obj->indices.size()), 1, 0, 0, 0, - obj->id); - } else { - commandBuffer->draw(static_cast(obj->vertices.size()), 1, - 0, 0, obj->id); - } + commandBuffer->draw(static_cast(obj->vertices.size()), 1, 0, + 0, obj->id); commandBuffer->unbindDrawingState(); renderTargetPipeline->enableDepthTest(true); @@ -940,9 +955,8 @@ void RenderTarget::render(float dt, : 0; debugPacket.triangleCount = 2; debugPacket.vertexBufferSizeMb = - static_cast(sizeof(CoreVertex) * 4) / (1024.0f * 1024.0f); - debugPacket.indexBufferSizeMb = - static_cast(sizeof(Index) * 6) / (1024.0f * 1024.0f); + static_cast(sizeof(CoreVertex) * 6) / (1024.0f * 1024.0f); + debugPacket.indexBufferSizeMb = 0.0f; debugPacket.textureCount = 1 + (brightTexture.id != 0 ? 1 : 0) + (depthTexture.id != 0 ? 1 : 0) + (gPosition.id != 0 ? 1 : 0) + diff --git a/atlas/graphics/texture.cpp b/atlas/graphics/texture.cpp index 9ef7c1e5..38b63958 100644 --- a/atlas/graphics/texture.cpp +++ b/atlas/graphics/texture.cpp @@ -246,9 +246,11 @@ Texture Texture::fromResource(const Resource& resource, TextureType type, dataFormat = opal::TextureDataFormat::Rgba; } + const uint mipLevels = 1u + static_cast(std::floor(std::log2( + std::max(width, height)))); opalTexture = opal::Texture::create(opal::TextureType::Texture2D, internalFormat, - width, height, dataFormat, data, 1); + width, height, dataFormat, data, mipLevels); stbi_image_free(data); } else { @@ -286,9 +288,11 @@ Texture Texture::fromResource(const Resource& resource, TextureType type, dataFormat = opal::TextureDataFormat::Red; } + const uint mipLevels = 1u + static_cast(std::floor(std::log2( + std::max(width, height)))); opalTexture = opal::Texture::create(opal::TextureType::Texture2D, internalFormat, - width, height, dataFormat, data, 1); + width, height, dataFormat, data, mipLevels); stbi_image_free(data); } @@ -313,9 +317,12 @@ Texture Texture::fromResource(const Resource& resource, TextureType type, ? opal::TextureFilterMode::Nearest : opal::TextureFilterMode::Linear; }; + const opal::TextureFilterMode minFilter = + params.minifyingFilter == TextureFilteringMode::Nearest + ? opal::TextureFilterMode::NearestMipmapNearest + : opal::TextureFilterMode::LinearMipmapLinear; opalTexture->setParameters(toOpalWrap(params.wrappingModeS), - toOpalWrap(params.wrappingModeT), - toOpalFilter(params.minifyingFilter), + toOpalWrap(params.wrappingModeT), minFilter, toOpalFilter(params.magnifyingFilter)); if (params.wrappingModeS == TextureWrappingMode::ClampToBorder || diff --git a/atlas/object/model.cpp b/atlas/object/model.cpp index 6657b7cb..c50eea27 100644 --- a/atlas/object/model.cpp +++ b/atlas/object/model.cpp @@ -17,15 +17,25 @@ #include "atlas/window.h" #include "atlas/workspace.h" #include +#include #include #include #include +#include +#include #include +#include +#include +#include +#include #include #include +#include #include #include +#include #include +#include #include #include #include "stb/stb_image.h" @@ -75,6 +85,290 @@ float roughnessFromShininess(float shininess, float strength) { return saturate(std::sqrt(2.0f / (effectiveShininess + 2.0f))); } +struct ModelTextureJob { + std::string cacheKey; + std::string fullPath; + std::string filename; + std::string aoPath; + TextureType textureType = TextureType::Color; + ResourceType resourceType = ResourceType::Image; + int maximumDimension = 0; +}; + +struct DecodedModelTexture { + std::vector pixels; + std::vector ao; + int width = 0; + int height = 0; +}; + +struct DecodedTextureCacheHeader { + uint64_t magic = 0x41544C4153544558ULL; + uint32_t version = 1; + uint32_t width = 0; + uint32_t height = 0; + uint64_t pixelBytes = 0; + uint64_t aoBytes = 0; +}; + +uint64_t modelTextureCacheKey(const ModelTextureJob &job) { + uint64_t hash = 1469598103934665603ULL; + auto append = [&hash](const void *data, size_t size) { + const auto *bytes = static_cast(data); + for (size_t index = 0; index < size; ++index) { + hash ^= bytes[index]; + hash *= 1099511628211ULL; + } + }; + auto appendPath = [&](const std::string &path) { + append(path.data(), path.size()); + std::error_code error; + const auto size = std::filesystem::file_size(path, error); + if (!error) { + append(&size, sizeof(size)); + } + const auto timestamp = std::filesystem::last_write_time(path, error); + if (!error) { + const auto count = timestamp.time_since_epoch().count(); + append(&count, sizeof(count)); + } + }; + appendPath(job.fullPath); + appendPath(job.aoPath); + append(&job.maximumDimension, sizeof(job.maximumDimension)); + append(&job.textureType, sizeof(job.textureType)); + return hash; +} + +std::filesystem::path modelTextureCachePath(const ModelTextureJob &job) { + std::error_code error; + const auto temporaryDirectory = std::filesystem::temp_directory_path(error); + if (error) { + return {}; + } + return temporaryDirectory / "atlas-model-cache-v1" / + (std::to_string(modelTextureCacheKey(job)) + ".rgba"); +} + +std::optional +loadDecodedTextureCache(const ModelTextureJob &job) { + if (job.maximumDimension <= 0) { + return std::nullopt; + } + const std::filesystem::path path = modelTextureCachePath(job); + if (path.empty()) { + return std::nullopt; + } + std::ifstream input(path, std::ios::binary); + if (!input) { + return std::nullopt; + } + DecodedTextureCacheHeader header; + input.read(reinterpret_cast(&header), sizeof(header)); + const uint64_t expectedPixels = static_cast(header.width) * + static_cast(header.height) * 4; + const uint64_t expectedAo = static_cast(header.width) * + static_cast(header.height); + if (!input || header.magic != 0x41544C4153544558ULL || + header.version != 1 || header.width == 0 || header.height == 0 || + header.width > static_cast(job.maximumDimension) || + header.height > static_cast(job.maximumDimension) || + header.pixelBytes != expectedPixels || + (header.aoBytes != 0 && header.aoBytes != expectedAo)) { + return std::nullopt; + } + DecodedModelTexture decoded; + decoded.width = static_cast(header.width); + decoded.height = static_cast(header.height); + decoded.pixels.resize(header.pixelBytes); + decoded.ao.resize(header.aoBytes); + input.read(reinterpret_cast(decoded.pixels.data()), + static_cast(decoded.pixels.size())); + if (!decoded.ao.empty()) { + input.read(reinterpret_cast(decoded.ao.data()), + static_cast(decoded.ao.size())); + } + if (!input) { + return std::nullopt; + } + return decoded; +} + +void storeDecodedTextureCache(const ModelTextureJob &job, + const DecodedModelTexture &decoded) { + if (job.maximumDimension <= 0 || decoded.pixels.empty()) { + return; + } + const std::filesystem::path path = modelTextureCachePath(job); + if (path.empty()) { + return; + } + std::error_code error; + std::filesystem::create_directories(path.parent_path(), error); + if (error) { + return; + } + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output) { + return; + } + DecodedTextureCacheHeader header; + header.width = static_cast(decoded.width); + header.height = static_cast(decoded.height); + header.pixelBytes = decoded.pixels.size(); + header.aoBytes = decoded.ao.size(); + output.write(reinterpret_cast(&header), sizeof(header)); + output.write(reinterpret_cast(decoded.pixels.data()), + static_cast(decoded.pixels.size())); + if (!decoded.ao.empty()) { + output.write(reinterpret_cast(decoded.ao.data()), + static_cast(decoded.ao.size())); + } +} + +std::vector resizeModelTexture(const unsigned char *source, + int sourceWidth, int sourceHeight, + int channels, int targetWidth, + int targetHeight) { + std::vector resized(static_cast(targetWidth) * + targetHeight * channels); + const bool halfResolution = + sourceWidth == targetWidth * 2 && sourceHeight == targetHeight * 2; + for (int y = 0; y < targetHeight; ++y) { + const int sourceY = + std::min(sourceHeight - 1, + static_cast((static_cast(y) * sourceHeight) / + targetHeight)); + for (int x = 0; x < targetWidth; ++x) { + const int sourceX = std::min( + sourceWidth - 1, + static_cast((static_cast(x) * sourceWidth) / + targetWidth)); + const size_t sourceOffset = + (static_cast(sourceY) * sourceWidth + sourceX) * + channels; + const size_t targetOffset = + (static_cast(y) * targetWidth + x) * channels; + if (halfResolution) { + const size_t rightOffset = sourceOffset + channels; + const size_t lowerOffset = + sourceOffset + static_cast(sourceWidth) * channels; + const size_t lowerRightOffset = lowerOffset + channels; + for (int channel = 0; channel < channels; ++channel) { + const unsigned int total = + source[sourceOffset + channel] + + source[rightOffset + channel] + + source[lowerOffset + channel] + + source[lowerRightOffset + channel]; + resized[targetOffset + channel] = + static_cast((total + 2) / 4); + } + } else { + std::memcpy(resized.data() + targetOffset, + source + sourceOffset, + static_cast(channels)); + } + } + } + return resized; +} + +DecodedModelTexture decodeModelTexture(const ModelTextureJob &job) { + if (auto cached = loadDecodedTextureCache(job)) { + return std::move(*cached); + } + stbi_set_flip_vertically_on_load_thread(false); + int width = 0; + int height = 0; + int channels = 0; + std::unique_ptr source( + stbi_load(job.fullPath.c_str(), &width, &height, &channels, + STBI_rgb_alpha), + stbi_image_free); + if (source == nullptr || width <= 0 || height <= 0) { + return {}; + } + + const float scale = + job.maximumDimension > 0 + ? std::min(1.0f, static_cast(job.maximumDimension) / + static_cast(std::max(width, height))) + : 1.0f; + const int targetWidth = std::max(1, static_cast(width * scale)); + const int targetHeight = std::max(1, static_cast(height * scale)); + + DecodedModelTexture decoded; + decoded.width = targetWidth; + decoded.height = targetHeight; + if (targetWidth == width && targetHeight == height) { + const size_t byteCount = static_cast(width) * height * 4; + decoded.pixels.assign(source.get(), source.get() + byteCount); + } else { + decoded.pixels = resizeModelTexture(source.get(), width, height, 4, + targetWidth, targetHeight); + } + + if (!job.aoPath.empty()) { + int aoWidth = 0; + int aoHeight = 0; + int aoChannels = 0; + std::unique_ptr aoSource( + stbi_load(job.aoPath.c_str(), &aoWidth, &aoHeight, &aoChannels, + STBI_grey), + stbi_image_free); + if (aoSource != nullptr && aoWidth > 0 && aoHeight > 0) { + if (aoWidth == targetWidth && aoHeight == targetHeight) { + const size_t byteCount = + static_cast(aoWidth) * aoHeight; + decoded.ao.assign(aoSource.get(), aoSource.get() + byteCount); + } else { + decoded.ao = + resizeModelTexture(aoSource.get(), aoWidth, aoHeight, 1, + targetWidth, targetHeight); + } + } + } + storeDecodedTextureCache(job, decoded); + return decoded; +} + +Texture uploadModelTexture(const ModelTextureJob &job, + DecodedModelTexture decoded) { + if (decoded.pixels.empty() || decoded.width <= 0 || decoded.height <= 0) { + throw std::runtime_error("Failed to decode model texture"); + } + if (job.textureType == TextureType::PBRPack) { + const size_t pixelCount = + static_cast(decoded.width) * decoded.height; + for (size_t pixel = 0; pixel < pixelCount; ++pixel) { + decoded.pixels[pixel * 4] = + decoded.ao.empty() ? 255 : decoded.ao[pixel]; + } + } + + Resource resource = Workspace::get().createResource( + job.fullPath, job.filename, job.resourceType); + const opal::TextureFormat format = job.textureType == TextureType::Color + ? opal::TextureFormat::sRgba8 + : opal::TextureFormat::Rgba8; + const uint mipLevels = + 1u + static_cast(std::floor(std::log2( + std::max(decoded.width, decoded.height)))); + auto opalTexture = opal::Texture::create( + opal::TextureType::Texture2D, format, decoded.width, decoded.height, + opal::TextureDataFormat::Rgba, decoded.pixels.data(), mipLevels); + opalTexture->setParameters( + opal::TextureWrapMode::Repeat, opal::TextureWrapMode::Repeat, + opal::TextureFilterMode::LinearMipmapLinear, + opal::TextureFilterMode::Linear); + opalTexture->automaticallyGenerateMipmaps(); + return Texture{.resource = resource, + .creationData = {decoded.width, decoded.height, 4}, + .id = opalTexture->textureID, + .texture = opalTexture, + .type = job.textureType}; +} + void importMaterialProperties(aiMaterial *material, CoreObject &object) { aiColor4D baseColor; if (material->Get(AI_MATKEY_BASE_COLOR, baseColor) == AI_SUCCESS) { @@ -82,7 +376,8 @@ void importMaterialProperties(aiMaterial *material, CoreObject &object) { baseColor.a}; } else { aiColor3D diffuseColor; - if (material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuseColor) == AI_SUCCESS) { + if (material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuseColor) == + AI_SUCCESS) { object.material.albedo.r = diffuseColor.r; object.material.albedo.g = diffuseColor.g; object.material.albedo.b = diffuseColor.b; @@ -90,12 +385,17 @@ void importMaterialProperties(aiMaterial *material, CoreObject &object) { } float opacity = 1.0f; if (material->Get(AI_MATKEY_OPACITY, opacity) == AI_SUCCESS) { - object.material.albedo.a = saturate(opacity); + opacity = saturate(opacity); + if (std::abs(object.material.albedo.a - opacity) > 1e-5f) { + object.material.albedo.a = + saturate(object.material.albedo.a * opacity); + } } else { float transparency = 0.0f; if (material->Get(AI_MATKEY_TRANSPARENCYFACTOR, transparency) == AI_SUCCESS) { - object.material.albedo.a = saturate(1.0f - transparency); + object.material.albedo.a = saturate( + object.material.albedo.a * (1.0f - transparency)); } } @@ -183,8 +483,14 @@ void Model::loadModel( unsigned int importFlags = aiProcess_Triangulate | aiProcess_CalcTangentSpace | - aiProcess_JoinIdenticalVertices | aiProcess_ImproveCacheLocality | aiProcess_SortByPType | aiProcess_GenSmoothNormals; + std::string extension = resource.path.extension().string(); + std::transform(extension.begin(), extension.end(), extension.begin(), + [](unsigned char value) { return std::tolower(value); }); + if (extension != ".gltf" && extension != ".glb") { + importFlags |= + aiProcess_JoinIdenticalVertices | aiProcess_ImproveCacheLocality; + } const aiScene *scene = importer.ReadFile(resource.path.string(), importFlags); @@ -205,6 +511,8 @@ void Model::loadModel( // Texture cache to avoid loading the same texture multiple times std::unordered_map textureCache; + preloadMaterialTextures(scene, textureCache); + processNode(scene->mRootNode, scene, glm::mat4(1.0f), textureCache); if (progress) @@ -241,6 +549,145 @@ void Model::loadModel( // std::cout << "Total Triangles: " << totalTriangles << std::endl; } +void Model::preloadMaterialTextures( + const aiScene *scene, + std::unordered_map &textureCache) { + std::vector jobs; + std::unordered_set scheduled; + + auto queueTextures = [&](aiMaterial *material, aiTextureType sourceType, + const std::string &typeName, + TextureType textureType) { + for (unsigned int index = 0; + index < material->GetTextureCount(sourceType); ++index) { + aiString path; + if (material->GetTexture(sourceType, index, &path) != AI_SUCCESS) { + continue; + } + const std::string filename = path.C_Str(); + const std::string fullPath = directory + "/" + filename; + std::string aoPath; + std::string cacheKey = fullPath + "|" + typeName; + if (textureType == TextureType::PBRPack) { + aiString aoTexturePath; + if (material->GetTexture(aiTextureType_AMBIENT_OCCLUSION, 0, + &aoTexturePath) == AI_SUCCESS) { + aoPath = + directory + "/" + std::string(aoTexturePath.C_Str()); + cacheKey += "|" + std::string(aoTexturePath.C_Str()); + } + } + if (!scheduled.insert(cacheKey).second) { + continue; + } + jobs.push_back( + ModelTextureJob{.cacheKey = std::move(cacheKey), + .fullPath = fullPath, + .filename = filename, + .aoPath = std::move(aoPath), + .textureType = textureType, + .resourceType = typeName == "texture_specular" + ? ResourceType::SpecularMap + : ResourceType::Image}); + } + }; + + for (unsigned int materialIndex = 0; materialIndex < scene->mNumMaterials; + ++materialIndex) { + aiMaterial *material = scene->mMaterials[materialIndex]; + if (material->GetTextureCount(aiTextureType_BASE_COLOR) > 0) { + queueTextures(material, aiTextureType_BASE_COLOR, "texture_diffuse", + TextureType::Color); + } else if (material->GetTextureCount(aiTextureType_DIFFUSE) > 0) { + queueTextures(material, aiTextureType_DIFFUSE, "texture_diffuse", + TextureType::Color); + } else { + queueTextures(material, aiTextureType_AMBIENT, "texture_diffuse", + TextureType::Color); + } + + queueTextures(material, aiTextureType_SPECULAR, "texture_specular", + TextureType::Specular); + if (material->GetTextureCount(aiTextureType_NORMALS) > 0) { + queueTextures(material, aiTextureType_NORMALS, "texture_normal", + TextureType::Normal); + } else if (material->GetTextureCount(aiTextureType_HEIGHT) > 0) { + queueTextures(material, aiTextureType_HEIGHT, "texture_normal", + TextureType::Normal); + } else { + queueTextures(material, aiTextureType_DISPLACEMENT, + "texture_normal", TextureType::Normal); + } + + if (material->GetTextureCount(aiTextureType_GLTF_METALLIC_ROUGHNESS) > + 0) { + queueTextures(material, aiTextureType_GLTF_METALLIC_ROUGHNESS, + "texture_pbr_pack", TextureType::PBRPack); + } else { + queueTextures(material, aiTextureType_METALNESS, "texture_metallic", + TextureType::Metallic); + queueTextures(material, aiTextureType_DIFFUSE_ROUGHNESS, + "texture_roughness", TextureType::Roughness); + if (material->GetTextureCount(aiTextureType_AMBIENT_OCCLUSION) > + 0) { + queueTextures(material, aiTextureType_AMBIENT_OCCLUSION, + "texture_ao", TextureType::AO); + } else { + queueTextures(material, aiTextureType_LIGHTMAP, "texture_ao", + TextureType::AO); + } + } + queueTextures(material, aiTextureType_OPACITY, "texture_opacity", + TextureType::Opacity); + } + + const size_t workerCount = + std::clamp(std::thread::hardware_concurrency(), 2, 6); + if (jobs.size() >= 48) { + for (auto &job : jobs) { + job.maximumDimension = 2048; + } + } + for (size_t batchStart = 0; batchStart < jobs.size(); + batchStart += workerCount) { + const size_t batchEnd = std::min(jobs.size(), batchStart + workerCount); + std::vector> futures; + futures.reserve(batchEnd - batchStart); + for (size_t index = batchStart; index < batchEnd; ++index) { + futures.push_back( + std::async(std::launch::async, [job = jobs[index]] { + return decodeModelTexture(job); + })); + } + for (size_t index = batchStart; index < batchEnd; ++index) { + try { + while (futures[index - batchStart].wait_for( + std::chrono::milliseconds(16)) != + std::future_status::ready) { + if (importProgress && !jobs.empty()) { + const float completed = + static_cast(index) / jobs.size(); + importProgress(0.88f + completed * 0.08f, + "Loading model textures"); + } + } + DecodedModelTexture decoded = futures[index - batchStart].get(); + textureCache[jobs[index].cacheKey] = + uploadModelTexture(jobs[index], std::move(decoded)); + } catch (const std::exception &error) { + atlas_warning("Failed to preload texture '" + + jobs[index].filename + "': " + error.what()); + } + if (importProgress && !jobs.empty()) { + const float completed = + static_cast(index + 1) / jobs.size(); + importProgress(0.88f + completed * 0.08f, + "Loading model textures"); + } + } + } +} + void Model::processNode( aiNode *node, const aiScene *scene, glm::mat4 parentTransform, std::unordered_map &textureCache) { @@ -256,7 +703,7 @@ void Model::processNode( if (importProgress && totalMeshCount > 0) { const float completed = static_cast(importedMeshCount) / static_cast(totalMeshCount); - importProgress(0.88f + completed * 0.11f, + importProgress(0.96f + completed * 0.03f, "Loading meshes and materials"); } } @@ -385,15 +832,15 @@ Model::processMesh(aiMesh *mesh, const aiScene *scene, aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex]; importMaterialProperties(material, object); - auto diffuseMaps = - loadMaterialTextures(material, std::any(aiTextureType_DIFFUSE), - "texture_diffuse", textureCache); + auto diffuseMaps = loadMaterialTextures( + material, std::any(aiTextureType_BASE_COLOR), "texture_diffuse", + textureCache); if (diffuseMaps.empty()) { - auto baseColorMaps = loadMaterialTextures( - material, std::any(aiTextureType_BASE_COLOR), "texture_diffuse", + auto legacyDiffuseMaps = loadMaterialTextures( + material, std::any(aiTextureType_DIFFUSE), "texture_diffuse", textureCache); - diffuseMaps.insert(diffuseMaps.end(), baseColorMaps.begin(), - baseColorMaps.end()); + diffuseMaps.insert(diffuseMaps.end(), legacyDiffuseMaps.begin(), + legacyDiffuseMaps.end()); } if (diffuseMaps.empty()) { auto ambientMaps = @@ -437,8 +884,8 @@ Model::processMesh(aiMesh *mesh, const aiScene *scene, if (pbrPackMaps.empty()) { auto metallicMaps = loadMaterialTextures( - material, std::any(aiTextureType_METALNESS), - "texture_metallic", textureCache); + material, std::any(aiTextureType_METALNESS), "texture_metallic", + textureCache); textures.insert(textures.end(), metallicMaps.begin(), metallicMaps.end()); @@ -466,6 +913,14 @@ Model::processMesh(aiMesh *mesh, const aiScene *scene, auto opacityMaps = loadMaterialTextures(material, std::any(aiTextureType_OPACITY), "texture_opacity", textureCache); + aiString alphaMode; + if (opacityMaps.empty() && !diffuseMaps.empty() && + material->Get(AI_MATKEY_GLTF_ALPHAMODE, alphaMode) == AI_SUCCESS && + std::string(alphaMode.C_Str()) != "OPAQUE") { + Texture opacityMap = diffuseMaps.front(); + opacityMap.type = TextureType::Opacity; + opacityMaps.push_back(std::move(opacityMap)); + } textures.insert(textures.end(), opacityMaps.begin(), opacityMaps.end()); } @@ -574,15 +1029,16 @@ std::vector Model::loadMaterialTextures( const std::string fullAoPath = directory + "/" + std::string(aoPath.C_Str()); std::unique_ptr - aoData(stbi_load(fullAoPath.c_str(), &aoWidth, &aoHeight, - &aoChannels, STBI_grey), + aoData(stbi_load(fullAoPath.c_str(), &aoWidth, + &aoHeight, &aoChannels, STBI_grey), stbi_image_free); if (aoData != nullptr && aoWidth > 0 && aoHeight > 0) { for (int y = 0; y < height; y++) { const int aoY = y * aoHeight / height; for (int x = 0; x < width; x++) { const int aoX = x * aoWidth / width; - data.get()[(static_cast(y) * width + x) * + data.get()[(static_cast(y) * width + + x) * 4] = aoData.get()[static_cast(aoY) * aoWidth + @@ -592,14 +1048,17 @@ std::vector Model::loadMaterialTextures( } } + const uint mipLevels = + 1u + static_cast(std::floor(std::log2( + std::max(width, height)))); auto opalTexture = opal::Texture::create( opal::TextureType::Texture2D, opal::TextureFormat::Rgba8, - width, height, opal::TextureDataFormat::Rgba, data.get(), 1); - opalTexture->setParameters( - opal::TextureWrapMode::Repeat, - opal::TextureWrapMode::Repeat, - opal::TextureFilterMode::Linear, - opal::TextureFilterMode::Linear); + width, height, opal::TextureDataFormat::Rgba, data.get(), + mipLevels); + opalTexture->setParameters(opal::TextureWrapMode::Repeat, + opal::TextureWrapMode::Repeat, + opal::TextureFilterMode::LinearMipmapLinear, + opal::TextureFilterMode::Linear); opalTexture->automaticallyGenerateMipmaps(); loadedTexture = Texture{.resource = resource, .creationData = {width, height, 4}, diff --git a/editor/views/editor/editor.cpp b/editor/views/editor/editor.cpp index 2486a361..70954176 100644 --- a/editor/views/editor/editor.cpp +++ b/editor/views/editor/editor.cpp @@ -84,6 +84,7 @@ #include "editor/views/postProcessing.h" #include "editor/views/viewport.h" #include "editor/views/viewportTools.h" +#include "editor/views/splashScreen.h" namespace { constexpr int DockStateVersion = 9; @@ -613,6 +614,30 @@ void EditorWindow::setupDocks() { : "Runtime unavailable"); emit startupReady(success, message); }); + connect(viewportPanel, &ViewportPanel::runtimeLoadingStarted, this, + [this] { + if (!startupComplete || assetLoadingSplash != nullptr) { + return; + } + assetLoadingSplash = new SplashScreen(this); + assetLoadingSplash->start("Loading assets..."); + }); + connect(viewportPanel, &ViewportPanel::runtimeLoadingStatusChanged, this, + [this](const QString &status) { + emit startupStatusChanged(status); + if (assetLoadingSplash != nullptr) { + assetLoadingSplash->setStatus(status); + } + }); + connect(viewportPanel, &ViewportPanel::runtimeLoadingFinished, this, + [this] { + if (assetLoadingSplash == nullptr) { + return; + } + assetLoadingSplash->finish(); + assetLoadingSplash->deleteLater(); + assetLoadingSplash = nullptr; + }); viewportTools = new ViewportTools(viewportPanel, projectFile); materialEditorPanel = new MaterialEditorPanel(viewportPanel); postProcessingPanel = new PostProcessingPanel(viewportPanel); diff --git a/editor/views/editor/hierarchy.cpp b/editor/views/editor/hierarchy.cpp index 4d8b9538..9865eea6 100644 --- a/editor/views/editor/hierarchy.cpp +++ b/editor/views/editor/hierarchy.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -23,15 +24,18 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include #include #include +#include #include #include @@ -41,6 +45,45 @@ namespace { constexpr int ObjectIdRole = Qt::UserRole + 1; constexpr int ObjectTypeRole = Qt::UserRole + 2; constexpr int AssetPathRole = Qt::UserRole + 3; +constexpr int CreationTypeRole = Qt::UserRole + 4; +constexpr int CreationNameRole = Qt::UserRole + 5; +constexpr int CreationCategoryRole = Qt::UserRole + 6; +constexpr int NoCreationResultsRole = Qt::UserRole + 7; + +struct CreationEntry { + QString category; + QString name; + QString type; +}; + +const QList &creationEntries() { + static const QList entries = { + {"3D Object", "Cube", "cube"}, + {"3D Object", "Sphere", "sphere"}, + {"3D Object", "Plane", "plane"}, + {"3D Object", "Pyramid", "pyramid"}, + {"3D Object", "Capsule", "capsule"}, + {"3D Object", "Terrain", "terrain"}, + {"Light", "Point Light", "pointLight"}, + {"Light", "Spot Light", "spotLight"}, + {"Light", "Directional Light", "directionalLight"}, + {"Light", "Area Light", "areaLight"}, + {"Light", "Ambient Light", "ambientLight"}, + {"Scene", "Empty Object", "group"}, + {"Scene", "Camera", "camera"}, + {"Scene", "Particle Emitter", "particleEmitter"}, + }; + return entries; +} + +int objectCount(const QJsonArray &objects) { + int count = 0; + for (const QJsonValue &value : objects) { + ++count; + count += objectCount(value.toObject().value("children").toArray()); + } + return count; +} QIcon hierarchyIcon(QWidget *, const QString &type) { const QString normalized = type.toLower(); @@ -124,7 +167,7 @@ HierarchyPanel::HierarchyPanel(ViewportPanel *viewport, QWidget *parent) model = new QStandardItemModel(this); treeView->setModel(model); treeView->setHeaderHidden(true); - treeView->setAnimated(true); + treeView->setAnimated(false); treeView->setEditTriggers(QAbstractItemView::NoEditTriggers); treeView->setSelectionMode(QAbstractItemView::ExtendedSelection); treeView->setSelectionBehavior(QAbstractItemView::SelectRows); @@ -256,6 +299,7 @@ HierarchyPanel::HierarchyPanel(ViewportPanel *viewport, QWidget *parent) if (viewport != nullptr) { addButton->setEnabled(false); moreButton->setEnabled(false); + treeView->setEnabled(false); connect(viewport, &ViewportPanel::sceneSnapshotChanged, this, &HierarchyPanel::applySceneSnapshot); connect(viewport, &ViewportPanel::runtimeAvailabilityChanged, this, @@ -263,7 +307,22 @@ HierarchyPanel::HierarchyPanel(ViewportPanel *viewport, QWidget *parent) addButton->setEnabled(available); moreButton->setEnabled(available); treeView->setEnabled(available); + if (!available) { + lastStructureSignature.clear(); + return; + } + QTimer::singleShot(0, this, [this] { + const QString snapshot = + this->viewport->currentSceneSnapshot(); + if (!snapshot.isEmpty()) + applySceneSnapshot(snapshot); + }); }); + QTimer::singleShot(0, this, [this] { + const QString snapshot = this->viewport->currentSceneSnapshot(); + if (!snapshot.isEmpty()) + applySceneSnapshot(snapshot); + }); } } @@ -282,7 +341,12 @@ void HierarchyPanel::applySceneSnapshot(const QString &snapshot) { const int selectedId = scene.value("selectedId").toInt(-1); const QString signature = sceneSignature(sceneName, objects, interfaces); - if (signature != lastStructureSignature) { + const bool incompleteModel = + model->rowCount() != 1 || itemsById.size() != objectCount(objects) || + !specialItems.contains("camera") || + !specialItems.contains("environment") || + !specialItems.contains("graphite"); + if (signature != lastStructureSignature || incompleteModel) { rebuildScene(sceneName, objects, interfaces, selectedId); lastStructureSignature = signature; return; @@ -308,6 +372,7 @@ void HierarchyPanel::applySceneSnapshot(const QString &snapshot) { treeView->setCurrentIndex(QModelIndex()); } applyingSnapshot = false; + treeView->viewport()->repaint(); } void HierarchyPanel::rebuildScene(const QString &sceneName, @@ -391,6 +456,8 @@ void HierarchyPanel::rebuildScene(const QString &sceneName, specialItems.value(selectedSpecialType)->index()); } applyingSnapshot = false; + treeView->doItemsLayout(); + treeView->viewport()->repaint(); } bool HierarchyPanel::eventFilter(QObject *watched, QEvent *event) { @@ -621,7 +688,97 @@ void HierarchyPanel::focusSearch() { } void HierarchyPanel::showCreationPopup() { - showAddObjectMenu(addButton->mapToGlobal(QPoint(0, addButton->height()))); + if (viewport == nullptr || !addButton->isEnabled()) + return; + + QDialog dialog(this); + dialog.setObjectName("commandPaletteDialog"); + dialog.setWindowTitle("Add Object"); + dialog.setWindowFlags(dialog.windowFlags() | Qt::FramelessWindowHint); + dialog.resize(620, 430); + auto *layout = new QVBoxLayout(&dialog); + auto *search = new QLineEdit(&dialog); + search->setObjectName("commandPaletteSearch"); + search->setPlaceholderText("Type an object to create…"); + auto *objects = new QListWidget(&dialog); + objects->setObjectName("commandPaletteList"); + layout->addWidget(search); + layout->addWidget(objects, 1); + + for (const CreationEntry &entry : creationEntries()) { + auto *item = new QListWidgetItem(objects); + item->setText(QStringLiteral("%1 · %2") + .arg(entry.name, entry.category)); + item->setIcon(hierarchyIcon(this, entry.type)); + item->setData(CreationTypeRole, entry.type); + item->setData(CreationNameRole, entry.name); + item->setData(CreationCategoryRole, entry.category); + } + if (objects->count() > 0) + objects->setCurrentRow(0); + + auto *noObjects = new QListWidgetItem("No matching objects", objects); + noObjects->setData(NoCreationResultsRole, true); + noObjects->setHidden(true); + connect(search, &QLineEdit::textChanged, &dialog, + [objects, noObjects](const QString &text) { + const QString query = text.trimmed(); + int firstMatch = -1; + for (int index = 0; index < objects->count(); ++index) { + QListWidgetItem *item = objects->item(index); + if (item == noObjects) + continue; + const QString searchable = + item->data(CreationNameRole).toString() + ' ' + + item->data(CreationCategoryRole).toString() + ' ' + + item->data(CreationTypeRole).toString(); + const bool matches = + query.isEmpty() || + searchable.contains(query, Qt::CaseInsensitive); + item->setHidden(!matches); + if (matches && firstMatch < 0) + firstMatch = index; + } + noObjects->setHidden(firstMatch >= 0); + objects->setCurrentItem(firstMatch >= 0 + ? objects->item(firstMatch) + : noObjects); + }); + connect(objects, &QListWidget::itemActivated, &dialog, + [this, &dialog](QListWidgetItem *item) { + if (item == nullptr || + item->data(NoCreationResultsRole).toBool()) + return; + const QString type = item->data(CreationTypeRole).toString(); + const QString name = item->data(CreationNameRole).toString(); + dialog.accept(); + createObject(type, name); + }); + connect(search, &QLineEdit::returnPressed, &dialog, [objects] { + if (objects->currentItem() != nullptr) + emit objects->itemActivated(objects->currentItem()); + }); + + auto moveSelection = [objects](int direction) { + if (objects->count() == 0) + return; + int row = objects->currentRow(); + for (int attempt = 0; attempt < objects->count(); ++attempt) { + row = (row + direction + objects->count()) % objects->count(); + if (!objects->item(row)->isHidden()) { + objects->setCurrentRow(row); + return; + } + } + }; + auto *down = new QShortcut(QKeySequence(Qt::Key_Down), &dialog); + auto *up = new QShortcut(QKeySequence(Qt::Key_Up), &dialog); + connect(down, &QShortcut::activated, &dialog, + [moveSelection] { moveSelection(1); }); + connect(up, &QShortcut::activated, &dialog, + [moveSelection] { moveSelection(-1); }); + search->setFocus(); + dialog.exec(); } QString HierarchyPanel::sceneSignature(const QString &sceneName, diff --git a/editor/views/editor/materialEditor.cpp b/editor/views/editor/materialEditor.cpp index a2948e5c..cbfd0467 100644 --- a/editor/views/editor/materialEditor.cpp +++ b/editor/views/editor/materialEditor.cpp @@ -3,10 +3,12 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -14,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -22,13 +25,14 @@ #include #include #include -#include +#include #include #include #include #include #include #include +#include #include #include #include @@ -36,10 +40,11 @@ #include #include #include +#include #include #include -#include +#include namespace { QColor jsonColor(const QJsonValue &value, const QColor &fallback) { @@ -108,253 +113,170 @@ double arrayValue(const QJsonValue &value, int index, double fallback) { return array.size() > index ? array.at(index).toDouble(fallback) : fallback; } -double channelAt(const QImage &image, double u, double v) { - if (image.isNull()) { - return 1.0; - } - const int x = - std::clamp(static_cast(u * image.width()), 0, image.width() - 1); - const int y = - std::clamp(static_cast(v * image.height()), 0, image.height() - 1); - return QColor::fromRgba(image.pixel(x, y)).lightnessF(); -} - -QColor imageAt(const QImage &image, double u, double v, - const QColor &fallback) { - if (image.isNull()) { - return fallback; - } - const int x = - std::clamp(static_cast(u * image.width()), 0, image.width() - 1); - const int y = - std::clamp(static_cast(v * image.height()), 0, image.height() - 1); - return QColor::fromRgba(image.pixel(x, y)); -} } // namespace class MaterialPreviewWidget : public QWidget { public: - explicit MaterialPreviewWidget(QWidget *parent = nullptr) - : QWidget(parent) { + explicit MaterialPreviewWidget(QString projectFile, + QWidget *parent = nullptr) + : QWidget(parent), projectFile(std::move(projectFile)) { setObjectName("materialPreview"); + setAttribute(Qt::WA_DontCreateNativeAncestors); + setAttribute(Qt::WA_NativeWindow); + setAttribute(Qt::WA_NoSystemBackground); + setAttribute(Qt::WA_OpaquePaintEvent); + setAttribute(Qt::WA_PaintOnScreen); + setAutoFillBackground(false); setMinimumSize(80, 80); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + frameTimer = new QTimer(this); + frameTimer->setSingleShot(true); + connect(frameTimer, &QTimer::timeout, this, + [this] { renderRuntime(); }); } + ~MaterialPreviewWidget() override { shutdownRuntime(); } + void setMaterial(const QJsonObject &next, const QString &nextBaseDir) { - material = next; + materialDefinition = + QJsonDocument(next).toJson(QJsonDocument::Compact); baseDir = nextBaseDir; - albedoImage = - loadTextureImage(baseDir, material.value("albedoTexture")); - normalImage = - loadTextureImage(baseDir, material.value("normalTexture")); - metallicImage = - loadTextureImage(baseDir, material.value("metallicTexture")); - roughnessImage = - loadTextureImage(baseDir, material.value("roughnessTexture")); - aoImage = loadTextureImage(baseDir, material.value("aoTexture")); - displacementImage = - loadTextureImage(baseDir, material.value("displacementTexture")); - textureScaleU = arrayValue(material.value("textureScale"), 0, 1.0); - textureScaleV = arrayValue(material.value("textureScale"), 1, 1.0); - textureOffsetU = arrayValue(material.value("textureOffset"), 0, 0.0); - textureOffsetV = arrayValue(material.value("textureOffset"), 1, 0.0); - update(); + if (runtimeContext != nullptr) { + runtimeContext->setMaterialPreviewMaterial( + materialDefinition.toStdString(), baseDir.toStdString()); + } + scheduleFrame(); } void setEnvironmentMode(int mode) { environmentMode = mode; - update(); + if (runtimeContext != nullptr) { + runtimeContext->setMaterialPreviewEnvironment(environmentMode); + } + scheduleFrame(); } protected: - void paintEvent(QPaintEvent *) override { - const qreal scale = devicePixelRatioF(); - const int widthPixels = std::max(1, static_cast(width() * scale)); - const int heightPixels = - std::max(1, static_cast(height() * scale)); - QImage rendered(widthPixels, heightPixels, QImage::Format_ARGB32); - rendered.setDevicePixelRatio(scale); - - const QColor albedo = - jsonColor(material.value("albedo"), QColor::fromRgbF(.8, .8, .8)); - const QColor emission = jsonColor(material.value("emissiveColor"), - QColor::fromRgbF(0, 0, 0)); - const double metallic = - std::clamp(material.value("metallic").toDouble(0.0), 0.0, 1.0); - const double roughness = - std::clamp(material.value("roughness").toDouble(0.5), 0.02, 1.0); - const double ao = - std::clamp(material.value("ao").toDouble(1.0), 0.0, 1.0); - const double reflectivity = - std::clamp(material.value("reflectivity").toDouble(0.5), 0.0, 1.0); - const double emissionStrength = - std::max(0.0, material.value("emissiveIntensity").toDouble(0.0)); - const double transmission = - std::clamp(material.value("transmittance").toDouble(0.0), 0.0, 1.0); - const double normalStrength = std::clamp( - material.value("normalMapStrength").toDouble(1.0), 0.0, 4.0); - const bool useNormal = material.value("useNormalMap").toBool(true) && - !normalImage.isNull(); - const double cx = widthPixels * 0.5; - const double cy = heightPixels * 0.5; - const double radius = std::min(widthPixels, heightPixels) * 0.39; - const double lx = -0.42; - const double ly = -0.55; - const double lz = 0.72; - - for (int y = 0; y < heightPixels; ++y) { - QRgb *line = reinterpret_cast(rendered.scanLine(y)); - for (int x = 0; x < widthPixels; ++x) { - const QColor background = environmentAt( - (static_cast(x) / widthPixels) * 2.0 - 1.0, - 1.0 - (static_cast(y) / heightPixels) * 2.0); - const double px = (x - cx) / radius; - const double py = (cy - y) / radius; - const double rr = px * px + py * py; - if (rr > 1.0) { - line[x] = background.rgba(); - continue; - } - - double nx = px; - double ny = py; - double nz = std::sqrt(std::max(0.0, 1.0 - rr)); - double u = - std::atan2(nx, nz) / (2.0 * std::numbers::pi_v)+0.5; - double v = 0.5 - std::asin(std::clamp(ny, -1.0, 1.0)) / - std::numbers::pi_v; - u = u * textureScaleU + textureOffsetU; - v = v * textureScaleV + textureOffsetV; - u -= std::floor(u); - v -= std::floor(v); - if (useNormal) { - const QColor sampled = - imageAt(normalImage, u, v, QColor(128, 128, 255)); - const double tx = sampled.redF() * 2.0 - 1.0; - const double ty = sampled.greenF() * 2.0 - 1.0; - nx += tx * normalStrength * 0.28; - ny += ty * normalStrength * 0.28; - const double length = - std::sqrt(nx * nx + ny * ny + nz * nz); - nx /= length; - ny /= length; - nz /= length; - } - - const QColor sampledAlbedo = - imageAt(albedoImage, u, v, QColor(255, 255, 255)); - const double localMetallic = std::clamp( - metallic * channelAt(metallicImage, u, v), 0.0, 1.0); - const double localRoughness = std::clamp( - roughness * channelAt(roughnessImage, u, v), 0.02, 1.0); - const double localAo = - std::clamp(ao * channelAt(aoImage, u, v), 0.0, 1.0); - const double diffuse = - std::max(0.0, nx * lx + ny * ly + nz * lz); - const double hx = lx; - const double hy = ly; - const double hz = lz + 1.0; - const double hlen = std::sqrt(hx * hx + hy * hy + hz * hz); - const double ndh = - std::max(0.0, (nx * hx + ny * hy + nz * hz) / hlen); - const double exponent = 4.0 + (1.0 - localRoughness) * - (1.0 - localRoughness) * - 252.0; - const double specular = std::pow(ndh, exponent) * - (0.12 + reflectivity * 0.88) * - (0.35 + localMetallic * 0.65); - const double fresnel = - std::pow(1.0 - std::clamp(nz, 0.0, 1.0), 5.0); - const double light = - localAo * 0.17 + diffuse * (0.83 - localMetallic * 0.38); - const double edgeTransmission = - transmission * (0.2 + fresnel * 0.55); - const double rx = 2.0 * nx * nz; - const double ry = 2.0 * ny * nz; - const QColor reflected = environmentAt(rx, ry); - const double reflectionWeight = - std::clamp(reflectivity * (0.12 + localMetallic * 0.88) * - (1.0 - localRoughness * 0.72) + - fresnel * 0.24, - 0.0, 0.92); - auto output = [&](double base, double texture, double emitted, - double environment, double behind) { - double surface = base * texture * light + specular + - fresnel * reflectivity * 0.18; - surface = surface * (1.0 - reflectionWeight) + - environment * reflectionWeight; - return std::clamp(surface * (1.0 - edgeTransmission) + - behind * edgeTransmission + - emitted * emissionStrength, - 0.0, 1.0); - }; - line[x] = qRgba( - static_cast(output(albedo.redF(), sampledAlbedo.redF(), - emission.redF(), reflected.redF(), - background.redF()) * - 255.0), - static_cast( - output(albedo.greenF(), sampledAlbedo.greenF(), - emission.greenF(), reflected.greenF(), - background.greenF()) * - 255.0), - static_cast(output(albedo.blueF(), - sampledAlbedo.blueF(), - emission.blueF(), reflected.blueF(), - background.blueF()) * - 255.0), - 255); + QPaintEngine *paintEngine() const override { return nullptr; } + + void showEvent(QShowEvent *event) override { + QWidget::showEvent(event); + scheduleFrame(); + } + + void hideEvent(QHideEvent *event) override { + frameTimer->stop(); + QWidget::hideEvent(event); + } + + void resizeEvent(QResizeEvent *event) override { + QWidget::resizeEvent(event); + scheduleFrame(); + } + + private: + void scheduleFrame() { + pendingFrames = std::max(pendingFrames, 2); + if (isVisible() && frameTimer != nullptr) { + frameTimer->start(0); + } + } + + void startRuntime() { + if (runtimeContext != nullptr || projectFile.isEmpty() || width() <= 1 || + height() <= 1 || materialDefinition.isEmpty()) { + return; + } +#ifdef METAL + try { + void *metalView = + reinterpret_cast(static_cast(winId())); + runtimeContext = runtime::makeMaterialPreviewContextForMetalView( + projectFile.toStdString(), metalView); + if (!runtimeContext->initializeMaterialPreview( + materialDefinition.toStdString(), baseDir.toStdString(), + environmentMode)) { + shutdownRuntime(); + return; } + resizeRuntime(); + } catch (const std::exception &error) { + qWarning().noquote() + << QStringLiteral("Failed to start runtime material preview: %1") + .arg(QString::fromUtf8(error.what())); + runtimeContext.reset(); + } +#endif + } + + void resizeRuntime() { + if (runtimeContext == nullptr) { + return; } + const float scale = + std::max(1.0f, static_cast(devicePixelRatioF())); + const int pixelWidth = + std::max(1, static_cast(std::round(width() * scale))); + const int pixelHeight = + std::max(1, static_cast(std::round(height() * scale))); + if (pixelWidth == runtimeWidth && pixelHeight == runtimeHeight) { + return; + } + runtimeContext->resize(width(), height(), scale); + runtimeWidth = pixelWidth; + runtimeHeight = pixelHeight; + } - QPainter painter(this); - painter.setRenderHint(QPainter::SmoothPixmapTransform); - painter.drawImage(rect(), rendered); + void renderRuntime() { + if (runtimeContext == nullptr) { + startRuntime(); + } + if (runtimeContext == nullptr) { + return; + } + try { + resizeRuntime(); + if (!runtimeContext->stepFrame()) { + shutdownRuntime(); + return; + } + pendingFrames = std::max(0, pendingFrames - 1); + if (pendingFrames > 0 && isVisible()) + frameTimer->start(1); + } catch (const std::exception &error) { + qWarning().noquote() + << QStringLiteral("Runtime material preview frame failed: %1") + .arg(QString::fromUtf8(error.what())); + shutdownRuntime(); + } } - private: - QColor environmentAt(double x, double y) const { - const double horizon = std::clamp((y + 1.0) * 0.5, 0.0, 1.0); - if (environmentMode == 1) { - const double sun = std::pow( - std::max(0.0, 1.0 - std::hypot(x + 0.38, y - 0.08)), 12.0); - return QColor::fromRgbF( - std::clamp(0.16 + horizon * 0.58 + sun, 0.0, 1.0), - std::clamp(0.07 + horizon * 0.27 + sun * 0.55, 0.0, 1.0), - std::clamp(0.12 + horizon * 0.24 + sun * 0.18, 0.0, 1.0)); + void shutdownRuntime() { + if (frameTimer != nullptr) { + frameTimer->stop(); + } + if (runtimeContext == nullptr) { + return; } - if (environmentMode == 2) { - const double cloud = - std::pow(std::max(0.0, std::sin(x * 8.0 + y * 3.0)), 6.0) * - 0.22; - return QColor::fromRgbF( - std::clamp(0.12 + horizon * 0.3 + cloud, 0.0, 1.0), - std::clamp(0.24 + horizon * 0.42 + cloud, 0.0, 1.0), - std::clamp(0.39 + horizon * 0.48 + cloud, 0.0, 1.0)); + auto context = std::move(runtimeContext); + try { + context->end(); + } catch (...) { } - const double strip = std::pow(std::max(0.0, 1.0 - std::abs(y)), 24.0); - const double panel = - std::pow(std::max(0.0, std::cos(x * 5.5)), 18.0) * 0.58; - const double value = 0.055 + horizon * 0.12 + strip * (0.34 + panel); - return QColor::fromRgbF(std::clamp(value * 0.92, 0.0, 1.0), - std::clamp(value * 0.98, 0.0, 1.0), - std::clamp(value, 0.0, 1.0)); + runtimeWidth = 0; + runtimeHeight = 0; + pendingFrames = 0; } - QJsonObject material; + QString projectFile; + QByteArray materialDefinition; QString baseDir; - QImage albedoImage; - QImage normalImage; - QImage metallicImage; - QImage roughnessImage; - QImage aoImage; - QImage displacementImage; - double textureScaleU = 1.0; - double textureScaleV = 1.0; - double textureOffsetU = 0.0; - double textureOffsetV = 0.0; + QTimer *frameTimer = nullptr; + std::shared_ptr runtimeContext; + int runtimeWidth = 0; + int runtimeHeight = 0; int environmentMode = 0; + int pendingFrames = 0; }; MaterialEditorPanel::MaterialEditorPanel(ViewportPanel *viewport, @@ -514,7 +436,9 @@ void MaterialEditorPanel::showMaterial() { auto *previewLayout = new QVBoxLayout(previewPane); previewLayout->setContentsMargins(0, 0, 5, 0); previewLayout->setSpacing(8); - preview = new MaterialPreviewWidget(previewPane); + preview = new MaterialPreviewWidget( + viewport != nullptr ? viewport->runtimeProjectFile() : QString(), + previewPane); preview->setMaterial(material, QFileInfo(materialPath).absolutePath()); auto *previewOptions = new QWidget(previewPane); auto *previewOptionsLayout = new QHBoxLayout(previewOptions); diff --git a/editor/views/editor/viewport.cpp b/editor/views/editor/viewport.cpp index 67d9971a..2244f074 100644 --- a/editor/views/editor/viewport.cpp +++ b/editor/views/editor/viewport.cpp @@ -137,8 +137,8 @@ QJsonObject findSnapshotObjectByName(const QJsonArray &objects, const QJsonObject object = entry.toObject(); if (object.value("name").toString() == name) return object; - const QJsonObject child = findSnapshotObjectByName( - object.value("children").toArray(), name); + const QJsonObject child = + findSnapshotObjectByName(object.value("children").toArray(), name); if (!child.isEmpty()) return child; } @@ -184,25 +184,27 @@ class RuntimePropertyCommand : public QUndoCommand { void undo() override { if (viewport != nullptr) { - viewport->applyRuntimeObjectProperty( - objectId, component, componentIndex, path, before); + viewport->applyRuntimeObjectProperty(objectId, component, + componentIndex, path, before); } } void redo() override { if (viewport != nullptr) { - viewport->applyRuntimeObjectProperty( - objectId, component, componentIndex, path, after); + viewport->applyRuntimeObjectProperty(objectId, component, + componentIndex, path, after); } } int id() const override { return 0x415450; } bool mergeWith(const QUndoCommand *other) override { - const auto *command = dynamic_cast(other); + const auto *command = + dynamic_cast(other); if (command == nullptr || command->viewport != viewport || command->objectId != objectId || command->component != component || - command->componentIndex != componentIndex || command->path != path) { + command->componentIndex != componentIndex || + command->path != path) { return false; } after = command->after; @@ -265,20 +267,22 @@ ViewportPanel::ViewportPanel(const QString &projectFile, QWidget *parent) 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] { stepRuntime(); }); - connect(resizeTimer, &QTimer::timeout, this, - [this] { resizeRuntime(); }); + connect(frameTimer, &QTimer::timeout, this, [this] { + if (stepRuntime() && isVisible()) + frameTimer->start(pathTracingPreview ? 16 : 1); + }); + 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(); }); } - } ViewportPanel::~ViewportPanel() { shutdownRuntime(); } @@ -298,7 +302,7 @@ void ViewportPanel::setRuntimeStartupEnabled(bool enabled) { void ViewportPanel::showEvent(QShowEvent *event) { QWidget::showEvent(event); if (runtimeContext != nullptr) { - frameTimer->start(16); + frameTimer->start(pathTracingPreview ? 16 : 1); return; } if (runtimeStartupEnabled) @@ -306,6 +310,7 @@ void ViewportPanel::showEvent(QShowEvent *event) { } void ViewportPanel::hideEvent(QHideEvent *event) { + frameTimer->stop(); QWidget::hideEvent(event); } @@ -323,12 +328,11 @@ void ViewportPanel::dragEnterEvent(QDragEnterEvent *event) { const bool model = suffix == "obj" || suffix == "fbx" || suffix == "gltf" || suffix == "glb" || suffix == "dae"; - if (model || - (selectedRuntimeObjectId() >= 0 && - (suffix == "amat" || suffix == "material" || suffix == "ts" || - suffix == "js" || suffix == "wav" || suffix == "mp3" || - suffix == "ogg" || suffix == "flac" || suffix == "m4a" || - suffix == "aac"))) { + if (model || (selectedRuntimeObjectId() >= 0 && + (suffix == "amat" || suffix == "material" || + suffix == "ts" || suffix == "js" || suffix == "wav" || + suffix == "mp3" || suffix == "ogg" || suffix == "flac" || + suffix == "m4a" || suffix == "aac"))) { event->acceptProposedAction(); return; } @@ -351,8 +355,7 @@ void ViewportPanel::dropEvent(QDropEvent *event) { const int objectId = selectedRuntimeObjectId(); if (objectId >= 0 && event->mimeData()->hasUrls() && attachRuntimeAsset( - objectId, - event->mimeData()->urls().constFirst().toLocalFile())) { + objectId, event->mimeData()->urls().constFirst().toLocalFile())) { event->acceptProposedAction(); emit runtimeObjectActivated(objectId); return; @@ -399,9 +402,8 @@ void ViewportPanel::shutdownRuntime() { void ViewportPanel::mousePressEvent(QMouseEvent *event) { setFocus(Qt::MouseFocusReason); - if (keyboardTransformActive && - (event->button() == Qt::LeftButton || - event->button() == Qt::RightButton)) { + if (keyboardTransformActive && (event->button() == Qt::LeftButton || + event->button() == Qt::RightButton)) { finishKeyboardTransform(event->button() == Qt::LeftButton); event->accept(); return; @@ -409,20 +411,18 @@ void ViewportPanel::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { leftPointerMoved = false; const int selected = selectedRuntimeObjectId(); - transformUndoBefore = - findSnapshotObject( - QJsonDocument::fromJson(lastSceneSnapshot.toUtf8()) - .object() - .value("objects") - .toArray(), - selected); + transformUndoBefore = findSnapshotObject( + QJsonDocument::fromJson(lastSceneSnapshot.toUtf8()) + .object() + .value("objects") + .toArray(), + selected); } int pointerButton = runtimeMouseButton(event->button()); if (event->button() == Qt::RightButton) { - rightDragRuntimeButton = - event->modifiers().testFlag(Qt::ShiftModifier) - ? runtimeMouseButton(Qt::RightButton) - : runtimeMouseButton(Qt::MiddleButton); + rightDragRuntimeButton = event->modifiers().testFlag(Qt::ShiftModifier) + ? runtimeMouseButton(Qt::RightButton) + : runtimeMouseButton(Qt::MiddleButton); pointerButton = rightDragRuntimeButton; } sendPointerEvent(0, static_cast(event->position().x()), @@ -436,10 +436,10 @@ void ViewportPanel::mousePressEvent(QMouseEvent *event) { void ViewportPanel::mouseMoveEvent(QMouseEvent *event) { if (event->buttons().testFlag(Qt::LeftButton)) leftPointerMoved = true; - sendPointerEvent(1, static_cast(event->position().x()), - static_cast(event->position().y()), - activeRuntimeMouseButton(event->buttons(), - rightDragRuntimeButton)); + sendPointerEvent( + 1, static_cast(event->position().x()), + static_cast(event->position().y()), + activeRuntimeMouseButton(event->buttons(), rightDragRuntimeButton)); if (keyboardTransformActive) { const QRect bounds(mapToGlobal(QPoint(0, 0)), size()); QPoint cursor = event->globalPosition().toPoint(); @@ -537,8 +537,7 @@ void ViewportPanel::keyPressEvent(QKeyEvent *event) { deleteRuntimeObject(selectedRuntimeObjectId()); event->accept(); return; - } else if (event->key() == Qt::Key_G || - event->key() == Qt::Key_R || + } else if (event->key() == Qt::Key_G || event->key() == Qt::Key_R || event->key() == Qt::Key_S) { beginKeyboardTransform(event->key() == Qt::Key_G ? 1 : event->key() == Qt::Key_R ? 2 @@ -605,12 +604,27 @@ void ViewportPanel::startRuntime() { } try { - runtimeContext = runtime::makeContextForMetalViewNonBlocking( - runtimeProjectFile, metalView); + emit runtimeLoadingStarted(); + emit runtimeLoadingStatusChanged("Loading assets..."); + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + runtimeContext = + runtime::makeContextForMetalView(runtimeProjectFile, metalView); + runtimeContext->modelImportProgress = [this]( + float value, + const std::string &status) { + const int percentage = std::clamp( + static_cast(std::round(value * 100.0f)), 0, 100); + emit runtimeLoadingStatusChanged( + QString::fromStdString(status) + + QStringLiteral("... %1%").arg(percentage)); + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + }; + runtimeContext->loadProject(); runtimeContext->setEditorControlsEnabled(true); runtimeContext->setEditorSimulationEnabled(false); runtimeContext->setEditorControlMode(0); runtimeContext->setEditorShadingMode(shadingMode); + runtimeContext->setEditorPathTracingPreview(pathTracingPreview); resizeRuntime(); refreshSceneSnapshot(); if (!selectionToRestore.isEmpty()) { @@ -631,8 +645,17 @@ void ViewportPanel::startRuntime() { emit cameraFocusChanged(false); playbackState = 0; emit playbackStateChanged(playbackState); - frameTimer->start(16); 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(pathTracingPreview ? 16 : 1); + emit runtimeLoadingFinished(); emit runtimeStartupFinished(true, {}); if (playAfterRuntimeStart) { playAfterRuntimeStart = false; @@ -646,12 +669,13 @@ void ViewportPanel::startRuntime() { .arg(QString::fromUtf8(error.what())); runtimeContext.reset(); playAfterRuntimeStart = false; - emit runtimeStartupFinished(false, - QString::fromUtf8(error.what())); + emit runtimeLoadingFinished(); + emit runtimeStartupFinished(false, QString::fromUtf8(error.what())); } catch (...) { qWarning() << "Failed to start Atlas viewport runtime"; runtimeContext.reset(); playAfterRuntimeStart = false; + emit runtimeLoadingFinished(); emit runtimeStartupFinished(false, "Runtime initialization failed"); } #else @@ -690,25 +714,28 @@ void ViewportPanel::stopRuntime() { runtimeScale = 0.0f; } -void ViewportPanel::stepRuntime() { +bool ViewportPanel::stepRuntime() { if (runtimeContext == nullptr) { - return; + return false; } try { if (!runtimeContext->stepFrame()) { stopRuntime(); - return; + return false; } refreshSceneSnapshot(); emit frameRateChanged(runtimeContext->frameRate()); + return true; } catch (const std::exception &error) { qWarning().noquote() << QStringLiteral("Atlas viewport runtime frame failed: %1") .arg(QString::fromUtf8(error.what())); stopRuntime(); + return false; } catch (...) { qWarning() << "Atlas viewport runtime frame failed"; stopRuntime(); + return false; } } @@ -791,12 +818,12 @@ bool ViewportPanel::focusRuntimeObjects(const QList &ids) { bool ViewportPanel::renameRuntimeObject(int id, const QString &name) { if (playbackState != 0) return false; - const QJsonObject object = findSnapshotObject( - QJsonDocument::fromJson(lastSceneSnapshot.toUtf8()) - .object() - .value("objects") - .toArray(), - id); + const QJsonObject object = + findSnapshotObject(QJsonDocument::fromJson(lastSceneSnapshot.toUtf8()) + .object() + .value("objects") + .toArray(), + id); const QString previous = object.value("name").toString(); if (previous.isEmpty() || previous == name) { return previous == name; @@ -816,13 +843,14 @@ bool ViewportPanel::renameRuntimeObjectDirect(int id, const QString &name) { return true; } -bool ViewportPanel::setRuntimeObjectProperty( - int id, const QString &component, int componentIndex, - const QString &propertyPath, const QJsonValue &value) { +bool ViewportPanel::setRuntimeObjectProperty(int id, const QString &component, + int componentIndex, + const QString &propertyPath, + const QJsonValue &value) { if (playbackState != 0) return false; - const QJsonValue previous = runtimeObjectProperty( - id, component, componentIndex, propertyPath); + const QJsonValue previous = + runtimeObjectProperty(id, component, componentIndex, propertyPath); if (previous.isUndefined()) { return applyRuntimeObjectProperty(id, component, componentIndex, propertyPath, value); @@ -835,9 +863,9 @@ bool ViewportPanel::setRuntimeObjectProperty( return true; } -bool ViewportPanel::setRuntimeSceneProperty( - const QString §ion, int index, const QString &propertyPath, - const QJsonValue &value) { +bool ViewportPanel::setRuntimeSceneProperty(const QString §ion, int index, + const QString &propertyPath, + const QJsonValue &value) { if (runtimeContext == nullptr || playbackState != 0 || section.isEmpty()) { return false; } @@ -848,9 +876,9 @@ bool ViewportPanel::setRuntimeSceneProperty( try { const json parsed = json::parse(payload.constData()); if (!parsed.is_array() || parsed.empty() || - !runtimeContext->setSceneProperty( - section.toStdString(), index, propertyPath.toStdString(), - parsed.front())) { + !runtimeContext->setSceneProperty(section.toStdString(), index, + propertyPath.toStdString(), + parsed.front())) { return false; } } catch (const json::exception &) { @@ -905,9 +933,10 @@ bool ViewportPanel::clearRuntimePropertySync(const QJsonObject &target) { return true; } -bool ViewportPanel::applyRuntimeObjectProperty( - int id, const QString &component, int componentIndex, - const QString &propertyPath, const QJsonValue &value) { +bool ViewportPanel::applyRuntimeObjectProperty(int id, const QString &component, + int componentIndex, + const QString &propertyPath, + const QJsonValue &value) { if (runtimeContext == nullptr || playbackState != 0) { return false; } @@ -932,8 +961,8 @@ bool ViewportPanel::applyRuntimeObjectProperty( return true; } -int ViewportPanel::addRuntimeObjectComponent( - int id, const QString &type, const QJsonObject &properties) { +int ViewportPanel::addRuntimeObjectComponent(int id, const QString &type, + const QJsonObject &properties) { if (runtimeContext == nullptr || playbackState != 0 || type.isEmpty()) { return -1; } @@ -1047,8 +1076,8 @@ bool ViewportPanel::pasteRuntimeObject() { objectClipboard.isEmpty()) { return false; } - const int id = runtimeContext->pasteObjectDefinition( - objectClipboard.toStdString()); + const int id = + runtimeContext->pasteObjectDefinition(objectClipboard.toStdString()); if (id < 0) return false; if (undoStack != nullptr) @@ -1111,7 +1140,8 @@ bool ViewportPanel::saveRuntimeSceneAs(const QString &path) { const QString source = currentRuntimeScene(); if (source.isEmpty()) return false; - if (QFileInfo(source).absoluteFilePath() != QFileInfo(path).absoluteFilePath()) { + if (QFileInfo(source).absoluteFilePath() != + QFileInfo(path).absoluteFilePath()) { if (QFile::exists(path) && !QFile::remove(path)) return false; if (!QFile::copy(source, path)) @@ -1176,14 +1206,13 @@ bool ViewportPanel::attachRuntimeAsset(int id, const QString &path) { bool ViewportPanel::importRuntimeModel(const QString &path) { if (runtimeContext == nullptr || playbackState != 0 || path.isEmpty()) return false; - const QJsonObject definition{ - {"type", "model"}, - {"name", QFileInfo(path).completeBaseName()}, - {"source", QFileInfo(path).absoluteFilePath()}, - {"position", QJsonArray{0.0, 0.0, 0.0}}, - {"rotation", QJsonArray{0.0, 0.0, 0.0}}, - {"scale", QJsonArray{1.0, 1.0, 1.0}}, - {"components", QJsonArray{}}}; + const QJsonObject definition{{"type", "model"}, + {"name", QFileInfo(path).completeBaseName()}, + {"source", QFileInfo(path).absoluteFilePath()}, + {"position", QJsonArray{0.0, 0.0, 0.0}}, + {"rotation", QJsonArray{0.0, 0.0, 0.0}}, + {"scale", QJsonArray{1.0, 1.0, 1.0}}, + {"components", QJsonArray{}}}; QProgressDialog progress(this); progress.setWindowTitle(tr("Importing Model")); progress.setLabelText(tr("Preparing %1…").arg(QFileInfo(path).fileName())); @@ -1199,13 +1228,12 @@ bool ViewportPanel::importRuntimeModel(const QString &path) { const int id = runtimeContext->pasteObjectDefinition( QJsonDocument(definition).toJson(QJsonDocument::Compact).toStdString(), [&progress, &fileName](float value, const std::string &status) { - const int percentage = - std::clamp(static_cast(std::round(value * 100.0f)), 0, 100); + const int percentage = std::clamp( + static_cast(std::round(value * 100.0f)), 0, 100); progress.setValue(percentage); - progress.setLabelText(QString::fromStdString(status) + - QObject::tr("\n%1 — %2%") - .arg(fileName) - .arg(percentage)); + progress.setLabelText( + QString::fromStdString(status) + + QObject::tr("\n%1 — %2%").arg(fileName).arg(percentage)); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); }); progress.setValue(100); @@ -1242,13 +1270,14 @@ void ViewportPanel::setSceneDirty(bool dirty) { emit sceneDirtyChanged(sceneDirty); } -QJsonValue ViewportPanel::runtimeObjectProperty( - int id, const QString &component, int componentIndex, - const QString &propertyPath) const { +QJsonValue +ViewportPanel::runtimeObjectProperty(int id, const QString &component, + int componentIndex, + const QString &propertyPath) const { const QJsonDocument document = QJsonDocument::fromJson(lastSceneSnapshot.toUtf8()); - const QJsonObject object = findSnapshotObject( - document.object().value("objects").toArray(), id); + const QJsonObject object = + findSnapshotObject(document.object().value("objects").toArray(), id); if (object.isEmpty()) return QJsonValue(QJsonValue::Undefined); const QString normalized = component.toLower(); @@ -1272,9 +1301,9 @@ void ViewportPanel::playRuntime() { return; } QString error; - if (!ToolchainInstaller::run( - {"script", "compile"}, QFileInfo(projectFile).absolutePath(), - &error)) { + if (!ToolchainInstaller::run({"script", "compile"}, + QFileInfo(projectFile).absolutePath(), + &error)) { QMessageBox::warning( this, "Script Compilation Failed", error.isEmpty() ? "Atlas could not compile the project scripts." @@ -1331,13 +1360,11 @@ void ViewportPanel::reloadRuntime() { if (selected >= 0) { const QJsonDocument document = QJsonDocument::fromJson(lastSceneSnapshot.toUtf8()); - selectionToRestore = findSnapshotObject( - document.object() - .value("objects") - .toArray(), - selected) - .value("name") - .toString(); + selectionToRestore = + findSnapshotObject(document.object().value("objects").toArray(), + selected) + .value("name") + .toString(); } } finishKeyboardTransform(false); @@ -1355,6 +1382,31 @@ void ViewportPanel::setRuntimeShadingMode(int mode) { } } +void ViewportPanel::setPathTracingPreview(bool enabled) { + pathTracingPreview = enabled; + if (runtimeContext == nullptr) { + return; + } + emit runtimeLoadingStarted(); + emit runtimeLoadingStatusChanged(enabled + ? "Preparing PBR preview..." + : "Preparing path-traced viewport..."); + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + runtimeContext->setEditorPathTracingPreview(enabled); + frameTimer->stop(); + const bool frameReady = stepRuntime(); + if (frameReady && isVisible()) + frameTimer->start(pathTracingPreview ? 16 : 1); + emit runtimeLoadingFinished(); + if (!enabled && runtimeContext != nullptr) { + const std::string error = runtimeContext->getPathTracingError(); + if (!error.empty()) { + QMessageBox::critical(this, "Path Tracing Error", + QString::fromStdString(error)); + } + } +} + void ViewportPanel::setRuntimeControlMode(int mode) { if (mode < 0 || mode > 3 || runtimeContext == nullptr) { return; @@ -1365,7 +1417,8 @@ void ViewportPanel::setRuntimeControlMode(int mode) { void ViewportPanel::toggleTransformSpace() { if (runtimeContext != nullptr) - emit transformSpaceChanged(runtimeContext->toggleEditorTransformSpace()); + emit transformSpaceChanged( + runtimeContext->toggleEditorTransformSpace()); } void ViewportPanel::toggleTransformSnapping() { @@ -1389,12 +1442,11 @@ void ViewportPanel::beginKeyboardTransform(int mode) { if (runtimeContext == nullptr || selectedRuntimeObjectId() < 0) return; transformUndoBefore = - findSnapshotObject( - QJsonDocument::fromJson(lastSceneSnapshot.toUtf8()) - .object() - .value("objects") - .toArray(), - selectedRuntimeObjectId()); + findSnapshotObject(QJsonDocument::fromJson(lastSceneSnapshot.toUtf8()) + .object() + .value("objects") + .toArray(), + selectedRuntimeObjectId()); const QPoint pointer = mapFromGlobal(QCursor::pos()); if (!runtimeContext->beginEditorKeyboardTransform( mode, static_cast(pointer.x()), @@ -1405,8 +1457,9 @@ void ViewportPanel::beginKeyboardTransform(int mode) { keyboardTransformMode = mode; keyboardTransformAxes = 7; grabMouse(); - const QString operation = - mode == 1 ? "Move" : mode == 2 ? "Rotate" : "Scale"; + const QString operation = mode == 1 ? "Move" + : mode == 2 ? "Rotate" + : "Scale"; emit transformHintChanged( QStringLiteral("%1 · All axes · X/Y/Z constrain · Shift+Axis exclude " "· Enter/LMB confirm · Esc/RMB cancel") @@ -1462,27 +1515,25 @@ void ViewportPanel::finishKeyboardTransform(bool commit) { keyboardTransformMode = 0; keyboardTransformAxes = 7; transformUndoBefore = {}; - emit transformHintChanged( - "Tab Frame · Right-Drag Pan · Middle-Drag Orbit · G Move · R Rotate · S Scale · X Delete"); + emit transformHintChanged("Tab Frame · Right-Drag Pan · Middle-Drag Orbit " + "· G Move · R Rotate · S Scale · X Delete"); } -void ViewportPanel::pushTransformUndo(int objectId, - const QJsonObject &before) { +void ViewportPanel::pushTransformUndo(int objectId, const QJsonObject &before) { if (undoStack == nullptr || objectId < 0 || before.isEmpty()) return; - const QJsonObject after = findSnapshotObject( - QJsonDocument::fromJson(lastSceneSnapshot.toUtf8()) - .object() - .value("objects") - .toArray(), - objectId); + const QJsonObject after = + findSnapshotObject(QJsonDocument::fromJson(lastSceneSnapshot.toUtf8()) + .object() + .value("objects") + .toArray(), + objectId); if (after.isEmpty()) return; auto *command = new QUndoCommand("Transform Object"); - const QList> properties{ - {"position", "/position"}, - {"rotation", "/rotation"}, - {"scale", "/scale"}}; + const QList> properties{{"position", "/position"}, + {"rotation", "/rotation"}, + {"scale", "/scale"}}; for (const auto &[key, path] : properties) { if (before.value(key) != after.value(key)) { new RuntimePropertyCommand(this, objectId, "transform", -1, path, diff --git a/editor/views/editor/viewportTools.cpp b/editor/views/editor/viewportTools.cpp index 980825a1..177f931e 100644 --- a/editor/views/editor/viewportTools.cpp +++ b/editor/views/editor/viewportTools.cpp @@ -4,11 +4,13 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -120,28 +122,49 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, tools->addWidget(reloadButton); tools->addStretch(); + QFile manifest(projectFile); + bool pathTracingProject = false; + 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)); + } + auto *shadingGroup = new QActionGroup(toolbar); shadingGroup->setExclusive(true); - const QStringList shadingNames{"Lit", "Wireframe", "Points"}; - const QList shadingIcons{ - styling::Icon::Sphere, styling::Icon::CubeTransparent, - styling::Icon::DotsNine}; + const QStringList shadingNames = + pathTracingProject + ? QStringList{"PBR Preview", "Path Traced"} + : QStringList{"Lit", "Wireframe", "Points"}; + const QList shadingIcons = + pathTracingProject + ? QList{styling::Icon::Sphere, + styling::Icon::Aperture} + : QList{styling::Icon::Sphere, + styling::Icon::CubeTransparent, + styling::Icon::DotsNine}; for (int index = 0; index < shadingNames.size(); ++index) { auto *button = new QToolButton(toolbar); button->setObjectName("viewportShadingButton"); - button->setToolButtonStyle(Qt::ToolButtonIconOnly); + button->setToolButtonStyle(pathTracingProject + ? Qt::ToolButtonTextBesideIcon + : Qt::ToolButtonIconOnly); button->setCheckable(true); auto *action = new QAction(shadingNames.at(index), button); - action->setIcon( - styling::icon(shadingIcons.at(index), "#9AA6B8")); - action->setToolTip(shadingNames.at(index) + " shading"); + action->setIcon(styling::icon(shadingIcons.at(index), "#9AA6B8")); + action->setToolTip(pathTracingProject + ? shadingNames.at(index) + : shadingNames.at(index) + " shading"); action->setCheckable(true); action->setData(index); button->setDefaultAction(action); shadingGroup->addAction(action); tools->addWidget(button); - if (index == 0) + if (index == 0) { action->setChecked(true); + } } auto *fpsButton = new QToolButton(toolbar); @@ -182,8 +205,13 @@ ViewportTools::ViewportTools(ViewportPanel *viewport, viewport->setRuntimeControlMode(action->data().toInt()); }); connect(shadingGroup, &QActionGroup::triggered, this, - [viewport](QAction *action) { - viewport->setRuntimeShadingMode(action->data().toInt()); + [viewport, pathTracingProject](QAction *action) { + if (pathTracingProject) { + viewport->setPathTracingPreview(action->data().toInt() == + 0); + } else { + viewport->setRuntimeShadingMode(action->data().toInt()); + } }); connect(spaceButton, &QToolButton::clicked, viewport, &ViewportPanel::toggleTransformSpace); diff --git a/include/atlas/core/default_shaders.h b/include/atlas/core/default_shaders.h index a8efab9e..d992fea7 100644 --- a/include/atlas/core/default_shaders.h +++ b/include/atlas/core/default_shaders.h @@ -6493,7 +6493,7 @@ vertex main0_out main0(main0_in in [[stage_in]], constant UBO& uniforms [[buffer float4 _57 = mvp * _56; out.gl_Position = _57; out.FragPos = float3((modelMatrix * float4(in.aPos, 1.0)).xyz); - out.TexCoord = float2(in.aTexCoord.x, 1.0 - in.aTexCoord.y); + out.TexCoord = in.aTexCoord; out.outColor = in.aColor; float3x3 normalMatrix = transpose(spvInverse3x3(float3x3(modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz))); out.Normal = fast::normalize(normalMatrix * in.aNormal); @@ -6650,16 +6650,18 @@ struct Material { float ior; float reflectivity; float _pad2; + packed_float2 textureScale; + packed_float2 textureOffset; }; -struct MeshData { - uint vertexOffset; - uint indexOffset; - uint _pad0; - uint _pad1; -}; +static_assert(sizeof(Material) == 112); +static_assert(__builtin_offsetof(Material, emissiveColor) == 32); +static_assert(__builtin_offsetof(Material, albedoTextureIndex) == 48); +static_assert(__builtin_offsetof(Material, transmittance) == 80); +static_assert(__builtin_offsetof(Material, textureScale) == 96); struct VertexData { + packed_float3 position; packed_float3 normal; packed_float2 uv; packed_float3 tangent; @@ -6731,9 +6733,18 @@ struct SceneData { float3 atmosphereSunDirection; float atmosphereSunIntensity; float3 atmosphereSunColor; - float _pad0; + uint pixelStride; + float3 ambientColor; + uint environmentEnabled; }; +static_assert(sizeof(SceneData) == 144); +static_assert(__builtin_offsetof(SceneData, atmosphereSunDirection) == 48); +static_assert(__builtin_offsetof(SceneData, atmosphereSunIntensity) == 64); +static_assert(__builtin_offsetof(SceneData, atmosphereSunColor) == 80); +static_assert(__builtin_offsetof(SceneData, pixelStride) == 96); +static_assert(__builtin_offsetof(SceneData, ambientColor) == 112); + float pow5(float x) { float x2 = x * x; return x2 * x2 * x; @@ -6741,6 +6752,12 @@ float pow5(float x) { float luminance(float3 c) { return dot(c, float3(0.2126, 0.7152, 0.0722)); } +float powerHeuristic(float pdfA, float pdfB) { + float a2 = pdfA * pdfA; + float b2 = pdfB * pdfB; + return a2 / max(a2 + b2, 1e-8); +} + float3 clampLuminance(float3 c, float maxL) { float l = luminance(c); if (l > maxL && l > 1e-6) { @@ -6780,6 +6797,16 @@ float3 skyColor(float3 dir, float intensity, texturecube skybox, sampleDir = float3(0.0, 1.0, 0.0); } float3 sky = skybox.sample(skyboxSampler, sampleDir).xyz; + if (sceneData.atmosphereEnabled != 0) { + float horizon = pow(clamp(1.0 - abs(sampleDir.y), 0.0, 1.0), 4.0); + float daylight = smoothstep(-0.2, 0.15, + sceneData.atmosphereSunDirection.y); + float3 zenith = float3(0.08, 0.28, 0.65); + float3 horizonColor = float3(0.58, 0.72, 0.92); + float3 proceduralSky = mix(zenith, horizonColor, horizon) * + max(daylight, 0.08); + sky = max(sky, proceduralSky); + } if (sceneData.atmosphereEnabled != 0 && sceneData.atmosphereSunDirection.y > -0.15) { float3 sunDirection = sceneData.atmosphereSunDirection; @@ -6837,6 +6864,28 @@ float3 normalizeOr(float3 v, float3 fallback) { return fallback; } +float rayOffsetDistance(float3 position) { + float positionScale = + max(abs(position.x), max(abs(position.y), abs(position.z))); + return max(0.0002, positionScale * 0.000002); +} + +float3 offsetRayOrigin(float3 position, float3 geometricNormal, + float3 direction) { + float side = dot(direction, geometricNormal) >= 0.0 ? 1.0 : -1.0; + return position + geometricNormal * (rayOffsetDistance(position) * side); +} + +float2 encodeNormal(float3 normal) { + normal /= max(abs(normal.x) + abs(normal.y) + abs(normal.z), 1e-6); + 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; + } + return encoded; +} + constexpr sampler materialTexSampler(coord::normalized, address::repeat, filter::linear, mip_filter::linear); @@ -6844,7 +6893,8 @@ constexpr sampler materialTexSampler(coord::normalized, address::repeat, texture2d materialTexture0, texture2d materialTexture1, \ texture2d materialTexture2, texture2d materialTexture3, \ texture2d materialTexture4, texture2d materialTexture5, \ - texture2d materialTexture6, texture2d materialTexture7, \ + texture2d materialTexture6, text)", +R"(ure2d materialTexture7, \ texture2d materialTexture8, texture2d materialTexture9, \ texture2d materialTexture10, \ texture2d materialTexture11, \ @@ -6871,8 +6921,7 @@ constexpr sampler materialTexSampler(coord::normalized, address::repeat, texture2d materialTexture32, \ texture2d materialTexture33, \ texture2d materialTexture34, \ - texture2d materialTexture35, )", -R"( \ + texture2d materialTexture35, \ texture2d materialTexture36, \ texture2d materialTexture37, \ texture2d materialTexture38, \ @@ -6949,7 +6998,8 @@ R"( \ texture2d materialTexture42 [[texture(54)]], \ texture2d materialTexture43 [[texture(55)]], \ texture2d materialTexture44 [[texture(56)]], \ - texture2d materialTexture45 [[texture(57)]], \ + textu)", +R"(re2d materialTexture45 [[texture(57)]], \ texture2d materialTexture46 [[texture(58)]], \ texture2d materialTexture47 [[texture(59)]] @@ -7003,8 +7053,7 @@ float4 sampleMaterialTexture(int textureIndex, float2 uv, case 22: return materialTexture22.sample(materialTexSampler, uv); case 23: - return materialTexture23.sample(materialTe)", -R"(xSampler, uv); + return materialTexture23.sample(materialTexSampler, uv); case 24: return materialTexture24.sample(materialTexSampler, uv); case 25: @@ -7079,6 +7128,20 @@ float4 sampleMaterialTexture( #define PT_MATERIAL_TEXTURE_BINDINGS \ constant MaterialTextureArguments &materialTextureArguments [[buffer(12)]] +float resolveMaterialOpacity(Material mat, float2 uv, uint textureCount, + PT_MATERIAL_TEXTURE_PARAMS) { + float opacity = clamp(mat.albedo.w, 0.0, 1.0); + if (mat.opacityTextureIndex >= 0 && + uint(mat.opacityTextureIndex) < textureCount) { + float4 opacitySample = sampleMaterialTexture( + mat.opacityTextureIndex, uv, PT_MATERIAL_TEXTURE_ARGS); + opacity *= mat.opacityTextureIndex == mat.albedoTextureIndex + ? opacitySample.w + : opacitySample.x; + } + return clamp(opacity, 0.0, 1.0); +} + void resolveMaterialParameters(Material mat, float2 uv, uint textureCount, PT_MATERIAL_TEXTURE_PARAMS, thread float3 &albedo, thread float &metallic, @@ -7137,7 +7200,8 @@ void resolveMaterialParameters(Material mat, float2 uv, uint textureCount, } float3 resolveShadingNormal(Material mat, float2 uv, float3 localN, - float3 localT, float3 localB, InstanceData inst, + float3 localT, float3 localB, Instance)", +R"(Data inst, uint textureCount, PT_MATERIAL_TEXTURE_PARAMS) { float3x3 normalMatrix = float3x3(inst.normalCol0.xyz, inst.normalCol1.xyz, inst.normalCol2.xyz); @@ -7171,98 +7235,70 @@ float3 resolveShadingNormal(Material mat, float2 uv, float3 localN, return N; } -float3 lambert(float3 albedo, float3 N, float3 L, float3 lightColor, - float intensity) { - float ndl = max(dot(N, L), 0.0); - return albedo * lightColor * intensity * ndl; -} - -bool isOccluded(intersector isect, - instance_acceleration_structure sceneAS, float3 P, float3 N, - float3 L, float maxDistance) { - float ndlAbs = abs(dot(N, L)); - float shadowBias = mix(0.003, 0.0008, ndlAbs); +bool isOccluded(intersector isect, + primitive_acceleration_structure sceneAS, float3 P, float3 Ng, + float3 L, float maxDistance, thread uint &rng, + constant Material *materials, + constant uint *primitiveObjects, + constant uint *blasPrimitiveOffsets, + constant VertexData *vertices, constant uint *indices, + constant SceneData &sceneData, PT_MATERIAL_TEXTURE_PARAMS) { + float shadowBias = rayOffsetDistance(P); ray shadowRay; - shadowRay.origin = P + N * shadowBias; + shadowRay.origin = offsetRayOrigin(P, Ng, L); shadowRay.direction = L; - shadowRay.min_distance = shadowBias; + shadowRay.min_distance = 0.0; shadowRay.max_distance = max(maxDistance - shadowBias, shadowBias + 1e-4); - auto shadowHit = isect.intersect(shadowRay, sceneAS, 0xFF); - return shadowHit.type != intersection_type::none; + for (uint alphaStep = 0; alphaStep < 16; ++alphaStep) { + auto shadowHit = isect.intersect(shadowRay, sceneAS); + if (shadowHit.type == intersection_type::none) { + return false; + } + + uint primitiveIndex = + blasPrimitiveOffsets[shadowHit.geometry_id] + shadowHit.primitive_id; + uint objectIndex = primitiveObjects[primitiveIndex]; + Material material = materials[objectIndex]; + uint i0 = indices[primitiveIndex * 3 + 0]; + uint i1 = indices[primitiveIndex * 3 + 1]; + uint i2 = indices[primitiveIndex * 3 + 2]; + float2 bary = shadowHit.triangle_barycentric_coord; + float b0 = 1.0 - bary.x - bary.y; + float2 uv = float2(vertices[i0].uv) * b0 + + float2(vertices[i1].uv) * bary.x + + float2(vertices[i2].uv) * bary.y; + uv = uv * float2(material.textureScale) + + float2(material.textureOffset); + float opacity = resolveMaterialOpacity( + material, uv, sceneData.materialTextureCount, + PT_MATERIAL_TEXTURE_ARGS); + if (opacity >= 0.999 || rand(rng) < opacity) { + return true; + } + + float advance = shadowHit.distance + rayOffsetDistance(shadowRay.origin); + shadowRay.origin += shadowRay.direction * advance; + shadowRay.max_distance -= advance; + if (shadowRay.max_distance <= shadowBias) { + return false; + } + } + + return true; } -bool isOccludedDirectionalLight(DirectionalLightData light, float3 P, float3 N, - thread uint &rng, - intersector isect, - instance_acceleration_structure sceneAS) { +float3 sampleDirectionalLightDirection(DirectionalLightData light, + thread uint &rng) { float3 baseL = normalize(-light.direction); float3x3 basis = buildOrthonormalBasis(baseL); float sunRadius = 0.0025; float2 u = float2(rand(rng), rand(rng)); float r = sunRadius * sqrt(u.x); float phi = 2.0 * M_PI_F * u.y; - )", -R"( float3 jittered = + float3 jittered = baseL + basis[0] * (r * cos(phi)) + basis[1] * (r * sin(phi)); - float3 L = normalize(jittered); - return isOccluded(isect, sceneAS, P, N, L, 1e30); -} - -bool isOccludedPointLight(PointLight light, float3 P, float3 N, - thread uint &rng, - intersector isect, - instance_acceleration_structure sceneAS) { - float lightRadius = clamp(light.range * 0.006, 0.005, 0.04); - float2 u = float2(rand(rng), rand(rng)); - float z = u.x * 2.0 - 1.0; - float r = sqrt(max(0.0, 1.0 - z * z)); - float phi = 2.0 * M_PI_F * u.y; - float3 sphereOffset = - float3(r * cos(phi), r * sin(phi), z) * lightRadius; - float3 sampledLightPos = light.position + sphereOffset; - - float3 toLight = sampledLightPos - P; - float dist2 = max(dot(toLight, toLight), 0.001); - float dist = sqrt(dist2); - float3 L = toLight / dist; - return isOccluded(isect, sceneAS, P, N, L, dist - 0.001); -} - -bool isOccludedSpotLight(SpotLight light, float3 P, float3 N, - thread uint &rng, - intersector isect, - instance_acceleration_structure sceneAS) { - float lightRadius = clamp(light.range * 0.004, 0.004, 0.03); - float2 u = float2(rand(rng), rand(rng)); - float z = u.x * 2.0 - 1.0; - float r = sqrt(max(0.0, 1.0 - z * z)); - float phi = 2.0 * M_PI_F * u.y; - float3 sphereOffset = - float3(r * cos(phi), r * sin(phi), z) * lightRadius; - float3 sampledLightPos = light.position + sphereOffset; - - float3 toLight = sampledLightPos - P; - float dist2 = max(dot(toLight, toLight), 1e-4); - float dist = sqrt(dist2); - float3 L = toLight / dist; - return isOccluded(isect, sceneAS, P, N, L, dist - 0.001); -} - -bool isOccludedAreaLight(AreaLight light, float3 P, float3 N, - thread uint &rng, - intersector isect, - instance_acceleration_structure sceneAS) { - float2 u = float2(rand(rng), rand(rng)); - float2 rect = (u * 2.0 - 1.0) * 0.25; - float3 sampledLightPos = light.position + light.right * rect.x + - light.up * rect.y; - - float3 toLight = sampledLightPos - P; - float dist2 = max(dot(toLight, toLight), 1e-4); - float dist = sqrt(dist2); - float3 L = toLight / dist; - return isOccluded(isect, sceneAS, P, N, L, dist - 0.001); + return normalize(jittered); } // --------------------------------------------------------------------------- @@ -7289,6 +7325,16 @@ float G_Smith(float NdotV, float NdotL, float roughness) { return gV * gL; } +float G1_SmithGGX(float NdotX, float roughness) { + float alpha = max(roughness * roughness, 1e-4); + float alphaSquared = alpha * alpha; + float cosineSquared = NdotX * NdotX; + return (2.0 * NdotX) / + max(NdotX + + sqrt(alphaSquared + (1.0 - alphaSquared) * cosineSquared), + 1e-6); +} + float disneyDiffuseFactor(float NdotV, float NdotL, float LdotH, float roughness) { float fd90 = 0.5 + 2.0 * LdotH * LdotH * roughness; @@ -7306,9 +7352,38 @@ float3 sampleGGX(float2 u, float roughness) { return float3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta); } +float3 sampleGGXVNDF(float3 localView, float roughness, float2 u) { + float alpha = max(roughness * roughness, 1e-3); + float3 stretchedView = + normalizeOr(float3(alpha * localView.x, alpha * localView.y, + max(localView.z, 1e-5)), + float3(0.0, 0.0, 1.0)); + float lensq = stretchedView.x * stretchedView.x + + stretchedView.y * stretchedView.y; + float3 tangentX = lensq > 1e-7 + ? float3(-stretchedView.y, stretchedView.x, 0.0) * + rsqrt(lensq) + : float3(1.0, 0.0, 0.0); + float3 tangentY = cross(stretchedView, tangentX); + float radius = sqrt(u.x); + float phi = 2.0 * M_PI_F * u.y; + float diskX = radius * cos(phi); + float diskY = radius * sin(phi); + float blend = 0.5 * (1.0 + stretchedView.z); + diskY = mix(sqrt(max(0.0, 1.0 - diskX * diskX)), diskY, blend); + float diskZ = sqrt(max(0.0, 1.0 - diskX * diskX - diskY * diskY)); + float3 visibleNormal = diskX * tangentX + diskY * tangentY + + diskZ * stretchedView; + return normalizeOr(float3(alpha * visibleNormal.x, + alpha * visibleNormal.y, + max(visibleNormal.z, 0.0)), + float3(0.0, 0.0, 1.0)); +} + // Full Cook-Torrance PBR for a single analytic light -float3 evalPBR(float3 albedo, float metallic, float roughness, float3 N, - float3 V, float3 L, float3 lightColor, float intensity) { +float3 evalPBR(float3 albedo, float metallic, float roughness, + float reflectivity, float3 N, float3 V, float3 L, + float3 lightColor, float intensity) { float3 H = normalize(V + L); float NdotL = max(dot(N, L), 0.0); float NdotV = max(dot(N, V), 1e-4); @@ -7316,13 +7391,17 @@ float3 evalPBR(float3 albedo, float metallic, float roughness, float3 N, float VdotH = max(dot(V, H), 0.0); float clampedRoughness = clamp(roughness, 0.045, 1.0); - float3 F0 = mix(float3(0.04), albedo, clamp(metallic, 0.0, 1.0)); + float3 baseF0 = mix(float3(0.04), albedo, clamp(metallic, 0.0, 1.0)); + float3 reflectedColor = mix(float3(1.0), albedo, metallic); + float3 F0 = mix(baseF0, reflectedColor, clamp(reflectivity, 0.0, 1.0)); float3 F = F_Schlick(VdotH, F0); float D = D_GGX(NdotH, clampedRoughness); - float G = G_Smith(NdotV, NdotL, clampedRoughness); + float G = )", +R"(G_Smith(NdotV, NdotL, clampedRoughness); float3 specular = (D * G * F) / max(4.0 * NdotV * NdotL, 1e-4); - float3 kD = (1.0 - F) * (1.0 - clamp(metallic, 0.0, 1.0)); + float3 kD = (1.0 - F) * (1.0 - clamp(metallic, 0.0, 1.0)) * + (1.0 - clamp(reflectivity, 0.0, 1.0)); float diffuseFactor = disneyDiffuseFactor(NdotV, NdotL, max(dot(L, H), 0.0), clampedRoughness); float3 diffuse = (kD * albedo * diffuseFactor) / M_PI_F; @@ -7351,59 +7430,66 @@ float3 evalSubsurface(float3 albedo, float3 N, float3 V, float3 L, float3 evalTransmission(float3 albedo, float3 N, float3 V, float3 L, float3 lightColor, float intensity, float roughness, float ior) { - float3 H = normalize(V + L); - float NdotL = max(dot(N, -L), 0.0); - float VdotH = max(dot(V, H), 0.0); + float backLighting = max(dot(N, -L), 0.0); + float forwardAlignment = max(dot(-V, L), 0.0); float3 F0 = float3(pow((ior - 1.0) / (ior + 1.0), 2.0)); - float3 F = F_Schlick(VdotH, F0); + float3 F = F_Schlick(max(dot(N, V), 0.0), F0); float3 transmitTint = mix(float3(1.0), albedo, 0.1); - float3 transmitFactor = (1.0 - F) * transmitTint; - float D = D_GGX(max(dot(N, H), 0.0), roughness); - return transmitFactor * lightColor * intensity * D * NdotL * 2.2 / M_PI_F; + float lobeExponent = mix(96.0, 2.0, sqrt(clamp(roughness, 0.0, 1.0))); + float transmissionLobe = pow(forwardAlignment, lobeExponent); + return (1.0 - F) * transmitTint * lightColor * intensity * backLighting * + transmissionLobe; } // --------------------------------------------------------------------------- // Direct lighting with full PBR (replaces old evalDirectLighting) // --------------------------------------------------------------------------- -float3 evalDirectLightingPBR(intersector isect, - instance_acceleration_structure sceneAS, float3 P, - float3 N, float3 V, float3 albedo, float metallic, - float roughness, float ior, float transmittance, - float sssStrength, float sssThickness, +float3 evalDirectLightingPBR(intersector isect, + primitive_acceleration_structure sceneAS, float3 P, + float3 N, float3 Ng, float3 V, float3 albedo, + float metallic, float roughness, float reflectivity, + float ior, float transmittance, float sssStrength, + float sssThickness, thread uint &rng, constant DirectionalLightData &dirLight, constant SceneData &sceneData, constant PointLight *pointLights, constant SpotLight *spotLights, - constant AreaLight *areaLights) { + constant AreaLight *areaLights, + constant Material *materials, + constant uint *primitiveObjects, + constant uint *blasPrimitiveOffsets, + constant VertexData *vertices, + constant uint *indices, + PT_MATERIAL_TEXTURE_PARAMS) { float3 lighting = float3(0.0); float surfaceOpacity = clamp(1.0 - transmittance * (1.0 - metallic), 0.0, 1.0); // Directional if (sceneData.numDirectionalLights > 0) { - float3 L = normalize(-dirLight.direction); - float3 c = evalPBR(albedo, metallic, roughness, N, V, L, dirLight.color, - max(dirLight.intensity, 0.0)); + float3 L = sampleDirectionalLightDirection(dirLight, rng); + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, + dirLight.color, max(dirLight.intensity, 0.0)); float3 s = evalSubsurface(albedo, N, V, L, dirLight.color, max(dirLight.intensity, 0.0), roughness, - )", -R"( sssStrength, sssThickness); + sssStrength, sssThickness); float3 t = evalTransmission(albedo, N, V, L, dirLight.color, max(dirLight.intensity, 0.0), roughness, ior) * transmittance; - if (!isOccludedDirectionalLight(dirLight, P, N, rng, isect, sceneAS)) { - float3 lightContribution = clampLuminance((c + s) * surfaceOpacity, 8.0); - lighting += lightContribution; - lighting += clampLuminance(t, 12.0); + if (!isOccluded(isect, sceneAS, P, Ng, L, 1e30, rng, materials, + primitiveObjects, blasPrimitiveOffsets, vertices, + indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { + lighting += (c + s) * surfaceOpacity + t; } } // Point lights for (uint i = 0; i < sceneData.numPointLights; ++i) { - float3 toLight = pointLights[i].position - P; + float3 sampledPosition = pointLights[i].position; + float3 toLight = sampledPosition - P; float dist = max(length(toLight), 1e-4); float3 L = toLight / dist; float lightRange = max(pointLights[i].range, 1e-4); @@ -7412,7 +7498,7 @@ R"( sssStrength, sssThickness); float rangeFade = 1.0 - smoothstep(lightRange * 0.75, lightRange, dist); float atten = rangeFade / max(distSq, 1e-4); float intensity = max(pointLights[i].intensity, 0.0) * atten; - float3 c = evalPBR(albedo, metallic, roughness, N, V, L, + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, pointLights[i].color, intensity); float3 s = evalSubsurface(albedo, N, V, L, pointLights[i].color, intensity, @@ -7420,16 +7506,17 @@ R"( sssStrength, sssThickness); float3 t = evalTransmission(albedo, N, V, L, pointLights[i].color, intensity, roughness, ior) * transmittance; - if (!isOccludedPointLight(pointLights[i], P, N, rng, isect, sceneAS)) { - float3 lightContribution = clampLuminance((c + s) * surfaceOpacity, 8.0); - lighting += lightContribution; - lighting += clampLuminance(t, 12.0); + if (!isOccluded(isect, sceneAS, P, Ng, L, dist, rng, materials, + primitiveObjects, blasPrimitiveOffsets, vertices, + indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { + lighting += (c + s) * surfaceOpacity + t; } } // Spot lights for (uint i = 0; i < sceneData.numSpotLights; ++i) { - float3 toLight = spotLights[i].position - P; + float3 sampledPosition = spotLights[i].position; + float3 toLight = sampledPosition - P; float dist = max(length(toLight), 1e-4); float3 L = toLight / dist; float3 fwd = normalize(spotLights[i].direction); @@ -7442,7 +7529,7 @@ R"( sssStrength, sssThickness); float rangeFade = 1.0 - smoothstep(lightRange * 0.75, lightRange, dist); float atten = rangeFade / max(distSq, 1e-4); float intensity = max(spotLights[i].intensity, 0.0) * atten * spot; - float3 c = evalPBR(albedo, metallic, roughness, N, V, L, + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, spotLights[i].color, intensity); float3 s = evalSubsurface(albedo, N, V, L, spotLights[i].color, intensity, @@ -7450,30 +7537,35 @@ R"( sssStrength, sssThickness); float3 t = evalTransmission(albedo, N, V, L, spotLights[i].color, intensity, roughness, ior) * transmittance; - if (!isOccludedSpotLight(spotLights[i], P, N, rng, isect, sceneAS)) { - float3 lightContribution = clampLuminance((c + s) * surfaceOpacity, 8.0); - lighting += lightContribution; - lighting += clampLuminance(t, 12.0); + if (!isOccluded(isect, sceneAS, P, Ng, L, dist, rng, materials, + primitiveObjects, blasPrimitiveOffsets, vertices, + indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { + lighting += (c + s) * surfaceOpacity + t; } } // Area lights for (uint i = 0; i < sceneData.numAreaLights; ++i) { - float3 toLight = areaLights[i].position - P; + float2 lightSample = float2(rand(rng), rand(rng)) * 2.0 - 1.0; + float3 sampledPosition = + areaLights[i].position + + areaLights[i].right * (lightSample.x * areaLights[i].halfWidth) + + areaLights[i].up * (lightSample.y * areaLights[i].halfHeight); + float3 toLight = sampledPosition - P; float dist = max(length(toLight), 1e-4); float3 L = toLight / dist; float3 lightNorm = - normalize(cross(areaLights[i].right, areaLights[i].up)); + normalize(cross(areaLights[i].right, a)", +R"(reaLights[i].up)); float cosLight = areaLights[i].twoSided > 0.5 ? abs(dot(lightNorm, -L)) : max(dot(lightNorm, -L), 0.0); float area = 4.0 * areaLights[i].halfWidth * areaLights[i].halfHeight; - float minDist = max( - max(areaLights[i].halfWidth, areaLights[i].halfHeight) * 0.5, 0.15); - float distSq = dist * dist + minDist * minDist; - float atten = (cosLight * area) / max(distSq, 1e-4); + float lightPdfArea = 1.0 / max(area, 1e-6); + float distSq = max(dist * dist, 1e-6); + float atten = cosLight / max(distSq * lightPdfArea, 1e-6); float intensity = max(areaLights[i].intensity, 0.0) * atten; - float3 c = evalPBR(albedo, metallic, roughness, N, V, L, + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, areaLights[i].color, intensity); float3 s = evalSubsurface(albedo, N, V, L, areaLights[i].color, intensity, @@ -7481,10 +7573,10 @@ R"( sssStrength, sssThickness); float3 t = evalTransmission(albedo, N, V, L, areaLights[i].color, intensity, roughness, ior) * transmittance; - if (!isOccludedAreaLight(areaLights[i], P, N, rng, isect, sceneAS)) { - float3 lightContribution = clampLuminance((c + s) * surfaceOpacity, 8.0); - lighting += lightContribution; - lighting += clampLuminance(t, 12.0); + if (!isOccluded(isect, sceneAS, P, Ng, L, dist, rng, materials, + primitiveObjects, blasPrimitiveOffsets, vertices, + indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { + lighting += (c + s) * surfaceOpacity + t; } } @@ -7492,13 +7584,15 @@ R"( sssStrength, sssThickness); } // --------------------------------------------------------------------------- -// sampleRadiance — primary path with GGX importance-sampled indirect bounce +// sampleRadiance — iterative path with GGX importance-sampled indirect bounces // --------------------------------------------------------------------------- float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, - intersector isect, - instance_acceleration_structure sceneAS, ray primaryRay, - constant Material *materials, constant MeshData *meshData, + intersector isect, + primitive_acceleration_structure sceneAS, ray primaryRay, + constant Material *materials, + constant uint *primitiveObjects, + constant uint *blasPrimitiveOffsets, constant VertexData *vertices, constant uint *indices, constant InstanceData *instanceData, constant DirectionalLightData &dirLight, @@ -7515,388 +7609,341 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, thread float &primaryHitDistance, thread uint &primaryObjectId) { uint rng = seedBase(gid, w, sceneData.frameIndex, sampleIndex); - + uint bounceLimit = min(sceneData.maxBounces, 16u); ray surfaceRay = primaryRay; - auto hit = isect.intersect(surfaceRay, sceneAS, 0xFF); - Material mat{}; - MeshData mesh{}; - InstanceData inst{}; - float2 texUV = float2(0.0); - float3 localN = float3(0.0, 1.0, 0.0); - float3 localT = float3(1.0, 0.0, 0.0); - float3 localB = float3(0.0, 0.0, 1.0); - bool foundOpaqueSurface = false; - - for (uint step = 0; step < 8; ++step) { - if (hit.type == intersection_type::none) { - return skyColor(surfaceRay.direction, 0.0, skybox, sceneData); - } - - uint instanceIndex = hit.instance_id; - uint primitiveIndex = hit.primitive_id; - - mat = materials[instanceIndex]; - mesh = meshData[instanceIndex]; - inst = instanceData[instanceIndex]; + float3 radiance = float3(0.0); + float3 throughput = float3(1.0); + float previousBsdfPdf = 0.0; + float previousEnvironmentPdf = 0.0; + bool previousEventWasDelta = true; + + for (uint depth = 0; depth <= bounceLimit; ++depth) { + auto hit = isect.intersect(surfaceRay, sceneAS); + Material mat{}; + InstanceData inst{}; + uint surfaceObjectIndex = 0xFFFFFFFFu; + float2 texUV = float2(0.0); + float3 localN = float3(0.0, 1.0, 0.0); + float3 localT = float3(1.0, 0.0, 0.0); + float3 localB = float3(0.0, 0.0, 1.0); + float3 geometricNormal = float3(0.0, 1.0, 0.0); + bool foundSurface = false; + + for (uint alphaStep = 0; alphaStep < 16; ++alphaStep) { + if (hit.type == intersection_type::none) { + break; + } - uint i0 = indices[mesh.indexOffset + primitiveIndex * 3 + 0]; - uint i1 = indices[mesh.indexOffset + primitiveIndex * 3 + 1]; - uint i2 = indices[mesh.indexOffset + primitiveIndex * 3 + 2]; + uint primitiveIndex = + blasPrimitiveOffsets[hit.geometry_id] + hit.primitive_id; + surfaceObjectIndex = primitiveObjects[primitiveIndex]; + mat = materials[surfaceObjectIndex]; + inst = instanceData[surfaceObjectIndex]; + + uint i0 = indices[primitiveIndex * 3 + 0]; + uint i1 = indices[primitiveIndex * 3 + 1]; + uint i2 = indices[primitiveIndex * 3 + 2]; + float2 bary = hit.triangle_barycentric_coord; + float b0 = 1.0 - bary.x - bary.y; + float b1 = bary.x; + float b2 = bary.y; + + texUV = float2(vertices[i0].uv) * b0 + + float2(vertices[i1].uv) * b1 + + float2(vertices[i2].uv) * b2; + texUV = texUV * float2(mat.textureScale) + + float2(mat.textureOffset); + localN = normalizeOr(float3(vertices[i0].normal) * b0 + + float3(vertices[i1].normal) * b1 + + float3(vertices[i2].normal) * b2, + float3(0.0, 1.0, 0.0)); + localT = normalizeOr(float3(vertices[i0].tangent) * b0 + + float3(vertices[i1].tangent) * b1 + + float3(vertices[i2].tangent) * b2, + float3(1.0, 0.0, 0.0)); + localB = normalizeOr(float3(vertices[i0].bitangent) * b0 + + float3(vertices[i1].bitangent) * b1 + + float3(vertices[i2].bitangent) * b2, + float3(0.0, 0.0, 1.0)); + float3 p0 = float3(vertices[i0].position); + float3 p1 = float3(vertices[i1].position); + float3 p2 = float3(vertices[i2].position); + float3x3 normalMatrix = float3x3( + inst.normalCol0.xyz, inst.normalCol1.xyz, inst.normalCol2.xyz); + geometricNormal = normalizeOr( + cross(p1 - p0, p2 - p0), + normalizeOr(normalMatrix * localN, float3(0.0, 1.0, 0.0))); + + float alpha = resolveMaterialOpacity( + mat, texUV, sceneData.materialTextureCount, + PT_MATERIAL_TEXTURE_ARGS); + if (alpha >= 0.999 || rand(rng) < alpha) { + foundSurface = true; + break; + } - float2 bary = hit.triangle_barycentric_coord; - float b0 = 1.0 - bary.x - bary.y; - float b1 = bary.x; - float b2 = bary.y; - - texUV = float2(vertices[i0].uv) * b0 + float2(vertices[i1].uv) * b1 + - float2(vertices[i2].uv) * b2; - localN = normalizeOr(float3(vertices[i0].normal) * b0 + - float3(vertices[i1].normal) * b1 + - float3(vertices[i2].normal) * b2, - float3(0.0, 1.0, 0.0)); - localT = normalizeOr(float3(vertices[i0].tangent) * b0 + - float3(vertices[i1].tangent) * b1 + - float3(vertices[i2].tangent) * b2, - float3(1.0, 0.0, 0.0)); - localB)", -R"( = normalizeOr(float3(vertices[i0].bitangent) * b0 + - float3(vertices[i1].bitangent) * b1 + - float3(vertices[i2].bitangent) * b2, - float3(0.0, 0.0, 1.0)); - - float alpha = 1.0; - if (mat.opacityTextureIndex >= 0 && - uint(mat.opacityTextureIndex) < sceneData.materialTextureCount) { - alpha = clamp(sampleMaterialTexture(mat.opacityTextureIndex, texUV, - PT_MATERIAL_TEXTURE_ARGS) - .x, - 0.0, 1.0); - } - - if (alpha >= 0.1) { - foundOpaqueSurface = true; + float3 rejectedPosition = + surfaceRay.origin + surfaceRay.direction * hit.distance; + surfaceRay.origin = + rejectedPosition + surfaceRay.direction * + rayOffsetDistance(rejectedPosition); + surfaceRay.min_distance = 0.0; + hit = isect.intersect(surfaceRay, sceneAS); + } + + if (!foundSurface) { + float misWeight = previousEventWasDelta + ? 1.0 + : powerHeuristic(previousBsdfPdf, + previousEnvironmentPdf); + radiance += throughput * misWeight * + skyColor(surfaceRay.direction, 0.0, skybox, sceneData); break; } - surfaceRay.origin = - surfaceRay.origin + surfaceRay.direction * (hit.distance + 0.001); - surfaceRay.min_distance = 0.0; - surfaceRay.max_distance = primaryRay.max_distance; - hit = isect.intersect(surfaceRay, sceneAS, 0xFF); - } - - if (!foundOpaqueSurface) { - return skyColor(surfaceRay.direction, 0.0, skybox, sceneData); - } + float3 shadingNormal = resolveShadingNormal( + mat, texUV, localN, localT, localB, inst, + sceneData.materialTextureCount, PT_MATERIAL_TEXTURE_ARGS); + float3 P = surfaceRay.origin + surfaceRay.direction * hit.distance; + float3 V = normalize(-surfaceRay.direction); + bool frontFace = dot(geometricNormal, V) >= 0.0; + float3 Ng = frontFace ? geometricNormal : -geometricNormal; + float3 N = dot(shadingNormal, Ng) >= 0.0 ? shadingNormal : -shadingNormal; + float shadingNormalCosine = dot(N, Ng); + if (shadingNormalCosine < 0.1) { + N = normalizeOr(N + Ng * (0.1 - shadingNormalCosine), Ng); + } - float3 N = resolveShadingNormal(mat, texUV, localN, localT, localB, inst, - sceneData.materialTextureCount, - PT_MATERIAL_TEXTURE_ARGS); - float3 P = surfaceRay.origin + surfaceRay.direction * hit.distance; - float3 V = normalize(-surfaceRay.direction); - if (dot(N, V) < 0.0) { - N = -N; - } + float3 albedo; + float metallic; + float roughness; + float ao; + float3 emissive; + float ior; + float transmittance; + resolveMaterialParameters(mat, texUV, sceneData.materialTextureCount, + PT_MATERIAL_TEXTURE_ARGS, albedo, metallic, + roughness, ao, emissive, ior, transmittance); + + if (depth == 0) { + primaryAlbedo = albedo; + primaryNormal = N; + primaryPosition = P; + prima)", +R"(ryDepth = length(P - primaryRay.origin); + primaryRoughness = roughness; + primaryHitDistance = hit.distance; + primaryObjectId = surfaceObjectIndex; + } + + float reflectivity = clamp(mat.reflectivity, 0.0, 1.0); + float sssStrength = 0.0; + float sssThickness = mix(0.25, 1.75, ao); + float3 direct = evalDirectLightingPBR( + isect, sceneAS, P, N, Ng, V, albedo, metallic, roughness, + reflectivity, ior, transmittance, sssStrength, sssThickness, rng, + dirLight, sceneData, pointLights, spotLights, areaLights, materials, + primitiveObjects, blasPrimitiveOffsets, vertices, indices, + PT_MATERIAL_TEXTURE_ARGS); + radiance += throughput * (direct + emissive); + + if (depth == 0 && sceneData.ambientIntensity > 0.0) { + float aoVisibility = mix(0.2, 1.0, ao); + float3 ambientF0 = mix(float3(0.04), albedo, metallic); + float3 ambientF = F_Schlick(max(dot(N, V), 0.0), ambientF0); + float3 ambientDiffuse = (1.0 - ambientF) * (1.0 - metallic) * + albedo * (1.0 - transmittance); + float3 ambientSpecular = + ambientF * mix(1.0, 0.35, roughness); + float3 ambient = (ambientDiffuse + ambientSpecular) * + sceneData.ambientColor * + sceneData.ambientIntensity * aoVisibility; + radiance += throughput * ambient; + } - float3 albedo; - float metallic; - float roughness; - float ao; - float3 emissive; - float ior; - float transmittance; - resolveMaterialParameters(mat, texUV, sceneData.materialTextureCount, - PT_MATERIAL_TEXTURE_ARGS, albedo, metallic, - roughness, ao, emissive, ior, transmittance); - primaryAlbedo = albedo; - primaryNormal = N; - primaryPosition = P; - primaryDepth = length(P - primaryRay.origin); - primaryRoughness = roughness; - primaryHitDistance = hit.distance; - primaryObjectId = hit.instance_id; - float reflectivity = clamp(mat.reflectivity, 0.0, 1.0); - float sssStrength = clamp(1.0 - mat.albedo.w, 0.0, 1.0) * (1.0 - metallic); - float sssThickness = mix(0.25, 1.75, ao); - - float3 direct = evalDirectLightingPBR( - isect, sceneAS, P, N, V, albedo, metallic, roughness, ior, - transmittance, sssStrength, sssThickness, rng, dirLight, sceneData, - pointLights, spotLights, areaLights); - - float3 indirect = float3(0.0); - - if (sceneData.maxBounces > 0) { float3 baseF0 = mix(float3(0.04), albedo, metallic); float3 reflectedColor = mix(float3(1.0), albedo, metallic); float3 F0 = mix(baseF0, reflectedColor, reflectivity); - float3 F_approx = F_Schlick(max(dot(N, V), 0.0), F0); - + float NdotV = max(dot(N, V), 1e-4); float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); - float dielectricSpec = - F_Schlick(max(dot(N, V), 0.0), float3(dielectricF0)).x; + float dielectricFresnel = + F_Schlick(NdotV, float3(dielectricF0)).x; float specProb = metallic * mix(0.35, 0.9, 1.0 - roughness) + - (1.0 - metallic) * dielectricSpec; - float transmitProb = - transmittance * (1.0 - metallic) * (1.0 - dielectricSpec); + (1.0 - metallic) * dielectricFresnel; + float transmitProb = transmittance * (1.0 - metallic) * + (1.0 - dielectricFresnel); float diffuseProb = (1.0 - metallic) * (1.0 - transmittance); specProb = mix(specProb, 1.0, reflectivity); transmitProb *= 1.0 - reflectivity; diffuseProb *= 1.0 - reflectivity; - float probSum = max(specProb + transmitProb + diffuseProb, 1e-4); - specProb /= probSum; - transmitProb /= probSum; - diffuseProb /= probSum; + float eta = frontFace ? 1.0 / ior : ior; + float3 idealRefractedDirection = refract(-V, N, eta); + bool totalInternalReflection = + dot(idealRefractedDirection, idealRefractedDirection) < 1e-8; + if (totalInternalReflection) { + specProb += transmitProb; + transmitProb = 0.0; + } + float probabilitySum = + max(specProb + transmitProb + diffuseProb, 1e-4); + specProb /= probabilitySum; + transmitProb /= probabilitySum; + diffuseProb /= probabilitySum; float3x3 basis = buildOrthonormalBasis(N); - ray bounceRay; - bounceRay.origin = P + N * 0.001; - bounceRay.min_distance = 0.0; - bounceRay.max_distance = 1.0e30; - - float3 brdfWeight; - float chooseSplit = rand(rng); - bool choseTransmission = false; - bool choseSpecular = false; - - if (specProb > 1e-4 && chooseSplit < specProb) { - choseSpecular = true; - float2 u = float2(rand(rng), rand(rng)); - float3 localH = sampleGGX(u, max(roughness, 0.001)); - float3 H_world = normalize(basis * localH); - float3 bounceDir = reflect(-V, H_world); - - if (dot(bounceDir, N) <= 0.0) - bounceDir = reflect(-V, N); - - bounceRay.direction = bounceDir; - - float NdotL2 = max(dot(N, bounceDir), 1e-4); - float NdotV2 = max(dot(N, V), 1e-4); - float3 Fs = F_Schlick(max(dot(V, H_world), 0.0), F0); - float Gs = G_Smith(NdotV2, NdotL2, roughness); - - brdfWeight = - (Fs * Gs / max(4.0 * NdotV2, 1e-4)) / max(specProb, 1e-4); - - } else if (transmitProb > 1e-4 && chooseSplit < specProb + transmitProb) { - choseTransmission = true; - bool entering = dot(N, V) > 0.0; - float eta = entering ? (1.0 / ior) : ior; - float3 faceN = entering ? N : -N; - - float3 refractDir = refract(-V, faceN, eta); - - if (length(refractDir) < 1e-5) { - refractDir = reflect(-V, faceN); + if (sceneData.environmentEnabled != 0 && + diffuseProb + specProb > 1e-4) { + float3 localEnvironmentDirection = + cosineSampleHemisphere(float2(rand(rng), rand(rng))); + float3 environmentDirection = + normalizeOr(basis * localEnvironmentDirection, N); + float NdotEnvironment = dot(N, environmentDirection); + if (NdotEnvironment > 0.0 && + dot(Ng, environmentDirection) > 0.0 && + !isOccluded(isect, sceneAS, P, Ng, environmentDirection, 1e30, + rng, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, sceneData, + PT_MATERIAL_TEXTURE_ARGS)) { + float3 H = normalizeOr(V + environmentDirection, N); + float NdotH = max(dot(N, H), 1e-5); + float VdotH = max(dot(V, H), 1e-5); + float3 F = F_Schlick(VdotH, F0); + float3 kD = (1.0 - F) * (1.0 - metallic) * + (1.0 - transmittance) * (1.0 - reflectivity); + float diffuseFactor = disneyDiffuseFactor( + NdotV, NdotEnvironment, + max(dot(environmentDirection, H), 0.0), roughness); + float3 reflectionBsdf = + kD * albedo * diffuseFactor / M_PI_F; + float environmentPdf = NdotEnvironment / M_PI_F; + float bsdfPdf = diffuseProb * environmentPdf; + if (roughness > 0.025 && specProb > 1e-4) { + float D = D_GGX(NdotH, roughness); + float G1V = G1_SmithGGX(NdotV, roughness); + float G1L = + G1_SmithGGX(NdotEnvironment, roughness); + reflectionBsdf += + D * G1V * G1L * F / + max(4.0 * NdotV * NdotEnvironment, 1e-6); + bsdfPdf += specProb * D * G1V / + max(4.0 * NdotV, 1e-6); + } + float competingBsdfPdf = depth < bounceLimit ? bsdfPdf : 0.0; + float misWeight = + powerHeuristic(environmentPdf, competingBsdfPdf); + float3 environmentRadiance = skyColor( + environmentDirection, 0.0, skybox, sceneData); + radiance += throughput * reflectionBsdf * + environmentRadiance * NdotEnvironment * misWeight / + max(environmentPdf, 1e-6); } - - bounceRay.origin = P - faceN * 0.002; - bounceRay.direction = normalize(refractDir); - - float3 F0t = float3(pow((ior - 1.0) / (ior + 1.0), 2.0)); - float3 Ft = F_Schlick(max(dot(V, faceN), 0.0), F0t); - float3 kT = (1.0 - Ft) * mix(float3(1.0), albedo, 0.15); - - float3 absorption = exp(-(1.0 - albedo) * 0.12); - - brdfWeight = (kT * absorption) / max(transmitProb, 1e-4); - - } else { - float2 u = float2(rand(rng), rand(rng)); - float3 localBounce = cosineSampleHemisphere(u); - bounceRay.direction = normalize(basis * localBounce); - - float3 kD = (1.0 - F_approx) * (1.0 - metallic); - brdfWeight = (kD * albedo) / max(diffuseProb, 1e-4); } - brdfWeight = clamp(brdfWeight, float3(0.0), float3(8.0)); - - auto bounceHit = isect.intersect(bounceRay, sceneAS, 0xFF); - auto resolvedBounceHit = bounceHit; - ray resolvedBounceRay = bounceRay; - - if (choseTransmission) { - for (uint shellStep = 0; shellStep < 8; ++shellStep) { - if (resolvedBounceHit.type == intersection_type::none) { - break; - } - - uint ti = resolvedBounceHit.instance_id; - uint tp = resolvedBounceHit.primitive_id; - - Material tmat = materials[ti]; - MeshData tmesh = meshData[ti]; - InstanceData tinst = instanceData[ti]; - - uint tj0 = indices[tmesh.indexOffset + tp * 3 + 0]; - uint tj1 = indices[tmesh.indexOffset + tp * 3 + 1]; - uint tj2 = indices[tmesh.indexOffset + tp * 3 + 2]; - - float2 tbary = resolvedBounceHit.triangle_barycentric_coord; - float tb0 = 1.0 - tbary.x - tbary.y; - float tb1 = tbary.x; - float tb2 = tbary.y; - - float2 tUV = float2(vertices[tj0].uv) * tb0 + - float2(vertices[tj1].uv) * tb1 + - float2(vertices[tj2].uv) * tb2; - float3 tLocalN = normalizeOr(float3(vertices[tj0].normal) * tb0 + - float3(vertices[tj1].normal) * tb1 + - float3(vertices[tj2].normal) * tb2, - float3(0.0, 1.0, 0.0)); - float3 tLocalT = normalizeOr(float3(vertices[tj0].tangent) * tb0 + - float3(vertices[tj1].tangent) * tb1 + - float3(vertices[tj2].tangent) * tb2, - float3(1.0, 0.0, 0.0)); - float3 tLocalB = normalizeOr(float3(vertices[tj0].bitangent) * tb0 + - float3(vertices[tj1].bitangent) * tb1 + - float3(vertices[tj2].bitangent) * tb2, - )", -R"( float3(0.0, 0.0, 1.0)); - - float3 tN = resolveShadingNormal(tmat, tUV, tLocalN, tLocalT, - tLocalB, tinst, - sceneData.materialTextureCount, - PT_MATERIAL_TEXTURE_ARGS); - float3 tP = resolvedBounceRay.origin + - resolvedBounceRay.direction * resolvedBounceHit.distance; - float3 tV = normalize(-resolvedBounceRay.direction); - if (dot(tN, tV) < 0.0) { - tN = -tN; - } - - float3 tAlbedo; - float tMetallic; - float tRoughness; - float tAo; - float3 tEmissive; - float tIor; - float tTransmittance; - resolveMaterialParameters( - tmat, tUV, sceneData.materialTextureCount, - PT_MATERIAL_TEXTURE_ARGS, tAlbedo, tMetallic, tRoughness, tAo, - tEmissive, tIor, tTransmittance); - - if (tTransmittance < 0.5 || tMetallic > 0.5) { - break; - } + if (depth == bounceLimit) { + break; + } - bool enteringShell = dot(tN, tV) > 0.0; - float etaShell = enteringShell ? (1.0 / tIor) : tIor; - float3 faceNShell = enteringShell ? tN : -tN; - float3 refractShell = refract(-tV, faceNShell, etaShell); - if (length(refractShell) < 1e-5) { - refractShell = reflect(-tV, faceNShell); + float choice = rand(rng); + float3 bounceWeight = float3(0.0); + float3 nextDirection = N; + float sampledBsdfPdf = 0.0; + float sampledEnvironmentPdf = 0.0; + bool sampledEventWasDelta = true; + + if (choice < specProb && specProb > 1e-4) { + if (roughness <= 0.025 || totalInternalReflection) { + nextDirection = reflect(-V, N); + float3 F = totalInternalReflection + ? float3(1.0) + : F_Schlick(NdotV, F0); + bounceWeight = F / max(specProb, 1e-4); + } else { + float3 localView = + float3(dot(V, basis[0]), dot(V, basis[1]), dot(V, N)); + float3 localH = sampleGGXVNDF( + localView, roughness, float2(rand(rng), rand(rng))); + float3 H = normalizeOr(basis * localH, N); + float VdotH = max(dot(V, H), 1e-5); + nextDirection = reflect(-V, H); + float NdotL = dot(N, nextDirection); + if (NdotL > 0.0 && dot(nextDirection, Ng) > 0.0) { + float NdotH = max(dot(N, H), 1e-5); + float D = D_GGX(NdotH, roughness); + float G1V = G1_SmithGGX(NdotV, roughness); + float G1L = G1_SmithGGX(NdotL, roughness); + float3 F = F_Schlick(VdotH, F0); + float3 specularBsdf = + D * G1V * G1L * F / + max(4.0 * NdotV * NdotL, 1e-6); + float conditionalPdf = + D * G1V / max(4.0 * NdotV, 1e-6); + float combinedPdf = specProb * conditionalPdf; + bounceWeight = specularBsdf * NdotL / + max(combinedPdf, 1e-6); + sampledBsdfPdf = combinedPdf; + sampledEnvironmentPdf = NdotL / M_PI_F; + sampledEventWasDelta = false; } - - resolvedBounceRay.origin = tP - faceNShell * 0.002; - resolvedBounceRay.direction = normalize(refractShell); - resolvedBounceRay.min_distance = 0.0; - resolvedBounceRay.max_distance = 1.0e30; - resolvedBounceHit = - isect.intersect(resolvedBounceRay, sceneAS, 0xFF); } - } - - if (resolvedBounceHit.type == intersection_type::none) { - float bounceStrength = choseSpecular - ? mix(sceneData.indirectStrength, 1.0, - max(metallic, reflectivity)) - : sceneData.indirectStrength; - indirect = brdfWeight * - skyColor(resolvedBounceRay.direction, 0.0, skybox, - sceneData) * - bounceStrength; + } else if (choice < specProb + transmitProb && + transmitProb > 1e-4) { + nextDirection = idealRefractedDirection; + float3 F = F_Schlick(NdotV, float3(dielectricF0)); + float3 tint = mix(float3(1.0), albedo, 0.15); + bounceWeight = (1.0 - F) * tint / + max(transmitProb, 1e-4); } else { - uint bi = resolvedBounceHit.instance_id; - uint bp = resolvedBounceHit.primitive_id; - - Material bmat = materials[bi]; - MeshData bmesh = meshData[bi]; - InstanceData binst = instanceData[bi]; - - uint bj0 = indices[bmesh.indexOffset + bp * 3 + 0]; - uint bj1 = indices[bmesh.indexOffset + bp * 3 + 1]; - uint bj2 = indices[bmesh.indexOffset + bp * 3 + 2]; - - float2 bbary = resolvedBounceHit.triangle_barycentric_coord; - float bb0 = 1.0 - bbary.x - bbary.y; - float bb1 = bbary.x; - float bb2 = bbary.y; - - float2 bUV = float2(vertices[bj0].uv) * bb0 + - float2(vertices[bj1].uv) * bb1 + - float2(vertices[bj2].uv) * bb2; - float3 bLocalN = - normalizeOr(float3(vertices[bj0].normal) * bb0 + - float3(vertices[bj1].normal) * bb1 + - float3(vertices[bj2].normal) * bb2, - float3(0.0, 1.0, 0.0)); - float3 bLocalT = - normalizeOr(float3(vertices[bj0].tangent) * bb0 + - float3(vertices[bj1].tangent) * bb1 + - float3(vertices[bj2].tangent) * bb2, - float3(1.0, 0.0, 0.0)); - float3 bLocalB = - normalizeOr(float3(vertices[bj0].bitangent) * bb0 + - float3(vertices[bj1].bitangent) * bb1 + - float3(vertices[bj2].bitangent) * bb2, - float3(0.0, 0.0, 1.0)); - float3 bN = resolveShadingNormal( - bmat, bUV, bLocalN, bLocalT, bLocalB, binst, - sceneData.materialTextureCount, PT_MATERIAL_TEXTURE_ARGS); - float3 bP = resolvedBounceRay.origin + - resolvedBounceRay.direction * resolvedBounceHit.distance; - float3 bV = normalize(-resolvedBounceRay.direction); - if (dot(bN, bV) < 0.0) { - bN = -bN; - } + float3 localDirection = + cosineSampleHemisphere(float2(rand(rng), rand(rng))); + nextDirection = norma)", +R"(lizeOr(basis * localDirection, N); + float NdotL = max(dot(N, nextDirection), 0.0); + float3 H = normalizeOr(V + nextDirection, N); + float3 F = F_Schlick(max(dot(V, H), 0.0), F0); + float3 kD = (1.0 - F) * (1.0 - metallic); + float diffuseFactor = disneyDiffuseFactor( + NdotV, NdotL, max(dot(nextDirection, H), 0.0), roughness); + float3 diffuseBsdf = kD * albedo * diffuseFactor / M_PI_F; + float conditionalPdf = NdotL / M_PI_F; + float combinedPdf = diffuseProb * conditionalPdf; + bounceWeight = diffuseBsdf * NdotL / + max(combinedPdf, 1e-6); + sampledBsdfPdf = combinedPdf; + sampledEnvironmentPdf = conditionalPdf; + sampledEventWasDelta = false; + } + + throughput *= max(bounceWeight, float3(0.0)); + if (depth == 0) { + throughput *= max(sceneData.indirectStrength, 0.0); + } + if (!all(isfinite(throughput)) || max(throughput.x, + max(throughput.y, throughput.z)) < + 1e-5) { + break; + } - float3 bAlbedo; - float bMetallic; - float bRoughness; - float bAo; - float3 bEmissive; - float bIor; - float bTransmittance; - resolveMaterialParameters(bmat, bUV, sceneData.materialTextureCount, - PT_MATERIAL_TEXTURE_ARGS, bAlbedo, - bMetallic, bRoughness, bAo, bEmissive, - bIor, bTransmittance); - - float bSssStrength = - clamp(1.0 - bmat.albedo.w, 0.0, 1.0) * (1.0 - bMetallic); - float bSssThickness = mix(0.25, 1.75, bAo); - - float3 bounceDirect = evalDirectLightingPBR( - isect, sceneAS, bP, bN, bV, bAlbedo, bMetallic, bRoughness, - bIor, bTransmittance, bSssStrength, bSssThickness, rng, dirLight, - sceneData, pointLights, spotLights, areaLights); - if (choseTransmission) { - float causticFocus = mix(1.0, 4.0, - transmittance * (1.0 - roughness)); - bounceDirect *= causticFocus; + if (depth >= 2) { + float survival = clamp(max(throughput.x, + max(throughput.y, throughput.z)), + 0.05, 0.95); + if (rand(rng) > survival) { + break; } - - float3 bAmbient = bAlbedo * max(sceneData.ambientIntensity, 0.0) * - (1.0 - bMetallic) * bAo; - float bounceStrength = choseSpecular - ? mix(sceneData.indirectStrength, 1.0, - max(metallic, reflectivity)) - : sceneData.indirectStrength; - indirect = brdfWeight * (bAmbient + bounceDirect + bEmissive) * - bounceStrength; + throughput /= survival; } - indirect = clampLuminance(indirect, 16.0); + previousBsdfPdf = sampledBsdfPdf; + previousEnvironmentPdf = sampledEnvironmentPdf; + previousEventWasDelta = sampledEventWasDelta; + + surfaceRay.origin = offsetRayOrigin(P, Ng, nextDirection); + surfaceRay.direction = normalizeOr(nextDirection, N); + surfaceRay.min_distance = 0.0; + surfaceRay.max_distance = 1.0e30; } - float3 ambient = - albedo * max(sceneData.ambientIntensity, 0.0) * (1.0 - metallic) * ao * - (1.0 - transmittance); - return clampLuminance(ambient + direct + emissive + indirect, 24.0); + return radiance; } kernel void main0(texture2d outTex [[texture(0)]], @@ -7907,10 +7954,10 @@ kernel void main0(texture2d outTex [[texture(0)]], texture2d motionObjectTex [[texture(5)]], texture2d momentsHitTex [[texture(6)]], texture2d historyGuideTex [[texture(7)]], - instance_acceleration_structure sceneAS [[buffer(0)]], + primitive_acceleration_structure sceneAS [[buffer(0)]], constant CameraUniforms &cam [[buffer(1)]], constant Material *materials [[buffer(2)]], - constant MeshData *meshData [[buffer(3)]], + constant uint *primitiveObjects [[buffer(3)]], constant VertexData *vertices [[buffer(4)]], constant uint *indices [[buffer(5)]], constant InstanceData *instanceData [[buffer(6)]], @@ -7920,26 +7967,20 @@ kernel void main0(texture2d outTex [[texture(0)]], constant SpotLight *spotLights [[buffer(10)]], constant AreaLight *areaLights [[buffer(11)]], PT_MATERIAL_TEXTURE_BINDINGS, + constant uint *blasPrimitiveOffsets [[buffer(13)]], texturecube skybox [[texture(60)]], - uint2 gid [[thread_position_i)", -R"(n_grid]]) { + uint2 gid [[thread_position_in_grid]]) { uint w = outTex.get_width(); uint h = outTex.get_height(); + uint pixelStride = max(sceneData.pixelStride, 1u); + gid *= pixelStride; if (gid.x >= w || gid.y >= h) return; float2 uv = (float2(gid) + 0.5) / float2(w, h); - float2 ndc = uv * 2.0 - 1.0; - ndc.y = -ndc.y; - - float4 clip = float4(ndc, 1.0, 1.0); - float4 worldH = cam.invViewProj * clip; - float3 worldP = worldH.xyz / worldH.w; - float3 ro = cam.camPos; - float3 rd = normalize(worldP - ro); - intersector isect; + intersector isect; isect.assume_geometry_type(geometry_type::triangle); isect.set_triangle_cull_mode(triangle_cull_mode::none); @@ -7954,22 +7995,54 @@ R"(n_grid]]) { uint spp = max(sceneData.raysPerPixel, 1u); for (uint s = 0; s < spp; ++s) { + uint cameraRng = seedBase(gid, w, sceneData.frameIndex, + s + 0x9E3779B9u); + float2 pixelJitter = + float2(rand(cameraRng), rand(cameraRng)) - 0.5; + float2 sampleUv = (float2(gid) + 0.5 + pixelJitter) / float2(w, h); + float2 sampleNdc = sampleUv * 2.0 - 1.0; + sampleNdc.y = -sampleNdc.y; + float4 sampleClip = float4(sampleNdc, 1.0, 1.0); + float4 sampleWorldH = cam.invViewProj * sampleClip; + float3 sampleWorldP = sampleWorldH.xyz / sampleWorldH.w; + ray primaryRay; primaryRay.origin = ro; - primaryRay.direction = rd; + primaryRay.direction = normalize(sampleWorldP - ro); primaryRay.min_distance = 0.001; primaryRay.max_distance = 1.0e30; + float3 sampleAlbedo = float3(0.0); + float3 sampleNormal = float3(0.0); + float3 samplePosition = float3(0.0); + float sampleDepth = 0.0; + float sampleRoughness = 1.0; + float sampleHitDistance = 0.0; + uint sampleObjectId = 0xFFFFFFFFu; + float3 sample = sampleRadiance( - gid, s, w, isect, sceneAS, primaryRay, materials, meshData, - vertices, indices, instanceData, dirLight, sceneData, pointLights, - spotLights, areaLights, PT_MATERIAL_TEXTURE_ARGS, skybox, - primaryAlbedo, primaryNormal, primaryPosition, primaryDepth, - primaryRoughness, primaryHitDistance, primaryObjectId); - color += clampLuminance(sample, 24.0); + gid, s, w, isect, sceneAS, primaryRay, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, instanceData, dirLight, + sceneData, pointLights, spotLights, areaLights, + PT_MATERIAL_TEXTURE_ARGS, skybox, sampleAlbedo, sampleNormal, + samplePosition, sampleDepth, sampleRoughness, sampleHitDistance, + sampleObjectId); + color += sample; + if (s == 0) { + primaryAlbedo = sampleAlbedo; + primaryNormal = sampleNormal; + primaryPosition = samplePosition; + primaryDepth = sampleDepth; + primaryRoughness = sampleRoughness; + primaryHitDistance = sampleHitDistance; + primaryObjectId = sampleObjectId; + } } color /= float(spp); + if (!all(isfinite(color))) { + color = float3(0.0); + } int frameIndex = int(sceneData.frameIndex); @@ -7978,71 +8051,70 @@ R"(n_grid]]) { float objectIdValue = primaryObjectId == 0xFFFFFFFFu ? -1.0 : float(primaryObjectId); + float2 encodedNormal = encodeNormal(primaryNormal); float4 currentGuide = - float4(primaryNormal.xy, primaryDepth, objectIdValue); + float4(encodedNormal, primaryDepth, objectIdValue); bool historyValid = frameIndex > 0 && abs(previousGuide.z - primaryDepth) < max(0.05, primaryDepth * 0.02) && - distance(previousGuide.xy, primaryNormal.xy) < 0.12 && + distance(previousGuide.xy, encodedNormal) < 0.08 && abs(previousGuide.w - objectIdValue) < 0.5; if (frameIndex == 0) prevColor = float4(0, 0, 0, 1); + float sampleLuminanceLimit = + historyValid ? max(8.0, luminance(prevColor.xyz) * 6.0 + 2.0) : 128.0; + color = clampLuminance(color, sampleLuminanceLimit); if (!historyValid) prevColor = float4(color, 1.0); - if (frameIndex > 2) { - float prevL = luminance(prevColor.xyz); - float currL = luminance(color); - float maxAllowed = max(prevL * 1.6 + 0.15, 0.75); - if (currL > maxAllowed && currL > 1e-6) { - color *= maxAllowed / currL; - } - } - - float historyLength = historyValid ? min(float(frameIndex), 31.0) : 0.0; + float historyLength = historyValid ? min(float(frameIndex), 255.0) : 0.0; float3 lower = min(prevColor.xyz, color) - float3(0.35); float3 upper = max(prevColor.xyz, color) + float3(0.35); float3 clippedHistory = clamp(prevColor.xyz, lower, upper); float3 accum = mix(color, clippedHistory, historyLength / (historyLength + 1.0)); - accum = clampLuminance(accum, 24.0); - - constexpr float bloomThreshold = 1.0; - constexpr float bloomKnee = 0.5; - - float brightness = luminance(accum); - float soft = clamp( - brightness - bloomThreshold + bloomKnee, - 0.0, - bloomKnee * 2.0 - ); - - soft = soft * soft / max(bloomKnee * 4.0, 0.00001); - - float contribution = - max(brightness - bloomThreshold, soft) / - max(brightness, 0.00001); - - float3 brightColor = accum * contribution; + accum = clampLuminance(accum, 256.0); + + constexpr float bloomThreshold = 0.8; + constexpr float bloomKnee = 0.35; + + float brightness = luminance(accum); + float soft = clamp(brightness - bloomThreshold + bloomKnee, 0.0, + bloomKnee * 2.0); + soft = soft * soft / max(bloomKnee * 4.0, 0.00001); + float contribution = max(brightness - bloomThreshold, soft) / + )", +R"(max(brightness, 0.00001); + float3 brightColor = accum * contribution; float4 previousClip = cam.prevViewProj * float4(primaryPosition, 1.0); float2 previousUv = previousClip.xy / max(abs(previousClip.w), 0.0001); previousUv = previousUv * 0.5 + 0.5; float2 motion = uv - previousUv; float moment = luminance(color); - historyTex.write(float4(accum, 1.0), gid); - historyGuideTex.write(currentGuide, gid); - albedoRoughnessTex.write(float4(primaryAlbedo, primaryRoughness), gid); - normalDepthTex.write(float4(primaryNormal, primaryDepth), gid); - motionObjectTex.write(float4(motion, objectIdValue, 1.0), gid); - momentsHitTex.write(float4(moment, moment * moment, - primaryRoughness, primaryHitDistance), gid); - outTex.write(float4(accum, 1.0), gid); - brightTex.write(float4(brightColor, 1.0), gid); + for (uint y = 0; y < pixelStride; ++y) { + for (uint x = 0; x < pixelStride; ++x) { + uint2 pixel = gid + uint2(x, y); + if (pixel.x >= w || pixel.y >= h) { + continue; + } + historyTex.write(float4(accum, 1.0), pixel); + historyGuideTex.write(currentGuide, pixel); + albedoRoughnessTex.write(float4(primaryAlbedo, primaryRoughness), + pixel); + normalDepthTex.write(float4(primaryNormal, primaryDepth), pixel); + motionObjectTex.write(float4(motion, objectIdValue, 1.0), pixel); + momentsHitTex.write(float4(moment, moment * moment, + primaryRoughness, primaryHitDistance), + pixel); + outTex.write(float4(accum, 1.0), pixel); + brightTex.write(float4(brightColor, 1.0), pixel); + } + } } )", }; -static const AtlasPackedShaderSource PATH = {PATH_PARTS, 8}; +static const AtlasPackedShaderSource PATH = {PATH_PARTS, 9}; static const char* const PATH_DENOISE_PARTS[] = { R"(#include @@ -8056,40 +8128,80 @@ kernel void main0(texture2d inputTexture [[texture(0)]], texture2d outputTexture [[texture(1)]], texture2d brightTexture [[texture(2)]], texture2d guideTexture [[texture(3)]], + texture2d albedoRoughnessTexture + [[texture(4)]], constant DenoiseParameters ¶meters [[buffer(0)]], uint2 gid [[thread_position_in_grid]]) { uint width = outputTexture.get_width(); uint height = outputTexture.get_height(); - if (gid.x >= width || gid.y >= height) return; + if (gid.x >= width || gid.y >= height) + return; - constexpr int2 offsets[9] = { - int2(0, 0), int2(1, 0), int2(-1, 0), int2(0, 1), int2(0, -1), - int2(1, 1), int2(-1, 1), int2(1, -1), int2(-1, -1)}; + constexpr int2 offsets[9] = {int2(0, 0), int2(1, 0), int2(-1, 0), + int2(0, 1), int2(0, -1), int2(1, 1), + int2(-1, 1), int2(1, -1), int2(-1, -1)}; constexpr float weights[9] = {0.28, 0.12, 0.12, 0.12, 0.12, 0.06, 0.06, 0.06, 0.06}; float3 center = inputTexture.read(gid).xyz; float4 centerGuide = guideTexture.read(gid); + float4 centerAlbedoRoughness = albedoRoughnessTexture.read(gid); + bool centerSurface = centerGuide.w > 0.0; + float centerNormalLength = dot(centerGuide.xyz, centerGuide.xyz); float centerLuminance = dot(center, float3(0.2126, 0.7152, 0.0722)); float3 filtered = float3(0.0); float totalWeight = 0.0; for (int i = 0; i < 9; ++i) { - int2 samplePosition = clamp(int2(gid) + offsets[i] * parameters.stepWidth, - int2(0), int2(width - 1, height - 1)); + int2 samplePosition = + clamp(int2(gid) + offsets[i] * parameters.stepWidth, int2(0), + int2(width - 1, height - 1)); float3 sampleColor = inputTexture.read(uint2(samplePosition)).xyz; float4 sampleGuide = guideTexture.read(uint2(samplePosition)); - float sampleLuminance = dot(sampleColor, float3(0.2126, 0.7152, 0.0722)); - float edgeWeight = exp(-abs(sampleLuminance - centerLuminance) * 6.0); - float normalWeight = - pow(max(dot(centerGuide.xyz, sampleGuide.xyz), 0.0), 24.0); - float depthWeight = exp(-abs(sampleGuide.w - centerGuide.w) / - max(0.05, centerGuide.w * 0.02)); - float weight = weights[i] * edgeWeight * normalWeight * depthWeight; + float4 sampleAlbedoRoughness = + albedoRoughnessTexture.read(uint2(samplePosition)); + float sampleLuminance = + dot(sampleColor, float3(0.2126, 0.7152, 0.0722)); + float roughness = clamp(centerAlbedoRoughness.w, 0.0, 1.0); + float luminanceScale = mix(2.5, 7.0, roughness); + float luminanceDifference = + abs(sampleLuminance - centerLuminance) / + max(1.0, max(sampleLuminance, centerLuminance)); + float edgeWeight = exp(-luminanceDifference * luminanceScale); + bool sampleSurface = sampleGuide.w > 0.0; + float normalWeight = centerSurface == sampleSurface ? 1.0 : 0.0; + float depthWeight = normalWeight; + float albedoWeight = normalWeight; + if (centerSurface && sampleSurface) { + float sampleNormalLength = dot(sampleGuide.xyz, sampleGuide.xyz); + if (centerNormalLength > 1e-6 && sampleNormalLength > 1e-6) { + float normalSimilarity = + dot(centerGuide.xyz * rsqrt(centerNormalLength), + sampleGuide.xyz * rsqrt(sampleNormalLength)); + normalWeight *= pow(max(normalSimilarity, 0.0), 24.0); + } else if (i != 0) { + normalWeight = 0.0; + } + float depthScale = max(0.01, abs(centerGuide.w) * 0.01) * + max(float(parameters.stepWidth), 1.0); + depthWeight *= + exp(-abs(sampleGuide.w - centerGuide.w) / depthScale); + albedoWeight *= exp(-length(sampleAlbedoRoughness.xyz - + centerAlbedoRoughness.xyz) * + 8.0); + } + float weight = weights[i] * edgeWeight * normalWeight * depthWeight * + albedoWeight; filtered += sampleColor * weight; totalWeight += weight; } - float3 result = filtered / max(totalWeight, 0.0001); + float3 result = totalWeight > 0.0001 ? filtered / totalWeight : center; float brightness = dot(result, float3(0.2126, 0.7152, 0.0722)); - float contribution = smoothstep(0.5, 1.5, brightness); + constexpr float bloomThreshold = 0.8; + constexpr float bloomKnee = 0.35; + float soft = clamp(brightness - bloomThreshold + bloomKnee, 0.0, + bloomKnee * 2.0); + soft = soft * soft / max(bloomKnee * 4.0, 0.00001); + float contribution = max(brightness - 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/object.h b/include/atlas/object.h index 74f6acbd..ff99b023 100644 --- a/include/atlas/object.h +++ b/include/atlas/object.h @@ -1057,6 +1057,9 @@ class Model : public GameObject { void processNode(aiNode *node, const aiScene *scene, glm::mat4 parentTransform, std::unordered_map &textureCache); + void preloadMaterialTextures( + const aiScene *scene, + std::unordered_map &textureCache); CoreObject processMesh(aiMesh *mesh, const aiScene *scene, const glm::mat4 &transform, std::unordered_map &textureCache); diff --git a/include/atlas/runtime/context.h b/include/atlas/runtime/context.h index ec43f43c..20dac9e2 100644 --- a/include/atlas/runtime/context.h +++ b/include/atlas/runtime/context.h @@ -85,6 +85,7 @@ class Context { bool cameraAutomaticMoving = false; bool editorCameraFocused = false; bool editorRuntime = false; + bool materialPreviewRuntime = false; std::unique_ptr window; std::vector> objects; @@ -126,6 +127,8 @@ class Context { bool isEditorCameraFocused() const { return editorCameraFocused; } bool setEditorControlMode(int mode); bool setEditorShadingMode(int mode); + bool setEditorPathTracingPreview(bool enabled); + std::string getPathTracingError() const; float frameRate() const; bool editorPointerEvent(int action, float x, float y, int button, float scale); @@ -151,6 +154,12 @@ class Context { bool setPropertySync(const json &target, const json &source); bool clearPropertySync(const json &target); bool setObjectMaterial(int id, const std::string &path); + bool initializeMaterialPreview(const std::string &definition, + const std::string &baseDir, + int environmentMode); + bool setMaterialPreviewMaterial(const std::string &definition, + const std::string &baseDir); + bool setMaterialPreviewEnvironment(int mode); int addObjectComponent(int id, const json &component); bool removeObjectComponent(int id, int componentIndex); bool controlObjectAudio(int id, int componentIndex, @@ -181,6 +190,9 @@ std::shared_ptr makeHiddenContext(std::string projectFile); std::shared_ptr makeContextForMetalView(std::string projectFile, void *metalView, CoreWindowReference sdlInputWindow = nullptr); +std::shared_ptr +makeMaterialPreviewContextForMetalView(std::string projectFile, + void *metalView); void runProjectInMetalView(std::string projectFile, void *metalView, CoreWindowReference sdlInputWindow = nullptr); std::shared_ptr makeContextForMetalViewNonBlocking( diff --git a/include/atlas/texture.h b/include/atlas/texture.h index 03ad9bda..ad625fbc 100644 --- a/include/atlas/texture.h +++ b/include/atlas/texture.h @@ -499,6 +499,7 @@ class RenderTarget : public Renderable { RenderTarget(Window &window, RenderTargetType type = RenderTargetType::Scene, int resolution = 1024); + void resize(Window &window); /** * @brief Displays the render target in the window. @@ -590,6 +591,7 @@ class RenderTarget : public Renderable { std::shared_ptr resolveFb = nullptr; std::shared_ptr renderbuffer = nullptr; std::vector> effects; + int creationResolution = 1024; friend class Window; friend struct Fluid; diff --git a/include/atlas/window.h b/include/atlas/window.h index f02aad36..0f0df96a 100644 --- a/include/atlas/window.h +++ b/include/atlas/window.h @@ -437,6 +437,7 @@ class Window { * @return (bool) True while rendering should continue. */ bool stepFrame(); + void activateRenderingContext(); void resize(int width, int height, float scale = 1.0f); void setEditorControlsEnabled(bool enabled); bool areEditorControlsEnabled() const { return editorControlsEnabled; } @@ -505,6 +506,8 @@ class Window { #ifdef METAL void enableGlobalIllumination(); void enablePathTracing(); + bool setEditorPathTracingPreview(bool enabled); + const std::string &getPathTracingError() const; #endif /** diff --git a/include/editor/views/editorWindow.h b/include/editor/views/editorWindow.h index 939e6a35..0bfd1e2a 100644 --- a/include/editor/views/editorWindow.h +++ b/include/editor/views/editorWindow.h @@ -36,6 +36,7 @@ class QShowEvent; class QTimer; class QFileSystemWatcher; class QEvent; +class SplashScreen; class EditorWindow : public QMainWindow { Q_OBJECT @@ -88,6 +89,7 @@ class EditorWindow : public QMainWindow { QMenu* viewMenu = nullptr; QMenu* windowMenu = nullptr; QTimer* layoutSaveTimer = nullptr; + SplashScreen* assetLoadingSplash = nullptr; QFileSystemWatcher* scriptWatcher = nullptr; QByteArray defaultDockState; QString projectFile; diff --git a/include/editor/views/viewport.h b/include/editor/views/viewport.h index 1571cd81..f2979f30 100644 --- a/include/editor/views/viewport.h +++ b/include/editor/views/viewport.h @@ -80,6 +80,7 @@ class ViewportPanel : public QWidget { bool openRuntimeScene(const QString &path); bool saveRuntimeSceneAs(const QString &path); QString currentRuntimeScene() const; + QString runtimeProjectFile() const { return projectFile; } QString currentSceneSnapshot() const { return lastSceneSnapshot; } int selectedRuntimeObjectId() const; bool applyRuntimeMaterial(int id, const QString &path); @@ -95,6 +96,7 @@ class ViewportPanel : public QWidget { void stopRuntimePlayback(); void reloadRuntime(); void setRuntimeShadingMode(int mode); + void setPathTracingPreview(bool enabled); void setRuntimeControlMode(int mode); void toggleTransformSpace(); void toggleTransformSnapping(); @@ -111,6 +113,9 @@ class ViewportPanel : public QWidget { void frameRateChanged(float framesPerSecond); void sceneDirtyChanged(bool dirty); void runtimeStartupFinished(bool success, const QString &message); + void runtimeLoadingStarted(); + void runtimeLoadingStatusChanged(const QString &status); + void runtimeLoadingFinished(); void transformHintChanged(const QString &hint); void sceneOpened(const QString &path); void transformSpaceChanged(bool local); @@ -136,7 +141,7 @@ class ViewportPanel : public QWidget { void scheduleRuntimeStart(); void startRuntime(); void stopRuntime(); - void stepRuntime(); + bool stepRuntime(); void resizeRuntime(); void sendPointerEvent(int action, float x, float y, int button); void refreshSceneSnapshot(); @@ -173,6 +178,7 @@ class ViewportPanel : public QWidget { int keyboardTransformAxes = 7; int playbackState = 0; int shadingMode = 0; + bool pathTracingPreview = true; int rightDragRuntimeButton = 0; }; diff --git a/include/opal/opal.h b/include/opal/opal.h index 057f5089..17199b5d 100644 --- a/include/opal/opal.h +++ b/include/opal/opal.h @@ -386,6 +386,7 @@ class Texture { TextureFormat format = TextureFormat::Rgba8; int width = 0; int height = 0; + uint mipLevels = 1; int samples = 1; // For multisampled textures #ifdef VULKAN @@ -1132,25 +1133,28 @@ struct PrimitiveVertex { class PrimitiveAccelerationStructure { public: - std::vector vertices; - std::vector indices; - + ~PrimitiveAccelerationStructure(); static std::shared_ptr create(const std::vector &vertices, const std::vector &indices); + static std::shared_ptr + create(const std::vector &positions, + const std::vector &indices); + static std::shared_ptr + create(const std::vector> &positions, + const std::vector> &indices); bool isBuilt = false; private: friend class CommandBuffer; friend class InstanceAccelerationStructure; - std::shared_ptr asBuffer; std::shared_ptr scratch; MTL::AccelerationStructureDescriptor *blasDescriptor = nullptr; MTL::AccelerationStructure *blas = nullptr; - std::shared_ptr vertexBuffer; - std::shared_ptr indexBuffer; + std::vector> vertexBuffers; + std::vector> indexBuffers; }; static inline void writeMetalTransform3x4(const glm::mat4 &M, float out3x4[12]); @@ -1165,6 +1169,7 @@ struct AccelerationStructureInstance { class InstanceAccelerationStructure { public: + ~InstanceAccelerationStructure(); static std::shared_ptr create(const std::vector &instances); @@ -1195,6 +1200,7 @@ class CommandBuffer { const std::shared_ptr &writeFramebuffer); void endPass(); void commit(); + void waitForSubmittedWork(); // The different commands void bindPipeline(const std::shared_ptr &pipeline); @@ -1230,6 +1236,12 @@ class CommandBuffer { void buildPrimitiveAccelerationStructure( const std::shared_ptr &blas); + std::shared_ptr + buildAccelerationStructures( + const std::vector> + &blases, + const std::vector &instances); + void bindPrimitiveAccelerationStructure( const std::shared_ptr &blas, uint32_t binding); diff --git a/include/photon/illuminate.h b/include/photon/illuminate.h index 18363de3..2caa9c9d 100644 --- a/include/photon/illuminate.h +++ b/include/photon/illuminate.h @@ -15,6 +15,7 @@ #include "atlas/units.h" #include "opal/opal.h" #include +#include #include #include #include @@ -75,11 +76,11 @@ class PathTracing { public: #ifdef METAL /** @brief Runs one path tracing pass into the active output texture. */ - void render(const std::shared_ptr &commandBuffer, + bool render(const std::shared_ptr &commandBuffer, const std::shared_ptr &output, const std::shared_ptr &brightOutput); /** @brief Rebuilds BLAS/TLAS data for the current scene geometry. */ - void buildAccelerationStructure( + bool buildAccelerationStructure( const std::shared_ptr &commandBuffer); /** @brief Uploads light lists used by path tracing shaders. */ bool createLightBuffers(); @@ -87,6 +88,7 @@ class PathTracing { void init(); /** @brief Resizes path tracing output and history textures. */ void resizeOutput(int width, int height); + const std::string &getLastError() const { return lastError; } /** @brief Current frame output texture. */ std::shared_ptr pathTracingTexturePrev; @@ -94,9 +96,9 @@ class PathTracing { /** @brief Rays traced per pixel each dispatch. */ int raysPerPixel = 1; /** @brief Maximum bounce count for indirect transport. */ - int maxBounces = 1; + int maxBounces = 6; /** @brief Scalar multiplier for indirect lighting contribution. */ - float indirectStrength = 0.55f; + float indirectStrength = 1.0f; /** @brief Whether normal maps are evaluated during shading. */ bool sampleNormalMaps = true; /** @brief Strength multiplier applied to sampled normal maps. */ @@ -112,8 +114,10 @@ class PathTracing { std::shared_ptr meshInfo; std::shared_ptr materialBuffer; std::shared_ptr instanceDataBuffer; + std::shared_ptr blasPrimitiveOffsets; std::vector> materialTextures; - std::shared_ptr sceneTLAS; + std::vector> materialTextureBindings; + std::shared_ptr sceneBLAS; std::shared_ptr pathTracingPipeline; std::shared_ptr pathDenoisePipeline; std::shared_ptr computePathTracer; @@ -121,24 +125,35 @@ class PathTracing { std::array, 2> denoiseTextures; std::array, 4> pathTracingAovTextures; std::shared_ptr pathTracingHistoryGuide; - std::unordered_map> - objectBLAS; + std::vector cachedBLASPrimitiveOffsets; std::vector cachedObjects; + std::vector cachedSceneObjects; std::vector cachedInstanceTransforms; std::vector cachedObjectStateHashes; + std::vector cachedSceneObjectStateHashes; uint64_t cachedLightHash = 0; int frameIndex = 0; int outputWidth = 0; int outputHeight = 0; + int interactiveFramesRemaining = 0; + bool interactive = false; + bool accelerationBuildFailed = false; + std::string lastError; glm::mat4 cachedInvViewProj = glm::mat4(1.0f); glm::mat4 previousViewProj = glm::mat4(1.0f); glm::vec3 cachedDirectionalLightDirection = glm::vec3(0.0f, -1.0f, 0.0f); glm::vec3 cachedDirectionalLightColor = glm::vec3(1.0f, 1.0f, 1.0f); float cachedDirectionalLightIntensity = -1.0f; + glm::vec3 cachedAmbientColor = glm::vec3(-1.0f); + float cachedAmbientIntensity = -1.0f; int cachedDirectionalLightCount = -1; uint64_t cachedSkyboxTextureId = 0; + glm::vec3 cachedAtmosphereSunDirection = glm::vec3(0.0f); + glm::vec3 cachedAtmosphereSunColor = glm::vec3(0.0f); + float cachedAtmosphereSunIntensity = -1.0f; + float cachedAtmosphereSunSize = -1.0f; + int cachedAtmosphereEnabled = -1; friend class ::Window; #endif diff --git a/opal/command_buffer.cpp b/opal/command_buffer.cpp index 6268f59a..1012585e 100644 --- a/opal/command_buffer.cpp +++ b/opal/command_buffer.cpp @@ -638,6 +638,17 @@ void uploadUniformBuffers(const std::shared_ptr &pipeline, return; } + if (bytes.size() <= 4096) { + if (stage == metal::MetalProgramStage::Fragment) { + encoder->setFragmentBytes(bytes.data(), bytes.size(), + binding.index); + } else { + encoder->setVertexBytes(bytes.data(), bytes.size(), + binding.index); + } + return; + } + MTL::Buffer *inlineBuffer = device->newBuffer(bytes.data(), static_cast(alignUp( @@ -843,6 +854,11 @@ void uploadComputeUniformBuffers(const std::shared_ptr &pipeline, continue; } + if (bytes.size() <= 4096) { + encoder->setBytes(bytes.data(), bytes.size(), binding.index); + continue; + } + MTL::Buffer *inlineBuffer = device->newBuffer(bytes.data(), static_cast(alignUp( @@ -1173,23 +1189,42 @@ void CommandBuffer::start() { vkResetFences(device->logicalDevice, 1, &inFlightFences[currentFrame]); #elif defined(METAL) auto &state = metal::commandBufferState(this); - state.inFlightCommandBuffers.erase( - std::remove_if(state.inFlightCommandBuffers.begin(), - state.inFlightCommandBuffers.end(), - [](MTL::CommandBuffer *buffer) { - if (buffer->status() < MTL::CommandBufferStatusCompleted) { - return false; - } - buffer->release(); - return true; - }), - state.inFlightCommandBuffers.end()); + for (size_t i = 0; i < state.inFlightCommandBuffers.size();) { + auto *buffer = state.inFlightCommandBuffers[i]; + if (buffer->status() < MTL::CommandBufferStatusCompleted) { + ++i; + continue; + } + if (buffer->status() == MTL::CommandBufferStatusError) { + auto *error = buffer->error(); + const char *description = + error != nullptr && error->localizedDescription() != nullptr + ? error->localizedDescription()->utf8String() + : "Unknown Metal command buffer error"; + atlas_error(std::string("Metal GPU command failed: ") + + description); + } + buffer->release(); + state.inFlightCommandBuffers.erase( + state.inFlightCommandBuffers.begin() + i); + state.inFlightResources.erase(state.inFlightResources.begin() + i); + } if (state.inFlightCommandBuffers.size() >= 3) { auto *oldest = state.inFlightCommandBuffers.front(); oldest->waitUntilCompleted(); + if (oldest->status() == MTL::CommandBufferStatusError) { + auto *error = oldest->error(); + const char *description = + error != nullptr && error->localizedDescription() != nullptr + ? error->localizedDescription()->utf8String() + : "Unknown Metal command buffer error"; + atlas_error(std::string("Metal GPU command failed: ") + + description); + } oldest->release(); state.inFlightCommandBuffers.erase( state.inFlightCommandBuffers.begin()); + state.inFlightResources.erase(state.inFlightResources.begin()); } if (state.autoreleasePool != nullptr) { state.autoreleasePool->release(); @@ -1527,6 +1562,8 @@ void CommandBuffer::commit() { state.commandBuffer->retain(); state.inFlightCommandBuffers.push_back(state.commandBuffer); + state.inFlightResources.push_back(std::move(state.pendingResources)); + state.pendingResources.clear(); state.commandBuffer->commit(); state.commandBuffer = nullptr; state.passDescriptor = nullptr; @@ -1541,6 +1578,27 @@ void CommandBuffer::commit() { #endif } +void CommandBuffer::waitForSubmittedWork() { +#ifdef METAL + auto &state = metal::commandBufferState(this); + for (auto *submitted : state.inFlightCommandBuffers) { + submitted->waitUntilCompleted(); + if (submitted->status() == MTL::CommandBufferStatusError) { + auto *error = submitted->error(); + const char *description = + error != nullptr && error->localizedDescription() != nullptr + ? error->localizedDescription()->utf8String() + : "Unknown Metal command buffer error"; + atlas_error(std::string("Metal GPU command failed: ") + + description); + } + submitted->release(); + } + state.inFlightCommandBuffers.clear(); + state.inFlightResources.clear(); +#endif +} + void CommandBuffer::bindPipeline(const std::shared_ptr &pipeline) { #ifdef METAL metal::pipelineState(pipeline.get()).suppressTextureReset = true; @@ -2111,6 +2169,29 @@ void CommandBuffer::dispatch(uint threadCountX, uint threadCountY, deviceState.device); bindComputeTextures(boundPipeline, state.computeEncoder, deviceState.device); + auto &pipelineState = metal::pipelineState(boundPipeline.get()); + for (const auto &[binding, accelerationStructure] : + pipelineState.primitiveAccelerationStructures) { + if (accelerationStructure == nullptr || + accelerationStructure->blas == nullptr || + !accelerationStructure->isBuilt) { + throw std::runtime_error( + "Metal primitive acceleration structure is unavailable"); + } + state.computeEncoder->setAccelerationStructure( + accelerationStructure->blas, binding); + } + for (const auto &[binding, accelerationStructure] : + pipelineState.instanceAccelerationStructures) { + if (accelerationStructure == nullptr || + accelerationStructure->tlas == nullptr || + !accelerationStructure->isBuilt) { + throw std::runtime_error( + "Metal instance acceleration structure is unavailable"); + } + state.computeEncoder->setAccelerationStructure( + accelerationStructure->tlas, binding); + } NS::UInteger tgX = static_cast(boundPipeline->getComputeThreadgroupSizeX()); @@ -2171,7 +2252,8 @@ void CommandBuffer::generateMipmaps(const std::shared_ptr &texture) { #elif defined(METAL) auto &state = metal::commandBufferState(this); auto &textureState = metal::textureState(texture.get()); - if (state.commandBuffer == nullptr || textureState.texture == nullptr) { + if (state.commandBuffer == nullptr || textureState.texture == nullptr || + textureState.texture->mipmapLevelCount() <= 1) { return; } if (state.encoder != nullptr) { @@ -2316,21 +2398,25 @@ void CommandBuffer::clear(float r, float g, float b, float a, float depth) { void CommandBuffer::bindPrimitiveAccelerationStructure( const std::shared_ptr &as, uint32_t binding) { - auto &state = metal::commandBufferState(this); - if (state.computeEncoder == nullptr) { - state.computeEncoder = state.commandBuffer->computeCommandEncoder(); + if (boundPipeline == nullptr) { + throw std::runtime_error( + "Cannot bind an acceleration structure without a pipeline"); } - state.computeEncoder->setAccelerationStructure(as->blas, binding); + auto &pipelineState = metal::pipelineState(boundPipeline.get()); + pipelineState.instanceAccelerationStructures.erase(binding); + pipelineState.primitiveAccelerationStructures[binding] = as; } void CommandBuffer::bindInstanceAccelerationStructure( const std::shared_ptr &as, uint32_t binding) { - auto &state = metal::commandBufferState(this); - if (state.computeEncoder == nullptr) { - state.computeEncoder = state.commandBuffer->computeCommandEncoder(); + if (boundPipeline == nullptr) { + throw std::runtime_error( + "Cannot bind an acceleration structure without a pipeline"); } - state.computeEncoder->setAccelerationStructure(as->tlas, binding); + auto &pipelineState = metal::pipelineState(boundPipeline.get()); + pipelineState.primitiveAccelerationStructures.erase(binding); + pipelineState.instanceAccelerationStructures[binding] = as; } #endif diff --git a/opal/metal_state.cpp b/opal/metal_state.cpp index 7736ae4b..66bc58c3 100644 --- a/opal/metal_state.cpp +++ b/opal/metal_state.cpp @@ -801,6 +801,8 @@ void releaseCommandBufferState(CommandBuffer *commandBuffer) { submitted->release(); } state.inFlightCommandBuffers.clear(); + state.inFlightResources.clear(); + state.pendingResources.clear(); state.commandBuffer = nullptr; state.drawable = nullptr; state.boundVertexTextures.fill(nullptr); diff --git a/opal/metal_state.h b/opal/metal_state.h index f3dab6f5..a9187561 100644 --- a/opal/metal_state.h +++ b/opal/metal_state.h @@ -130,6 +130,10 @@ struct PipelineState { MTL::Buffer *textureArgumentBuffer = nullptr; uint32_t textureArgumentBufferIndex = 0; std::vector> textureArgumentTextures; + std::unordered_map> + primitiveAccelerationStructures; + std::unordered_map> + instanceAccelerationStructures; MTL::PrimitiveType primitiveType = MTL::PrimitiveTypeTriangle; MTL::CullMode cullMode = MTL::CullModeBack; MTL::Winding frontFace = MTL::WindingCounterClockwise; @@ -159,6 +163,8 @@ struct CommandBufferState { NS::AutoreleasePool *autoreleasePool = nullptr; MTL::CommandBuffer *commandBuffer = nullptr; std::vector inFlightCommandBuffers; + std::vector>> inFlightResources; + std::vector> pendingResources; MTL::RenderCommandEncoder *encoder = nullptr; MTL::ComputeCommandEncoder *computeEncoder = nullptr; MTL::RenderPassDescriptor *passDescriptor = nullptr; diff --git a/opal/ray_tracing.cpp b/opal/ray_tracing.cpp index 903ab7ad..baad0947 100644 --- a/opal/ray_tracing.cpp +++ b/opal/ray_tracing.cpp @@ -17,63 +17,126 @@ #include "Metal/Metal.hpp" #include "metal_state.h" +opal::PrimitiveAccelerationStructure::~PrimitiveAccelerationStructure() { + if (blasDescriptor != nullptr) { + blasDescriptor->release(); + } + if (blas != nullptr) { + blas->release(); + } +} + std::shared_ptr opal::PrimitiveAccelerationStructure::create( const std::vector &vertices, const std::vector &indices) { + std::vector positions; + positions.reserve(vertices.size() * 3); + for (const auto &vertex : vertices) { + positions.push_back(vertex.position[0]); + positions.push_back(vertex.position[1]); + positions.push_back(vertex.position[2]); + } + return create(positions, indices); +} + +std::shared_ptr +opal::PrimitiveAccelerationStructure::create( + const std::vector &positions, const std::vector &indices) { + return create(std::vector>{positions}, + std::vector>{indices}); +} + +std::shared_ptr +opal::PrimitiveAccelerationStructure::create( + const std::vector> &positions, + const std::vector> &indices) { + if (positions.empty() || positions.size() != indices.size()) { + return nullptr; + } auto blas = std::make_shared(); - blas->vertices = vertices; - blas->indices = indices; auto &deviceState = metal::deviceState(Device::globalInstance); - blas->vertexBuffer = std::shared_ptr( - deviceState.device->newBuffer(vertices.data(), - vertices.size() * sizeof(PrimitiveVertex), - MTL::ResourceStorageModeShared), - [](MTL::Buffer *b) { - if (b) - b->release(); - }); - - blas->indexBuffer = std::shared_ptr( - deviceState.device->newBuffer(indices.data(), - indices.size() * sizeof(uint32_t), - MTL::ResourceStorageModeShared), - [](MTL::Buffer *b) { - if (b) - b->release(); - }); - - auto *triDesc = - MTL::AccelerationStructureTriangleGeometryDescriptor::descriptor(); - - triDesc->setVertexBuffer(blas->vertexBuffer.get()); - triDesc->setVertexStride(sizeof(PrimitiveVertex)); - triDesc->setVertexFormat(MTL::AttributeFormatFloat3); - triDesc->setVertexBufferOffset(offsetof(PrimitiveVertex, position)); - - triDesc->setIndexBuffer(blas->indexBuffer.get()); - triDesc->setIndexType(MTL::IndexType::IndexTypeUInt32); - triDesc->setTriangleCount(indices.size() / 3); + std::vector descriptors; + descriptors.reserve(positions.size()); + blas->vertexBuffers.reserve(positions.size()); + blas->indexBuffers.reserve(indices.size()); + for (size_t geometryIndex = 0; geometryIndex < positions.size(); + ++geometryIndex) { + const auto &geometryPositions = positions[geometryIndex]; + const auto &geometryIndices = indices[geometryIndex]; + if (geometryPositions.size() < 9 || geometryPositions.size() % 3 != 0 || + geometryIndices.size() < 3 || geometryIndices.size() % 3 != 0) { + return nullptr; + } + + auto vertexBuffer = std::shared_ptr( + deviceState.device->newBuffer(geometryPositions.data(), + geometryPositions.size() * + sizeof(float), + MTL::ResourceStorageModeShared), + [](MTL::Buffer *buffer) { + if (buffer != nullptr) { + buffer->release(); + } + }); + auto indexBuffer = std::shared_ptr( + deviceState.device->newBuffer(geometryIndices.data(), + geometryIndices.size() * + sizeof(uint32_t), + MTL::ResourceStorageModeShared), + [](MTL::Buffer *buffer) { + if (buffer != nullptr) { + buffer->release(); + } + }); + if (vertexBuffer == nullptr || indexBuffer == nullptr) { + return nullptr; + } + + auto *triangleDescriptor = + MTL::AccelerationStructureTriangleGeometryDescriptor::descriptor(); + triangleDescriptor->setVertexBuffer(vertexBuffer.get()); + triangleDescriptor->setVertexStride(sizeof(float) * 3); + triangleDescriptor->setVertexFormat(MTL::AttributeFormatFloat3); + triangleDescriptor->setVertexBufferOffset(0); + triangleDescriptor->setIndexBuffer(indexBuffer.get()); + triangleDescriptor->setIndexType(MTL::IndexType::IndexTypeUInt32); + triangleDescriptor->setTriangleCount(geometryIndices.size() / 3); + triangleDescriptor->setOpaque(true); + descriptors.push_back(triangleDescriptor); + blas->vertexBuffers.push_back(std::move(vertexBuffer)); + blas->indexBuffers.push_back(std::move(indexBuffer)); + } auto *blasDesc = MTL::PrimitiveAccelerationStructureDescriptor::descriptor()->retain(); - NS::Array *geoms = NS::Array::array((NS::Object **)&triDesc, 1); + NS::Array *geoms = + NS::Array::array((NS::Object **)descriptors.data(), descriptors.size()); blasDesc->setGeometryDescriptors(geoms); + blasDesc->setUsage(MTL::AccelerationStructureUsagePreferFastIntersection); MTL::AccelerationStructureSizes sizes = deviceState.device->accelerationStructureSizes(blasDesc); - blas->asBuffer = Buffer::create(BufferUsage::GeneralPurpose, - sizes.accelerationStructureSize); - blas->scratch = Buffer::create(BufferUsage::GeneralPurpose, - sizes.buildScratchBufferSize); + try { + blas->scratch = Buffer::create(BufferUsage::GeneralPurpose, + sizes.buildScratchBufferSize); + } catch (const std::runtime_error &) { + blasDesc->release(); + return nullptr; + } MTL::AccelerationStructure *blasPtr = deviceState.device->newAccelerationStructure( sizes.accelerationStructureSize); + if (blasPtr == nullptr) { + blasDesc->release(); + return nullptr; + } + blas->blas = blasPtr; blas->blasDescriptor = blasDesc; @@ -105,6 +168,87 @@ void opal::CommandBuffer::buildPrimitiveAccelerationStructure( asEnc->endEncoding(); blas->isBuilt = true; + state.pendingResources.emplace_back(blas->scratch); + for (const auto &vertexBuffer : blas->vertexBuffers) { + state.pendingResources.emplace_back(vertexBuffer); + } + for (const auto &indexBuffer : blas->indexBuffers) { + state.pendingResources.emplace_back(indexBuffer); + } + blas->scratch.reset(); + blas->vertexBuffers.clear(); + blas->indexBuffers.clear(); +} + +std::shared_ptr +opal::CommandBuffer::buildAccelerationStructures( + const std::vector> &blases, + const std::vector &instances) { + if (blases.empty() || instances.empty()) { + return nullptr; + } + + auto &deviceState = metal::deviceState(Device::globalInstance); + auto &state = metal::commandBufferState(this); + if (state.encoder != nullptr) { + state.encoder->endEncoding(); + state.encoder = nullptr; + state.textureBindingsInitialized = false; + } + if (state.commandBuffer == nullptr) { + state.commandBuffer = deviceState.queue->commandBuffer(); + } + if (state.computeEncoder != nullptr) { + state.computeEncoder->endEncoding(); + state.computeEncoder = nullptr; + } + + auto *asEnc = state.commandBuffer->accelerationStructureCommandEncoder(); + for (const auto &blas : blases) { + if (blas == nullptr || blas->scratch == nullptr || + blas->vertexBuffers.empty() || blas->indexBuffers.empty()) { + asEnc->endEncoding(); + return nullptr; + } + auto &scratchBuffer = metal::bufferState(blas->scratch.get()); + asEnc->buildAccelerationStructure(blas->blas, blas->blasDescriptor, + scratchBuffer.buffer, 0); + blas->isBuilt = true; + state.pendingResources.emplace_back(blas->scratch); + for (const auto &vertexBuffer : blas->vertexBuffers) { + state.pendingResources.emplace_back(vertexBuffer); + } + for (const auto &indexBuffer : blas->indexBuffers) { + state.pendingResources.emplace_back(indexBuffer); + } + blas->scratch.reset(); + blas->vertexBuffers.clear(); + blas->indexBuffers.clear(); + } + + std::shared_ptr tlas; + try { + tlas = InstanceAccelerationStructure::create(instances); + } catch (...) { + asEnc->endEncoding(); + throw; + } + if (tlas == nullptr) { + asEnc->endEncoding(); + return nullptr; + } + + auto &scratchState = metal::bufferState(tlas->scratch.get()); + asEnc->buildAccelerationStructure(tlas->tlas, tlas->tlasDescriptor, + scratchState.buffer, 0); + asEnc->endEncoding(); + + tlas->isBuilt = true; + state.pendingResources.emplace_back(tlas->scratch); + state.pendingResources.emplace_back(tlas->instanceBuffer); + tlas->scratch.reset(); + tlas->instanceBuffer.reset(); + return tlas; } static inline void opal::writeMetalTransform3x4(const glm::mat4 &M, @@ -123,16 +267,28 @@ static inline void opal::writeMetalTransform3x4(const glm::mat4 &M, out[11] = M[3][2]; } +opal::InstanceAccelerationStructure::~InstanceAccelerationStructure() { + if (tlasDescriptor != nullptr) { + tlasDescriptor->release(); + } + if (tlas != nullptr) { + tlas->release(); + } +} + std::shared_ptr opal::InstanceAccelerationStructure::create( const std::vector &instances) { + if (instances.empty()) { + return nullptr; + } auto tlas = std::make_shared(); tlas->instances = instances; tlas->blasRefs.reserve(instances.size()); tlas->blasPtrs.reserve(instances.size()); - std::vector descs; + std::vector descs; descs.resize(instances.size()); for (size_t i = 0; i < instances.size(); ++i) { @@ -154,25 +310,32 @@ opal::InstanceAccelerationStructure::create( d.accelerationStructureIndex = (uint32_t)i; d.mask = inst.mask ? inst.mask : 0xFF; - d.userID = inst.instanceId; - d.options = - inst.cullDisable - ? MTL::AccelerationStructureInstanceOptionDisableTriangleCulling - : 0; + d.options = MTL::AccelerationStructureInstanceOptionOpaque; + if (inst.cullDisable) { + d.options |= + MTL::AccelerationStructureInstanceOptionDisableTriangleCulling; + } d.intersectionFunctionTableOffset = 0; } tlas->instanceBuffer = Buffer::create(BufferUsage::GeneralPurpose, descs.size() * sizeof(descs[0]), descs.data()); + if (tlas->instanceBuffer == nullptr) { + return nullptr; + } tlas->tlasDescriptor = MTL::InstanceAccelerationStructureDescriptor::descriptor()->retain(); + tlas->tlasDescriptor->setUsage( + MTL::AccelerationStructureUsagePreferFastIntersection); + tlas->tlasDescriptor->setInstanceDescriptorType( + MTL::AccelerationStructureInstanceDescriptorTypeDefault); auto &ib = metal::bufferState(tlas->instanceBuffer.get()); tlas->tlasDescriptor->setInstanceDescriptorBuffer(ib.buffer); tlas->tlasDescriptor->setInstanceDescriptorStride( - sizeof(MTL::AccelerationStructureUserIDInstanceDescriptor)); + sizeof(MTL::AccelerationStructureInstanceDescriptor)); tlas->tlasDescriptor->setInstanceCount((NS::UInteger)descs.size()); NS::Array *instancedAS = @@ -193,6 +356,10 @@ opal::InstanceAccelerationStructure::create( deviceState.device->newAccelerationStructure( sizes.accelerationStructureSize); + if (tlasPtr == nullptr) { + return nullptr; + } + tlas->tlas = tlasPtr; return tlas; @@ -225,6 +392,10 @@ void opal::CommandBuffer::buildInstanceAccelerationStructure( asEnc->endEncoding(); tlas->isBuilt = true; + state.pendingResources.emplace_back(tlas->scratch); + state.pendingResources.emplace_back(tlas->instanceBuffer); + tlas->scratch.reset(); + tlas->instanceBuffer.reset(); } #endif diff --git a/opal/texture.cpp b/opal/texture.cpp index b21b19dd..e8d62778 100644 --- a/opal/texture.cpp +++ b/opal/texture.cpp @@ -331,6 +331,9 @@ std::shared_ptr Texture::create(TextureType type, TextureFormat format, texture->format = format; texture->width = width; texture->height = height; + texture->mipLevels = type == TextureType::Texture2DMultisample + ? 1 + : std::max(1, mipLevels); const GLenum textureType = getGLTextureType(type); const GLenum glFormat = getGLInternalFormat(format); @@ -393,6 +396,9 @@ std::shared_ptr Texture::create(TextureType type, TextureFormat format, texture->format = format; texture->width = width; texture->height = height; + texture->mipLevels = type == TextureType::Texture2DMultisample + ? 1 + : std::max(1, mipLevels); texture->samples = (type == TextureType::Texture2DMultisample) ? static_cast(mipLevels) : 1; @@ -414,7 +420,7 @@ std::shared_ptr Texture::create(TextureType type, TextureFormat format, descriptor->setHeight(static_cast(std::max(height, 1))); descriptor->setDepth(1); descriptor->setMipmapLevelCount( - static_cast(std::max(1, mipLevels))); + static_cast(texture->mipLevels)); descriptor->setUsage(metal::textureUsageFor(type, format)); descriptor->setStorageMode(MTL::StorageModeShared); @@ -783,7 +789,8 @@ void Texture::generateMipmaps([[maybe_unused]] uint levels) { } auto &deviceState = metal::deviceState(Device::globalInstance); auto &state = metal::textureState(this); - if (deviceState.queue == nullptr || state.texture == nullptr) { + if (deviceState.queue == nullptr || state.texture == nullptr || + state.texture->mipmapLevelCount() <= 1) { return; } MTL::CommandBuffer *commandBuffer = deviceState.queue->commandBuffer(); @@ -791,7 +798,6 @@ void Texture::generateMipmaps([[maybe_unused]] uint levels) { blit->generateMipmaps(state.texture); blit->endEncoding(); commandBuffer->commit(); - commandBuffer->waitUntilCompleted(); #endif } diff --git a/photon/path_tracing.cpp b/photon/path_tracing.cpp index 1bb1bcbc..51382d66 100644 --- a/photon/path_tracing.cpp +++ b/photon/path_tracing.cpp @@ -10,12 +10,14 @@ #include "atlas/window.h" #include "atlas/core/shader.h" #include "atlas/object.h" +#include "atlas/tracer/log.h" #include "photon/illuminate.h" #include "opal/opal.h" #include #include #include #include +#include #include #include #include @@ -26,13 +28,9 @@ namespace { constexpr int kPathTracerMaxMaterialTextures = 256; constexpr int kPathTracerSkyboxTextureUnit = 60; +constexpr size_t kPathTracerMaxPrimitivesPerGeometry = 250000; std::shared_ptr createFallbackSkyboxTexture() { - constexpr unsigned char horizon[4] = {0, 0, 0, 255}; - constexpr unsigned char zenith[4] = {0, 0, 0, 255}; - constexpr unsigned char nadir[4] = {0, 0, 0, 255}; - const unsigned char *faceColors[6] = {horizon, horizon, zenith, - nadir, horizon, horizon}; auto texture = opal::Texture::create( opal::TextureType::TextureCubeMap, opal::TextureFormat::Rgba8, 1, 1, opal::TextureDataFormat::Rgba, nullptr, 1); @@ -44,13 +42,25 @@ std::shared_ptr createFallbackSkyboxTexture() { opal::TextureWrapMode::ClampToEdge); texture->setWrapMode(opal::TextureAxis::R, opal::TextureWrapMode::ClampToEdge); + constexpr unsigned char black[4] = {0, 0, 0, 255}; for (int face = 0; face < 6; ++face) { - texture->updateFace(face, faceColors[face], 1, 1, - opal::TextureDataFormat::Rgba); + texture->updateFace(face, black, 1, 1, opal::TextureDataFormat::Rgba); } return texture; } +std::shared_ptr createFallbackMaterialTexture() { + constexpr unsigned char white[4] = {255, 255, 255, 255}; + auto texture = opal::Texture::create( + opal::TextureType::Texture2D, opal::TextureFormat::Rgba8, 1, 1, + opal::TextureDataFormat::Rgba, white, 1); + texture->setFilterMode(opal::TextureFilterMode::Linear, + opal::TextureFilterMode::Linear); + texture->setWrapMode(opal::TextureAxis::S, opal::TextureWrapMode::Repeat); + texture->setWrapMode(opal::TextureAxis::T, opal::TextureWrapMode::Repeat); + return texture; +} + bool mat4ApproximatelyEqual(const glm::mat4 &a, const glm::mat4 &b, float epsilon) { for (int c = 0; c < 4; ++c) { @@ -63,7 +73,8 @@ bool mat4ApproximatelyEqual(const glm::mat4 &a, const glm::mat4 &b, return true; } -uint64_t pathTracingObjectStateHash(const CoreObject *object) { +uint64_t pathTracingObjectStateHash(const CoreObject *object, + const glm::mat4 &model) { uint64_t hash = 1469598103934665603ULL; auto append = [&hash](const void *data, size_t size) { const auto *bytes = static_cast(data); @@ -86,6 +97,7 @@ uint64_t pathTracingObjectStateHash(const CoreObject *object) { append(&material.textureOffset, sizeof(material.textureOffset)); append(&material.transmittance, sizeof(material.transmittance)); append(&material.ior, sizeof(material.ior)); + append(&model, sizeof(model)); const size_t vertexCount = object->vertices.size(); const size_t indexCount = object->indices.size(); append(&vertexCount, sizeof(vertexCount)); @@ -187,8 +199,17 @@ void collectPathTracingObjectsFromQueue( void photon::PathTracing::init() { materialTextures.clear(); - objectBLAS.clear(); + materialTextureBindings.clear(); + sceneBLAS.reset(); + blasPrimitiveOffsets.reset(); + cachedBLASPrimitiveOffsets.clear(); cachedObjects.clear(); + cachedSceneObjects.clear(); + cachedObjectStateHashes.clear(); + cachedSceneObjectStateHashes.clear(); + cachedInstanceTransforms.clear(); + accelerationBuildFailed = false; + lastError.clear(); ComputeShader pathTracerShader = ComputeShader::fromDefaultShader(AtlasComputeShader::PathTracer); @@ -220,23 +241,20 @@ void photon::PathTracing::init() { Texture::create(outputWidth, outputHeight, opal::TextureFormat::Rgba16F, opal::TextureDataFormat::Rgba, TextureType::Color)); for (auto &texture : denoiseTextures) { - texture = std::make_shared( - Texture::create(outputWidth, outputHeight, - opal::TextureFormat::Rgba16F, - opal::TextureDataFormat::Rgba, - TextureType::Color)); + texture = std::make_shared(Texture::create( + outputWidth, outputHeight, opal::TextureFormat::Rgba16F, + opal::TextureDataFormat::Rgba, TextureType::Color)); } for (auto &texture : pathTracingAovTextures) { - texture = std::make_shared( - Texture::create(outputWidth, outputHeight, - opal::TextureFormat::Rgba16F, - opal::TextureDataFormat::Rgba, - TextureType::Color)); + texture = std::make_shared(Texture::create( + outputWidth, outputHeight, opal::TextureFormat::Rgba16F, + opal::TextureDataFormat::Rgba, TextureType::Color)); } pathTracingHistoryGuide = std::make_shared( - Texture::create(outputWidth, outputHeight, - opal::TextureFormat::Rgba16F, + Texture::create(outputWidth, outputHeight, opal::TextureFormat::Rgba16F, opal::TextureDataFormat::Rgba, TextureType::Color)); + interactiveFramesRemaining = 4; + interactive = true; } void photon::PathTracing::resizeOutput(int width, int height) { @@ -253,27 +271,24 @@ void photon::PathTracing::resizeOutput(int width, int height) { Texture::create(outputWidth, outputHeight, opal::TextureFormat::Rgba16F, opal::TextureDataFormat::Rgba, TextureType::Color)); for (auto &texture : denoiseTextures) { - texture = std::make_shared( - Texture::create(outputWidth, outputHeight, - opal::TextureFormat::Rgba16F, - opal::TextureDataFormat::Rgba, - TextureType::Color)); + texture = std::make_shared(Texture::create( + outputWidth, outputHeight, opal::TextureFormat::Rgba16F, + opal::TextureDataFormat::Rgba, TextureType::Color)); } for (auto &texture : pathTracingAovTextures) { - texture = std::make_shared( - Texture::create(outputWidth, outputHeight, - opal::TextureFormat::Rgba16F, - opal::TextureDataFormat::Rgba, - TextureType::Color)); + texture = std::make_shared(Texture::create( + outputWidth, outputHeight, opal::TextureFormat::Rgba16F, + opal::TextureDataFormat::Rgba, TextureType::Color)); } pathTracingHistoryGuide = std::make_shared( - Texture::create(outputWidth, outputHeight, - opal::TextureFormat::Rgba16F, + Texture::create(outputWidth, outputHeight, opal::TextureFormat::Rgba16F, opal::TextureDataFormat::Rgba, TextureType::Color)); frameIndex = 0; + interactiveFramesRemaining = 4; + interactive = true; } -void photon::PathTracing::buildAccelerationStructure( +bool photon::PathTracing::buildAccelerationStructure( const std::shared_ptr &commandBuffer) { struct MaterialData { float albedo[4]; @@ -295,23 +310,20 @@ void photon::PathTracing::buildAccelerationStructure( float ior; float reflectivity; float _pad2; - }; - - struct MeshData { - uint vertexOffset; - uint indexOffset; - uint _pad0; - uint _pad1; + float textureScale[2]; + float textureOffset[2]; }; struct VertexData { + float position[3]; float normal[3]; float uv[2]; float tangent[3]; float bitangent[3]; }; - static_assert(sizeof(VertexData) == 44); + static_assert(sizeof(MaterialData) == 112); + static_assert(sizeof(VertexData) == 56); std::vector pathTracingObjects; std::unordered_set seenPathObjects; @@ -321,84 +333,137 @@ void photon::PathTracing::buildAccelerationStructure( collectPathTracingObjectsFromQueue(Window::mainWindow->firstRenderables, seenPathObjects, pathTracingObjects); } + std::vector sceneObjectStateHashes; + sceneObjectStateHashes.reserve(pathTracingObjects.size()); + for (const auto *object : pathTracingObjects) { + sceneObjectStateHashes.push_back( + pathTracingObjectStateHash(object, object->model)); + } + + bool needsRebuild = cachedSceneObjects != pathTracingObjects || + cachedSceneObjectStateHashes != sceneObjectStateHashes; + if (!needsRebuild && accelerationBuildFailed) { + return false; + } + if (needsRebuild) { + accelerationBuildFailed = false; + } std::vector traceableObjects; - traceableObjects.reserve(pathTracingObjects.size()); - for (auto *object : pathTracingObjects) { - if (object == nullptr) { - continue; - } - if (!object->canUseDeferredRendering()) { - continue; + if (needsRebuild) { + traceableObjects.reserve(pathTracingObjects.size()); + for (auto *object : pathTracingObjects) { + if (object == nullptr || !object->canUseDeferredRendering() || + object->vertices.size() < 3 || object->indices.size() < 3) { + continue; + } + if (object->indices.size() % 3 != 0 || + std::ranges::any_of(object->indices, [&](uint32_t index) { + return index >= object->vertices.size(); + })) { + continue; + } + traceableObjects.push_back(object); } - if (object->vertices.size() < 3 || object->indices.size() < 3) { - continue; + cachedSceneObjects = pathTracingObjects; + cachedSceneObjectStateHashes = sceneObjectStateHashes; + cachedObjects = traceableObjects; + cachedObjectStateHashes.clear(); + cachedObjectStateHashes.reserve(traceableObjects.size()); + for (const auto *object : traceableObjects) { + cachedObjectStateHashes.push_back( + pathTracingObjectStateHash(object, object->model)); } - traceableObjects.push_back(object); + } else { + traceableObjects = cachedObjects; + } + if (traceableObjects.empty()) { + lastError = pathTracingObjects.empty() + ? "No renderable scene geometry was found" + : "Scene geometry is not valid for path tracing"; + sceneBLAS.reset(); + accelerationBuildFailed = true; + return false; } std::vector materialData; std::vector allVertices; std::vector allIndices; - std::vector meshData; - - bool needsRebuild = - objectBLAS.empty() || cachedObjects.size() != traceableObjects.size(); - std::vector objectStateHashes; - objectStateHashes.reserve(traceableObjects.size()); - for (const auto *object : traceableObjects) { - objectStateHashes.push_back(pathTracingObjectStateHash(object)); - } - if (cachedObjectStateHashes != objectStateHashes) { - needsRebuild = true; - } - if (!needsRebuild) { - for (size_t i = 0; i < traceableObjects.size(); ++i) { - if (cachedObjects[i] != traceableObjects[i]) { - needsRebuild = true; - break; - } - } - } + std::vector primitiveObjects; int objectID = 0; if (needsRebuild) { - objectBLAS.clear(); + sceneBLAS.reset(); + cachedBLASPrimitiveOffsets.clear(); materialTextures.clear(); - cachedObjects = traceableObjects; - cachedObjectStateHashes = objectStateHashes; std::unordered_map textureSlots; + size_t totalVertexCount = 0; + size_t totalIndexCount = 0; + for (const auto *object : traceableObjects) { + totalVertexCount += object->vertices.size(); + totalIndexCount += object->indices.size(); + } + allVertices.reserve(totalVertexCount); + allIndices.reserve(totalIndexCount); + primitiveObjects.reserve(totalIndexCount / 3); + materialData.reserve(traceableObjects.size()); + + std::vector accelerationPositions; + std::vector accelerationIndices; + std::vector> accelerationPositionChunks; + std::vector> accelerationIndexChunks; + accelerationIndices.reserve(std::min( + totalIndexCount, kPathTracerMaxPrimitivesPerGeometry * size_t{3})); + uint32_t chunkPrimitiveOffset = 0; + auto flushAccelerationChunk = [&]() { + if (accelerationIndices.empty()) { + return true; + } + cachedBLASPrimitiveOffsets.push_back(chunkPrimitiveOffset); + accelerationPositionChunks.push_back( + std::move(accelerationPositions)); + accelerationIndexChunks.push_back(std::move(accelerationIndices)); + accelerationPositions = {}; + accelerationIndices = {}; + accelerationIndices.reserve( + std::min(totalIndexCount, + kPathTracerMaxPrimitivesPerGeometry * size_t{3})); + chunkPrimitiveOffset = + static_cast(primitiveObjects.size()); + return true; + }; for (auto *object : traceableObjects) { const auto &objectVertices = object->vertices; const auto &objectIndices = object->indices; + const size_t objectPrimitiveCount = objectIndices.size() / 3; + + if (!accelerationIndices.empty() && + accelerationIndices.size() / 3 + objectPrimitiveCount > + kPathTracerMaxPrimitivesPerGeometry && + !flushAccelerationChunk()) { + lastError = + "Failed to allocate a scene acceleration structure chunk"; + sceneBLAS.reset(); + accelerationBuildFailed = true; + return false; + } - std::vector vertices; - vertices.reserve(objectVertices.size()); - std::vector indices; - indices.reserve(objectIndices.size()); - - int vertexOffset = allVertices.size(); - int indexOffset = allIndices.size(); + const uint32_t vertexOffset = + static_cast(allVertices.size()); + const uint32_t accelerationVertexOffset = + static_cast(accelerationPositions.size() / 3); for (const auto &v : objectVertices) { - opal::PrimitiveVertex pv{}; - pv.position[0] = v.position.x; - pv.position[1] = v.position.y; - pv.position[2] = v.position.z; - pv.normal[0] = v.normal.x; - pv.normal[1] = v.normal.y; - pv.normal[2] = v.normal.z; - pv.uv[0] = v.textureCoordinate[0]; - pv.uv[1] = v.textureCoordinate[1]; - pv.tangent[0] = v.tangent.x; - pv.tangent[1] = v.tangent.y; - pv.tangent[2] = v.tangent.z; - pv.bitangent[0] = v.bitangent.x; - pv.bitangent[1] = v.bitangent.y; - pv.bitangent[2] = v.bitangent.z; - vertices.push_back(pv); + glm::vec4 worldPosition = + object->model * glm::vec4(v.position.toGlm(), 1.0f); + accelerationPositions.push_back(worldPosition.x); + accelerationPositions.push_back(worldPosition.y); + accelerationPositions.push_back(worldPosition.z); VertexData vd{}; + vd.position[0] = worldPosition.x; + vd.position[1] = worldPosition.y; + vd.position[2] = worldPosition.z; vd.normal[0] = v.normal.x; vd.normal[1] = v.normal.y; vd.normal[2] = v.normal.z; @@ -413,16 +478,17 @@ void photon::PathTracing::buildAccelerationStructure( allVertices.push_back(vd); } - indices = objectIndices; - for (auto &index : indices) { - allIndices.push_back(vertexOffset + index); + for (auto index : objectIndices) { + const uint32_t globalIndex = vertexOffset + index; + allIndices.push_back(globalIndex); + accelerationIndices.push_back(accelerationVertexOffset + index); + } + for (size_t primitive = 0; primitive < objectIndices.size() / 3; + ++primitive) { + primitiveObjects.push_back(static_cast(objectID)); } - auto blas = - opal::PrimitiveAccelerationStructure::create(vertices, indices); - objectBLAS[objectID] = blas; - - MaterialData data; + MaterialData data{}; data.albedo[0] = object->material.albedo.r; data.albedo[1] = object->material.albedo.g; data.albedo[2] = object->material.albedo.b; @@ -438,6 +504,10 @@ void photon::PathTracing::buildAccelerationStructure( data.transmittance = object->material.transmittance; data.reflectivity = object->material.reflectivity; data._pad2 = 0.0f; + data.textureScale[0] = object->material.textureScale[0]; + data.textureScale[1] = object->material.textureScale[1]; + data.textureOffset[0] = object->material.textureOffset[0]; + data.textureOffset[1] = object->material.textureOffset[1]; const bool useNormalMap = object->material.useNormalMap && sampleNormalMaps; const float normalStrength = std::max( @@ -485,18 +555,38 @@ void photon::PathTracing::buildAccelerationStructure( data._pad1[1] = 0; materialData.push_back(data); - MeshData mdata; - mdata.vertexOffset = vertexOffset; - mdata.indexOffset = indexOffset; - meshData.push_back(mdata); - objectID++; } - for (const auto &[_, blas] : objectBLAS) { - if (blas != nullptr) { - commandBuffer->buildPrimitiveAccelerationStructure(blas); - } + if (!flushAccelerationChunk()) { + lastError = + "Failed to allocate a scene acceleration structure chunk"; + sceneBLAS.reset(); + accelerationBuildFailed = true; + return false; + } + + if (primitiveObjects.empty() || + primitiveObjects.size() != allIndices.size() / 3) { + lastError = "Path tracing triangle metadata is inconsistent"; + sceneBLAS.reset(); + accelerationBuildFailed = true; + return false; + } + if (accelerationPositionChunks.empty() || + cachedBLASPrimitiveOffsets.size() != + accelerationPositionChunks.size()) { + lastError = "Path tracing acceleration chunks are inconsistent"; + sceneBLAS.reset(); + accelerationBuildFailed = true; + return false; + } + sceneBLAS = opal::PrimitiveAccelerationStructure::create( + accelerationPositionChunks, accelerationIndexChunks); + if (sceneBLAS == nullptr) { + lastError = "Failed to allocate the scene acceleration structure"; + accelerationBuildFailed = true; + return false; } materialBuffer = opal::Buffer::create( @@ -511,13 +601,39 @@ void photon::PathTracing::buildAccelerationStructure( opal::BufferUsage::ShaderRead, allIndices.size() * sizeof(uint32_t), allIndices.data()); - meshInfo = opal::Buffer::create(opal::BufferUsage::ShaderRead, - meshData.size() * sizeof(MeshData), - meshData.data()); + meshInfo = + opal::Buffer::create(opal::BufferUsage::ShaderRead, + primitiveObjects.size() * sizeof(uint32_t), + primitiveObjects.data()); + blasPrimitiveOffsets = opal::Buffer::create( + opal::BufferUsage::ShaderRead, + cachedBLASPrimitiveOffsets.size() * sizeof(uint32_t), + cachedBLASPrimitiveOffsets.data()); + if (materialBuffer == nullptr || globalVertices == nullptr || + globalIndices == nullptr || meshInfo == nullptr || + blasPrimitiveOffsets == nullptr) { + lastError = "Failed to allocate path tracing scene buffers"; + sceneBLAS.reset(); + accelerationBuildFailed = true; + return false; + } + static std::shared_ptr fallbackMaterialTexture = + createFallbackMaterialTexture(); + materialTextureBindings = materialTextures; + materialTextureBindings.resize(kPathTracerMaxMaterialTextures, + fallbackMaterialTexture); frameIndex = 0; } - std::vector instances; + if (!needsRebuild) { + if (sceneBLAS != nullptr && sceneBLAS->isBuilt) { + accelerationBuildFailed = false; + return true; + } + lastError = "The scene acceleration structure is unavailable"; + accelerationBuildFailed = true; + return false; + } struct InstanceData { float model[16]; @@ -526,28 +642,11 @@ void photon::PathTracing::buildAccelerationStructure( float normalCol2[4]; }; - std::vector instanceData; + std::vector instanceData(traceableObjects.size()); for (size_t objectIndex = 0; objectIndex < traceableObjects.size(); ++objectIndex) { auto *object = traceableObjects[objectIndex]; - auto it = objectBLAS.find(static_cast(objectIndex)); - if (it == objectBLAS.end()) { - continue; - } - auto blas = it->second; - if (blas == nullptr || !blas->isBuilt) { - continue; - } - - opal::AccelerationStructureInstance instance{}; - instance.blas = blas; - instance.transform = object->model; - instance.instanceId = static_cast(objectIndex); - instance.mask = 0xFF; - instance.cullDisable = false; - instances.push_back(instance); - InstanceData d{}; const glm::mat4 &m = object->model; memcpy(d.model, &m[0][0], sizeof(float) * 16); @@ -569,34 +668,32 @@ void photon::PathTracing::buildAccelerationStructure( d.normalCol2[1] = normalMatrix[2][1]; d.normalCol2[2] = normalMatrix[2][2]; d.normalCol2[3] = 0.0f; - instanceData.push_back(d); - } - - bool transformsChanged = needsRebuild || - cachedInstanceTransforms.size() != - traceableObjects.size(); - if (!transformsChanged) { - for (size_t i = 0; i < traceableObjects.size(); ++i) { - if (!mat4ApproximatelyEqual(cachedInstanceTransforms[i], - traceableObjects[i]->model, 0.000001f)) { - transformsChanged = true; - break; - } - } + instanceData[objectIndex] = d; } - if (transformsChanged) { - cachedInstanceTransforms.clear(); - cachedInstanceTransforms.reserve(traceableObjects.size()); - for (auto *object : traceableObjects) { - cachedInstanceTransforms.push_back(object->model); - } - instanceDataBuffer = opal::Buffer::create( - opal::BufferUsage::ShaderRead, - instanceData.size() * sizeof(InstanceData), instanceData.data()); - sceneTLAS = opal::InstanceAccelerationStructure::create(instances); - commandBuffer->buildInstanceAccelerationStructure(sceneTLAS); - frameIndex = 0; + + cachedInstanceTransforms.clear(); + cachedInstanceTransforms.reserve(traceableObjects.size()); + for (auto *object : traceableObjects) { + cachedInstanceTransforms.push_back(object->model); + } + instanceDataBuffer = opal::Buffer::create( + opal::BufferUsage::ShaderRead, + instanceData.size() * sizeof(InstanceData), instanceData.data()); + if (instanceDataBuffer == nullptr || sceneBLAS == nullptr) { + sceneBLAS.reset(); + lastError = "Failed to allocate the scene acceleration structure"; + accelerationBuildFailed = true; + return false; + } + commandBuffer->buildPrimitiveAccelerationStructure(sceneBLAS); + frameIndex = 0; + if (!sceneBLAS->isBuilt) { + lastError = "The scene acceleration structure is unavailable"; + accelerationBuildFailed = true; + return false; } + accelerationBuildFailed = false; + return true; } bool photon::PathTracing::createLightBuffers() { @@ -729,8 +826,10 @@ bool photon::PathTracing::createLightBuffers() { }; hashBytes(pointLightData.data(), pointLightData.size() * sizeof(PointLightData)); - hashBytes(spotLightData.data(), spotLightData.size() * sizeof(SpotLightData)); - hashBytes(areaLightData.data(), areaLightData.size() * sizeof(AreaLightData)); + hashBytes(spotLightData.data(), + spotLightData.size() * sizeof(SpotLightData)); + hashBytes(areaLightData.data(), + areaLightData.size() * sizeof(AreaLightData)); if (cachedLightHash == lightHash && pointLights != nullptr && spotLights != nullptr && areaLights != nullptr) { return false; @@ -748,27 +847,58 @@ bool photon::PathTracing::createLightBuffers() { return true; } -void photon::PathTracing::render( +bool photon::PathTracing::render( const std::shared_ptr &commandBuffer, const std::shared_ptr &output, const std::shared_ptr &brightOutput) { + auto fail = [&](const std::string &message) { + if (lastError != message) { + atlas_error("Path tracing: " + message); + } + lastError = message; + frameIndex = 0; + return false; + }; + + if (commandBuffer == nullptr || output == nullptr || + brightOutput == nullptr) { + return fail("The render target or command buffer is unavailable"); + } + if (Window::mainWindow == nullptr || + Window::mainWindow->getCamera() == nullptr) { + return fail("The active window or camera is unavailable"); + } + auto view = Window::mainWindow->getCamera()->calculateViewMatrix(); auto proj = Window::mainWindow->calculateProjectionMatrix(); auto invViewProj = glm::inverse(proj * view); auto viewProj = proj * view; + bool cameraChanged = false; if (frameIndex == 0) { cachedInvViewProj = invViewProj; previousViewProj = viewProj; } else if (!mat4ApproximatelyEqual(cachedInvViewProj, invViewProj, 0.00001f)) { + cameraChanged = true; frameIndex = 0; cachedInvViewProj = invViewProj; previousViewProj = viewProj; } + if (cameraChanged) { + interactiveFramesRemaining = 4; + } else if (interactiveFramesRemaining > 0) { + interactiveFramesRemaining--; + } + bool nextInteractive = cameraChanged || interactiveFramesRemaining > 0; + if (interactive != nextInteractive) { + interactive = nextInteractive; + frameIndex = 0; + } + pathTracingPipeline->setUniformMat4f("cam.invViewProj", invViewProj); pathTracingPipeline->setUniformMat4f("cam.prevViewProj", previousViewProj); pathTracingPipeline->setUniform3f( @@ -784,6 +914,7 @@ void photon::PathTracing::render( glm::vec3 directionalLightColor(1.0f, 1.0f, 1.0f); float directionalLightIntensity = 0.0f; float ambientIntensity = 0.0f; + glm::vec3 ambientColor(1.0f); glm::vec3 atmosphereSunDirection(0.0f, 1.0f, 0.0f); glm::vec3 atmosphereSunColor(1.0f, 0.95f, 0.8f); float atmosphereSunIntensity = 0.0f; @@ -806,6 +937,11 @@ void photon::PathTracing::render( ambientIntensity = scene->isAutomaticAmbientEnabled() ? scene->getAutomaticAmbientIntensity() : scene->getAmbientIntensity(); + Color sceneAmbientColor = scene->isAutomaticAmbientEnabled() + ? scene->getAutomaticAmbientColor() + : scene->getAmbientColor(); + ambientColor = glm::vec3(sceneAmbientColor.r, sceneAmbientColor.g, + sceneAmbientColor.b); const auto &directionalLights = scene->getDirectionalLights(); for (auto *light : directionalLights) { if (light == nullptr) { @@ -865,6 +1001,8 @@ void photon::PathTracing::render( directionalLightIntensity); pathTracingPipeline->setUniform1f("sceneData.ambientIntensity", ambientIntensity); + pathTracingPipeline->setUniform3f("sceneData.ambientColor", ambientColor.x, + ambientColor.y, ambientColor.z); pathTracingPipeline->setUniform1i("sceneData.atmosphereEnabled", atmosphereEnabled); pathTracingPipeline->setUniform1f("sceneData.atmosphereSunSize", @@ -883,32 +1021,57 @@ void photon::PathTracing::render( spotLightCount); pathTracingPipeline->setUniform1i("sceneData.numAreaLights", areaLightCount); - pathTracingPipeline->setUniform1i("sceneData.frameIndex", this->frameIndex); pathTracingPipeline->setUniform1i("sceneData.raysPerPixel", this->raysPerPixel); - pathTracingPipeline->setUniform1i("sceneData.maxBounces", this->maxBounces); pathTracingPipeline->setUniform1f("sceneData.indirectStrength", this->indirectStrength); - this->buildAccelerationStructure(commandBuffer); - if (this->createLightBuffers()) { - frameIndex = 0; + const std::string previousError = lastError; + try { + if (!this->buildAccelerationStructure(commandBuffer)) { + if (previousError != lastError) { + atlas_error("Path tracing: " + lastError); + } + frameIndex = 0; + return false; + } + } catch (const std::exception &error) { + accelerationBuildFailed = true; + return fail(std::string("Acceleration structure build failed: ") + + error.what()); + } + if (instanceDataBuffer == nullptr || materialBuffer == nullptr || + meshInfo == nullptr || globalVertices == nullptr || + globalIndices == nullptr) { + return fail("Required scene buffers are unavailable"); + } + try { + if (this->createLightBuffers()) { + frameIndex = 0; + } + } catch (const std::exception &error) { + return fail(std::string("Light buffer creation failed: ") + + error.what()); + } + if (pointLights == nullptr || spotLights == nullptr || + areaLights == nullptr) { + return fail("Required light buffers are unavailable"); } commandBuffer->bindPipeline(this->pathTracingPipeline); pathTracingPipeline->bindTexture("outTex", output, 0); pathTracingPipeline->bindTexture("historyTex", pathTracingTexturePrev->texture, 1); pathTracingPipeline->bindTexture("brightTex", brightOutput, 2); - pathTracingPipeline->bindTexture( - "albedoRoughnessTex", pathTracingAovTextures[0]->texture, 3); - pathTracingPipeline->bindTexture( - "normalDepthTex", pathTracingAovTextures[1]->texture, 4); - pathTracingPipeline->bindTexture( - "motionObjectTex", pathTracingAovTextures[2]->texture, 5); - pathTracingPipeline->bindTexture( - "momentsHitTex", pathTracingAovTextures[3]->texture, 6); - pathTracingPipeline->bindTexture( - "historyGuideTex", pathTracingHistoryGuide->texture, 7); + pathTracingPipeline->bindTexture("albedoRoughnessTex", + pathTracingAovTextures[0]->texture, 3); + pathTracingPipeline->bindTexture("normalDepthTex", + pathTracingAovTextures[1]->texture, 4); + pathTracingPipeline->bindTexture("motionObjectTex", + pathTracingAovTextures[2]->texture, 5); + pathTracingPipeline->bindTexture("momentsHitTex", + pathTracingAovTextures[3]->texture, 6); + pathTracingPipeline->bindTexture("historyGuideTex", + pathTracingHistoryGuide->texture, 7); static std::shared_ptr fallbackSkyboxTexture = nullptr; if (fallbackSkyboxTexture == nullptr) { @@ -921,6 +1084,10 @@ void photon::PathTracing::render( skyboxTexture = skybox->cubemap.texture; } } + pathTracingPipeline->setUniform1i( + "sceneData.environmentEnabled", + skyboxTexture != fallbackSkyboxTexture || atmosphereEnabled != 0 ? 1 + : 0); pathTracingPipeline->bindTexture("skybox", skyboxTexture, kPathTracerSkyboxTextureUnit); auto skyboxTextureId = skyboxTexture->textureID; @@ -931,7 +1098,16 @@ void photon::PathTracing::render( glm::length(cachedDirectionalLightColor - directionalLightColor) > 0.0001f || std::fabs(cachedDirectionalLightIntensity - directionalLightIntensity) > - 0.0001f; + 0.0001f || + glm::length(cachedAmbientColor - ambientColor) > 0.0001f || + std::fabs(cachedAmbientIntensity - ambientIntensity) > 0.0001f || + cachedAtmosphereEnabled != atmosphereEnabled || + glm::length(cachedAtmosphereSunDirection - atmosphereSunDirection) > + 0.0001f || + glm::length(cachedAtmosphereSunColor - atmosphereSunColor) > 0.0001f || + std::fabs(cachedAtmosphereSunIntensity - atmosphereSunIntensity) > + 0.0001f || + std::fabs(cachedAtmosphereSunSize - atmosphereSunSize) > 0.0001f; bool skyChanged = cachedSkyboxTextureId != skyboxTextureId; if (lightChanged || skyChanged) { frameIndex = 0; @@ -940,52 +1116,87 @@ void photon::PathTracing::render( cachedDirectionalLightDirection = directionalLightDirection; cachedDirectionalLightColor = directionalLightColor; cachedDirectionalLightIntensity = directionalLightIntensity; + cachedAmbientColor = ambientColor; + cachedAmbientIntensity = ambientIntensity; cachedSkyboxTextureId = skyboxTextureId; - - commandBuffer->bindInstanceAccelerationStructure(this->sceneTLAS, 0); + cachedAtmosphereEnabled = atmosphereEnabled; + cachedAtmosphereSunDirection = atmosphereSunDirection; + cachedAtmosphereSunColor = atmosphereSunColor; + cachedAtmosphereSunIntensity = atmosphereSunIntensity; + cachedAtmosphereSunSize = atmosphereSunSize; + + const int refinementFrame = std::max(frameIndex, 0); + const int pixelStride = interactive ? 4 : (refinementFrame < 4 ? 2 : 1); + const int effectiveBounces = + interactive ? std::min(this->maxBounces, 1) + : std::min(this->maxBounces, 2 + refinementFrame / 8); + pathTracingPipeline->setUniform1i("sceneData.frameIndex", frameIndex); + pathTracingPipeline->setUniform1i("sceneData.maxBounces", effectiveBounces); + pathTracingPipeline->setUniform1i("sceneData.pixelStride", pixelStride); + + commandBuffer->bindPrimitiveAccelerationStructure(this->sceneBLAS, 0); pathTracingPipeline->bindBuffer("materials", materialBuffer, 2); - pathTracingPipeline->bindBuffer("meshData", meshInfo, 3); + pathTracingPipeline->bindBuffer("primitiveObjects", meshInfo, 3); pathTracingPipeline->bindBuffer("vertices", globalVertices, 4); pathTracingPipeline->bindBuffer("indices", globalIndices, 5); pathTracingPipeline->bindBuffer("instanceData", instanceDataBuffer, 6); pathTracingPipeline->bindBuffer("pointLights", pointLights, 9); pathTracingPipeline->bindBuffer("spotLights", spotLights, 10); pathTracingPipeline->bindBuffer("areaLights", areaLights, 11); + pathTracingPipeline->bindBuffer("blasPrimitiveOffsets", + blasPrimitiveOffsets, 13); pathTracingPipeline->setUniform1i( "sceneData.materialTextureCount", std::min(static_cast(materialTextures.size()), kPathTracerMaxMaterialTextures)); - pathTracingPipeline->bindTextureArray(materialTextures, 12); + if (materialTextureBindings.size() != kPathTracerMaxMaterialTextures) { + static std::shared_ptr fallbackMaterialTexture = + createFallbackMaterialTexture(); + materialTextureBindings = materialTextures; + materialTextureBindings.resize(kPathTracerMaxMaterialTextures, + fallbackMaterialTexture); + } + pathTracingPipeline->bindTextureArray(materialTextureBindings, 12); - commandBuffer->dispatch(outputWidth, outputHeight, 1); + commandBuffer->dispatch((outputWidth + pixelStride - 1) / pixelStride, + (outputHeight + pixelStride - 1) / pixelStride, 1); commandBuffer->computeBarrier(); - const std::array denoiseSteps = {1, 2, 4}; - for (size_t pass = 0; pass < denoiseSteps.size(); ++pass) { - const auto &input = pass == 0 - ? output - : denoiseTextures[(pass - 1) % 2]->texture; - const auto &denoisedOutput = - pass + 1 == denoiseSteps.size() - ? output - : denoiseTextures[pass % 2]->texture; - commandBuffer->bindPipeline(pathDenoisePipeline); - pathDenoisePipeline->bindTexture("inputTexture", input, 0); - pathDenoisePipeline->bindTexture("outputTexture", denoisedOutput, 1); - pathDenoisePipeline->bindTexture("brightTexture", brightOutput, 2); - pathDenoisePipeline->bindTexture( - "guideTexture", pathTracingAovTextures[1]->texture, 3); - pathDenoisePipeline->setUniform1i("parameters.stepWidth", - denoiseSteps[pass]); - commandBuffer->dispatch(outputWidth, outputHeight, 1); - commandBuffer->computeBarrier(); + if (!interactive && pixelStride == 1 && pathDenoisePipeline != nullptr && + denoiseTextures[0] != nullptr && denoiseTextures[1] != nullptr) { + const std::array denoiseSteps = {1, 2, 4}; + const size_t denoisePassCount = refinementFrame < 32 ? 2 : 3; + for (size_t pass = 0; pass < denoisePassCount; ++pass) { + const auto &input = + pass == 0 ? output : denoiseTextures[(pass - 1) % 2]->texture; + const auto &denoisedOutput = + pass + 1 == denoisePassCount + ? output + : denoiseTextures[pass % 2]->texture; + commandBuffer->bindPipeline(pathDenoisePipeline); + pathDenoisePipeline->bindTexture("inputTexture", input, 0); + pathDenoisePipeline->bindTexture("outputTexture", denoisedOutput, + 1); + pathDenoisePipeline->bindTexture("brightTexture", brightOutput, 2); + pathDenoisePipeline->bindTexture( + "guideTexture", pathTracingAovTextures[1]->texture, 3); + pathDenoisePipeline->bindTexture("albedoRoughnessTexture", + pathTracingAovTextures[0]->texture, + 4); + pathDenoisePipeline->setUniform1i("parameters.stepWidth", + denoiseSteps[pass]); + commandBuffer->dispatch(outputWidth, outputHeight, 1); + commandBuffer->computeBarrier(); + } } previousViewProj = viewProj; frameIndex++; + lastError.clear(); + return true; } #endif diff --git a/runtime/lib/context.cpp b/runtime/lib/context.cpp index 1c9695a0..f8b4e531 100644 --- a/runtime/lib/context.cpp +++ b/runtime/lib/context.cpp @@ -92,6 +92,28 @@ struct MaterialDefinition { std::vector textures; }; +class WindowActivationScope { + public: + explicit WindowActivationScope(Window &window) + : previousWindow(Window::mainWindow), + previousDevice(opal::Device::globalInstance) { + window.activateRenderingContext(); + } + + ~WindowActivationScope() { + if (previousWindow != nullptr) { + previousWindow->activateRenderingContext(); + return; + } + Window::mainWindow = nullptr; + opal::Device::globalInstance = previousDevice; + } + + private: + Window *previousWindow; + opal::Device *previousDevice; +}; + struct PendingComponent { GameObject *object = nullptr; std::string objectType; @@ -628,8 +650,7 @@ TextureType parseTextureTypeString(const std::string &value) { if (token == "normal" || token == "normalmap") { return TextureType::Normal; } - if (token == "parallax" || token == "displacement" || - token == "height") { + if (token == "parallax" || token == "displacement" || token == "height") { return TextureType::Parallax; } if (token == "metallic" || token == "metalness") { @@ -641,8 +662,7 @@ TextureType parseTextureTypeString(const std::string &value) { if (token == "ao" || token == "ambientocclusion") { return TextureType::AO; } - if (token == "pbrpack" || token == "orm" || - token == "metallicroughness") { + if (token == "pbrpack" || token == "orm" || token == "metallicroughness") { return TextureType::PBRPack; } if (token == "opacity" || token == "alpha") { @@ -752,7 +772,8 @@ MaterialDefinition loadMaterialDefinition(const json &value, loaded.material.normalMapStrength); tryReadBoolAny(materialData, {"useNormalMap"}, loaded.material.useNormalMap); - if (const json *scale = findField(materialData, {"textureScale", "uvScale"}); + if (const json *scale = + findField(materialData, {"textureScale", "uvScale"}); scale != nullptr && scale->is_array() && scale->size() >= 2) { loaded.material.textureScale = { static_cast((*scale)[0].get()), @@ -1567,9 +1588,9 @@ bool isEditorLightObject(const Context &context, GameObject &object) { void syncEditorLightObject(Context &context, GameObject &object) { const int id = static_cast(object.getId()); const auto sourceIt = context.editorLightSourceData.find(id); - const json *source = - sourceIt != context.editorLightSourceData.end() ? &sourceIt->second - : nullptr; + const json *source = sourceIt != context.editorLightSourceData.end() + ? &sourceIt->second + : nullptr; if (auto it = context.editorPointLights.find(id); it != context.editorPointLights.end() && it->second != nullptr) { it->second->position = object.getPosition(); @@ -1602,11 +1623,10 @@ void syncEditorLightObject(Context &context, GameObject &object) { tryReadColorAny(*source, {"shineColor"}, it->second->shineColor); tryReadFloatAny(*source, {"intensity"}, it->second->intensity); tryReadFloatAny(*source, {"range", "distance"}, it->second->range); - float cutoff = - glm::degrees(std::acos(std::clamp(it->second->cutOff, -1.0f, - 1.0f))); - float outerCutoff = glm::degrees(std::acos( - std::clamp(it->second->outerCutoff, -1.0f, 1.0f))); + float cutoff = glm::degrees( + std::acos(std::clamp(it->second->cutOff, -1.0f, 1.0f))); + float outerCutoff = glm::degrees( + std::acos(std::clamp(it->second->outerCutoff, -1.0f, 1.0f))); tryReadFloatAny(*source, {"cutoff"}, cutoff); tryReadFloatAny(*source, {"outerCutoff"}, outerCutoff); it->second->cutOff = glm::cos(glm::radians(cutoff)); @@ -1983,8 +2003,7 @@ bool updateObjectNode(json &node, const Context &context, GameObject &object) { } } } - if (auto components = - context.editorComponentData.find(object.getId()); + if (auto components = context.editorComponentData.find(object.getId()); components != context.editorComponentData.end()) { node["components"] = components->second; } @@ -3039,9 +3058,9 @@ bool updateAttachedComponent(Context &context, GameObject &object, script->variables = *variables; if (script->instance != nullptr) { const std::string serialized = variables->dump(); - JSValue parsed = JS_ParseJSON( - context.context, serialized.c_str(), serialized.size(), - ""); + JSValue parsed = + JS_ParseJSON(context.context, serialized.c_str(), + serialized.size(), ""); if (!JS_IsException(parsed)) { JS_SetPropertyStr(context.context, script->instance->instance, "variables", @@ -3057,8 +3076,7 @@ bool updateAttachedComponent(Context &context, GameObject &object, tryReadStringAny(data, {"name", "class", "className"}, script->className); std::string source; - if (tryReadStringAny(data, {"source"}, source) && - !source.empty()) { + if (tryReadStringAny(data, {"source"}, source) && !source.empty()) { const std::string resolvedSource = resolveRuntimePath(baseDir, source); std::string extension = @@ -3081,8 +3099,7 @@ bool updateAttachedComponent(Context &context, GameObject &object, script->className = inferScriptClassName(resolvedSource); } } - if (script->className.empty() || - script->entryModuleName.empty()) { + if (script->className.empty() || script->entryModuleName.empty()) { throw std::runtime_error( "Script component is missing a valid class or source"); } @@ -3104,8 +3121,7 @@ bool updateAttachedComponent(Context &context, GameObject &object, if (auto rigidbody = std::dynamic_pointer_cast(component); rigidbody != nullptr) { - tryReadStringAny(data, {"sendSignal", "signal"}, - rigidbody->sendSignal); + tryReadStringAny(data, {"sendSignal", "signal"}, rigidbody->sendSignal); tryReadBoolAny(data, {"isSensor"}, rigidbody->isSensor); if (rigidbody->body != nullptr) { rigidbody->body->sensorSignal = rigidbody->sendSignal; @@ -3162,8 +3178,7 @@ bool updateAttachedComponent(Context &context, GameObject &object, if (attached != context.editorRuntimeComponents.end()) { for (const auto &entry : attached->second) { const std::shared_ptr related = entry.lock(); - if (auto joint = - std::dynamic_pointer_cast(related); + if (auto joint = std::dynamic_pointer_cast(related); joint != nullptr) { joint->breakJoint(); } @@ -3182,8 +3197,7 @@ bool updateAttachedComponent(Context &context, GameObject &object, audio != nullptr) { if (propertyPath == "/source") { std::string source; - if (tryReadStringAny(data, {"source"}, source) && - !source.empty()) { + if (tryReadStringAny(data, {"source"}, source) && !source.empty()) { audio->setSource(createRuntimeResource( baseDir, source, ResourceType::Audio, "runtime-audio")); } @@ -3238,18 +3252,15 @@ bool updateAttachedComponent(Context &context, GameObject &object, limits != nullptr && limits->is_object()) { tryReadBoolAny(*limits, {"isEnabled", "enabled"}, hinge->limits.enabled); - tryReadFloatAny(*limits, {"minAngle"}, - hinge->limits.minAngle); - tryReadFloatAny(*limits, {"maxAngle"}, - hinge->limits.maxAngle); + tryReadFloatAny(*limits, {"minAngle"}, hinge->limits.minAngle); + tryReadFloatAny(*limits, {"maxAngle"}, hinge->limits.maxAngle); } if (const json *motor = findField(data, {"motor"}); motor != nullptr && motor->is_object()) { tryReadBoolAny(*motor, {"isEnabled", "enabled"}, hinge->motor.enabled); tryReadFloatAny(*motor, {"maxForce"}, hinge->motor.maxForce); - tryReadFloatAny(*motor, {"maxTorque"}, - hinge->motor.maxTorque); + tryReadFloatAny(*motor, {"maxTorque"}, hinge->motor.maxTorque); } } if (auto spring = std::dynamic_pointer_cast(component); @@ -3273,8 +3284,7 @@ bool updateAttachedComponent(Context &context, GameObject &object, spring->spring.dampingRatio); tryReadFloatAny(*settings, {"stiffness"}, spring->spring.stiffness); - tryReadFloatAny(*settings, {"damping"}, - spring->spring.damping); + tryReadFloatAny(*settings, {"damping"}, spring->spring.damping); } } return true; @@ -3813,9 +3823,8 @@ Font loadGraphiteFont(const json &data, const std::string &baseDir) { if (const auto found = cache.find(key); found != cache.end()) { return found->second; } - Resource resource = createRuntimeResource(baseDir, source, - ResourceType::Font, - "graphite-font"); + Resource resource = createRuntimeResource( + baseDir, source, ResourceType::Font, "graphite-font"); Font font = Font::fromResource(name, resource, size); cache[key] = font; return font; @@ -3882,12 +3891,10 @@ void repairGraphiteColors(json &value, const std::string &key = {}) { if (!value.is_array()) return; const std::string normalizedKey = normalizeToken(key); - const bool colorField = normalizedKey == "background" || - normalizedKey == "foreground" || - normalizedKey == "border" || - normalizedKey == "tint" || - normalizedKey == "color" || - normalizedKey.ends_with("color"); + const bool colorField = + normalizedKey == "background" || normalizedKey == "foreground" || + normalizedKey == "border" || normalizedKey == "tint" || + normalizedKey == "color" || normalizedKey.ends_with("color"); if (colorField && value.size() == 4 && value[3].is_number() && std::abs(value[3].get() - (1.0 / 255.0)) < 0.00001) value[3] = 1.0; @@ -3931,11 +3938,10 @@ JsonDefinition loadGraphiteDocument(const json &value, std::to_string(version)); } repairGraphiteColors(definition.data); - const json defaultFont = - definition.data.contains("defaultFont") && - definition.data["defaultFont"].is_object() - ? definition.data["defaultFont"] - : json::object(); + const json defaultFont = definition.data.contains("defaultFont") && + definition.data["defaultFont"].is_object() + ? definition.data["defaultFont"] + : json::object(); if (definition.data.contains("root")) { inheritGraphiteDocumentDefaults(definition.data["root"], defaultFont); } @@ -3981,8 +3987,8 @@ createRenderable(Context &context, const json &objectData, font = loadGraphiteFont(*fontData, baseDir); } graphite::UIStyle style; - const bool hasStyle = objectData.contains("style") && - objectData["style"].is_object(); + const bool hasStyle = + objectData.contains("style") && objectData["style"].is_object(); if (hasStyle) { style = parseGraphiteStyle(objectData["style"]); } @@ -4000,7 +4006,8 @@ createRenderable(Context &context, const json &objectData, tryReadStringAny(objectData, {"content", "text"}, content); Color color = Color::white(); tryReadColorAny(objectData, {"color", "textColor"}, color); - auto object = std::make_shared(content, font, color, position); + auto object = + std::make_shared(content, font, color, position); tryReadFloatAny(objectData, {"fontSize"}, object->fontSize); if (hasStyle) object->setStyle(style); @@ -4012,7 +4019,8 @@ createRenderable(Context &context, const json &objectData, object->position = position; object->size = Size2d{size.x, size.y}; tryReadColorAny(objectData, {"tint"}, object->tint); - if (const json *source = findField(objectData, {"source", "texture"}); + if (const json *source = + findField(objectData, {"source", "texture"}); source != nullptr && !isEmptyStringValue(*source)) { object->texture = loadTextureDefinition( *source, baseDir, TextureType::Color, false); @@ -4039,8 +4047,7 @@ createRenderable(Context &context, const json &objectData, object->hoverBackgroundColor); tryReadColorAny(objectData, {"pressedBackgroundColor"}, object->pressedBackgroundColor); - tryReadColorAny(objectData, {"borderColor"}, - object->borderColor); + tryReadColorAny(objectData, {"borderColor"}, object->borderColor); tryReadColorAny(objectData, {"hoverBorderColor"}, object->hoverBorderColor); if (hasStyle) @@ -4067,8 +4074,7 @@ createRenderable(Context &context, const json &objectData, object->boxBackgroundColor); tryReadColorAny(objectData, {"hoverBoxBackgroundColor"}, object->hoverBoxBackgroundColor); - tryReadColorAny(objectData, {"borderColor"}, - object->borderColor); + tryReadColorAny(objectData, {"borderColor"}, object->borderColor); tryReadColorAny(objectData, {"activeBorderColor"}, object->activeBorderColor); tryReadColorAny(objectData, {"checkColor"}, object->checkColor); @@ -4095,12 +4101,10 @@ createRenderable(Context &context, const json &objectData, object->placeholderColor); tryReadColorAny(objectData, {"backgroundColor"}, object->backgroundColor); - tryReadColorAny(objectData, {"borderColor"}, - object->borderColor); + tryReadColorAny(objectData, {"borderColor"}, object->borderColor); tryReadColorAny(objectData, {"focusedBorderColor"}, object->focusedBorderColor); - tryReadColorAny(objectData, {"cursorColor"}, - object->cursorColor); + tryReadColorAny(objectData, {"cursorColor"}, object->cursorColor); if (hasStyle) object->setStyle(style); return registerUIObject(object); @@ -4258,8 +4262,8 @@ createRenderable(Context &context, const json &objectData, } applyTransform(*object, objectData); - collectPendingComponents(context, *object, objectData, baseDir, rigidbodies, - standard, joints); + collectPendingComponents(context, *object, objectData, baseDir, + rigidbodies, standard, joints); return object; } @@ -4271,8 +4275,8 @@ createRenderable(Context &context, const json &objectData, generatedIndex); context.objects.push_back(object); applyTransform(*object, objectData); - collectPendingComponents(context, *object, objectData, baseDir, rigidbodies, - standard, joints); + collectPendingComponents(context, *object, objectData, baseDir, + rigidbodies, standard, joints); return object; } @@ -4303,8 +4307,8 @@ createRenderable(Context &context, const json &objectData, } applyTransform(*object, objectData); - collectPendingComponents(context, *object, objectData, baseDir, rigidbodies, - standard, joints); + collectPendingComponents(context, *object, objectData, baseDir, + rigidbodies, standard, joints); return object; } @@ -4316,9 +4320,9 @@ createRenderable(Context &context, const json &objectData, } auto object = std::make_shared(); - object->fromResource(createRuntimeResource( - baseDir, source, ResourceType::Model, - "runtime-model"), + object->fromResource(createRuntimeResource(baseDir, source, + ResourceType::Model, + "runtime-model"), context.modelImportProgress); registerGameObject(context, *object, objectData, normalizedType, @@ -4332,8 +4336,8 @@ createRenderable(Context &context, const json &objectData, } applyTransform(*object, objectData); - collectPendingComponents(context, *object, objectData, baseDir, rigidbodies, - standard, joints); + collectPendingComponents(context, *object, objectData, baseDir, + rigidbodies, standard, joints); return object; } @@ -4416,8 +4420,8 @@ createRenderable(Context &context, const json &objectData, object->setParticleSettings(settings); } - collectPendingComponents(context, *object, objectData, baseDir, rigidbodies, - standard, joints); + collectPendingComponents(context, *object, objectData, baseDir, + rigidbodies, standard, joints); return object; } @@ -4475,8 +4479,8 @@ createRenderable(Context &context, const json &objectData, } applyTransform(*object, objectData); - collectPendingComponents(context, *object, objectData, baseDir, rigidbodies, - standard, joints); + collectPendingComponents(context, *object, objectData, baseDir, + rigidbodies, standard, joints); return object; } @@ -4532,8 +4536,7 @@ makeContextWithWindowOptions(std::string projectFile, void *metalView, ssaoScale = (*windowTable)["ssaoScale"].value_or(0.4f); } if (auto *rendererTable = configTable["renderer"].as_table()) { - useUpscaling = - (*rendererTable)["use_upscaling"].value_or(false); + useUpscaling = (*rendererTable)["use_upscaling"].value_or(false); renderScale = std::clamp( (*rendererTable)["upscaling_ratio"].value_or(0.5f), 0.5f, 1.0f); } @@ -4575,8 +4578,7 @@ std::shared_ptr runtime::makeContext(std::string projectFile) { nullptr); } -std::shared_ptr -runtime::makeHiddenContext(std::string projectFile) { +std::shared_ptr runtime::makeHiddenContext(std::string projectFile) { return makeContextWithWindowOptions(std::move(projectFile), nullptr, nullptr, false); } @@ -4599,6 +4601,41 @@ runtime::makeContextForMetalView(std::string projectFile, void *metalView, #endif } +std::shared_ptr runtime::makeMaterialPreviewContextForMetalView( + std::string projectFile, void *metalView) { +#ifdef METAL + if (metalView == nullptr) { + throw std::runtime_error("Metal view pointer cannot be null"); + } + Window *previousWindow = Window::mainWindow; + opal::Device *previousDevice = opal::Device::globalInstance; + auto restorePrevious = [&] { + if (previousWindow != nullptr) { + previousWindow->activateRenderingContext(); + } else { + Window::mainWindow = nullptr; + opal::Device::globalInstance = previousDevice; + } + }; + try { + auto context = makeContextWithWindowOptions(std::move(projectFile), + metalView, nullptr); + context->editorRuntime = false; + context->materialPreviewRuntime = true; + restorePrevious(); + return context; + } catch (...) { + restorePrevious(); + throw; + } +#else + (void)projectFile; + (void)metalView; + throw std::runtime_error( + "Material preview embedding requires the Metal backend"); +#endif +} + std::shared_ptr runtime::makeContextForMetalViewNonBlocking( std::string projectFile, void *metalView, CoreWindowReference sdlInputWindow) { @@ -4694,6 +4731,7 @@ bool Context::stepFrame() { if (scene == nullptr) { throw std::runtime_error("Scene is not initialized"); } + WindowActivationScope activeWindow(*window); if (editorRuntime) { repairEditorCamera(*this); } @@ -4721,6 +4759,7 @@ bool Context::resize(int width, int height, float scale) { if (window == nullptr) { throw std::runtime_error("Window is not initialized"); } + WindowActivationScope activeWindow(*window); window->resize(width, height, scale); return true; } @@ -4781,6 +4820,26 @@ bool Context::setEditorShadingMode(int mode) { return true; } +bool Context::setEditorPathTracingPreview(bool enabled) { + if (window == nullptr || !editorRuntime) { + return false; + } +#ifdef METAL + return window->setEditorPathTracingPreview(enabled); +#else + (void)enabled; + return false; +#endif +} + +std::string Context::getPathTracingError() const { +#ifdef METAL + return window != nullptr ? window->getPathTracingError() : std::string(); +#else + return {}; +#endif +} + float Context::frameRate() const { return window != nullptr ? window->getFramesPerSecond() : 0.0f; } @@ -4841,8 +4900,9 @@ bool Context::toggleEditorTransformSnapping() { } float Context::changeEditorTransformSnapIncrement(float factor) { - return window != nullptr ? window->changeEditorTransformSnapIncrement(factor) - : 0.0f; + return window != nullptr + ? window->changeEditorTransformSnapIncrement(factor) + : 0.0f; } int Context::selectedObjectId() const { @@ -4888,9 +4948,8 @@ bool setJsonProperty(json &target, const std::string &propertyPath, return false; } try { - const std::string pointerPath = propertyPath.front() == '/' - ? propertyPath - : '/' + propertyPath; + const std::string pointerPath = + propertyPath.front() == '/' ? propertyPath : '/' + propertyPath; target[json::json_pointer(pointerPath)] = value; return true; } catch (const json::exception &) { @@ -4954,6 +5013,23 @@ std::string editorObjectType(const Context &context, GameObject &object) { bool editorObjectWorldBounds(GameObject &object, glm::vec3 &minimum, glm::vec3 &maximum) { + if (auto *model = dynamic_cast(&object)) { + bool found = false; + for (const auto &child : model->getObjects()) { + if (child == nullptr) { + continue; + } + glm::vec3 childMinimum; + glm::vec3 childMaximum; + if (!editorObjectWorldBounds(*child, childMinimum, childMaximum)) { + continue; + } + minimum = found ? glm::min(minimum, childMinimum) : childMinimum; + maximum = found ? glm::max(maximum, childMaximum) : childMaximum; + found = true; + } + return found; + } if (auto *compound = dynamic_cast(&object)) { bool found = false; for (auto *child : compound->objects) { @@ -4972,7 +5048,11 @@ bool editorObjectWorldBounds(GameObject &object, glm::vec3 &minimum, return found; } - const std::vector vertices = object.getVertices(); + const auto *coreObject = dynamic_cast(&object); + const std::vector copiedVertices = + coreObject == nullptr ? object.getVertices() : std::vector(); + const std::vector &vertices = + coreObject != nullptr ? coreObject->vertices : copiedVertices; if (vertices.empty()) { return false; } @@ -5022,8 +5102,7 @@ std::optional propertySyncJsonValue(const json &value, try { if (path.empty()) return value; - const std::string pointerPath = - path.front() == '/' ? path : '/' + path; + const std::string pointerPath = path.front() == '/' ? path : '/' + path; return value.at(json::json_pointer(pointerPath)); } catch (const json::exception &) { return std::nullopt; @@ -5038,11 +5117,10 @@ std::optional propertySyncSourceValue(Context &context, normalizeToken(source.value("section", std::string())); const std::string path = source.value("path", std::string()); auto withFallback = [&source](std::optional value) { - return value.has_value() - ? value - : source.contains("fallback") - ? std::optional(source["fallback"]) - : std::nullopt; + return value.has_value() ? value + : source.contains("fallback") + ? std::optional(source["fallback"]) + : std::nullopt; }; if (section == "camera") return withFallback( @@ -5076,10 +5154,10 @@ std::optional propertySyncSourceValue(Context &context, return withFallback( propertySyncJsonValue(sourceData->second, path)); auto objectData = context.editorObjectSourceData.find(id); - return withFallback(objectData != context.editorObjectSourceData.end() - ? propertySyncJsonValue(objectData->second, - path) - : std::nullopt); + return withFallback( + objectData != context.editorObjectSourceData.end() + ? propertySyncJsonValue(objectData->second, path) + : std::nullopt); } const int index = source.value("componentIndex", -1); auto components = context.editorComponentData.find(id); @@ -5090,8 +5168,7 @@ std::optional propertySyncSourceValue(Context &context, ? std::optional(source["fallback"]) : std::nullopt; } - return withFallback( - propertySyncJsonValue(components->second[index], path)); + return withFallback(propertySyncJsonValue(components->second[index], path)); } bool applyPropertySyncTarget(Context &context, const json &target, @@ -5111,8 +5188,7 @@ bool applyPropertySyncTarget(Context &context, const json &target, if (object == nullptr) return false; const int id = static_cast(object->getId()); - const std::string component = - target.value("component", std::string()); + const std::string component = target.value("component", std::string()); const std::string normalized = normalizeToken(component); const int index = target.value("componentIndex", -1); if (!attachComponents && normalized != "transform" && @@ -5158,10 +5234,9 @@ json canonicalPropertySyncEndpoint(Context &context, json endpoint) { return endpoint; const int id = static_cast(object->getId()); auto reference = context.objectSceneReferences.find(id); - endpoint["object"] = - reference != context.objectSceneReferences.end() - ? reference->second - : editorObjectName(context, *object); + endpoint["object"] = reference != context.objectSceneReferences.end() + ? reference->second + : editorObjectName(context, *object); return endpoint; } @@ -5487,8 +5562,8 @@ bool Context::setPropertySync(const json &target, const json &source) { if (!editorPropertySyncs.is_array()) editorPropertySyncs = json::array(); for (json &binding : editorPropertySyncs) { - if (binding.is_object() && binding.value("target", json()) == - canonicalTarget) { + if (binding.is_object() && + binding.value("target", json()) == canonicalTarget) { binding["source"] = canonicalSource; applyPropertySyncs(*this, true); return true; @@ -5541,6 +5616,169 @@ bool Context::setObjectMaterial(int id, const std::string &path) { return true; } +bool Context::initializeMaterialPreview(const std::string &definition, + const std::string &baseDir, + int environmentMode) { + if (window == nullptr || scene == nullptr || !materialPreviewRuntime) { + return false; + } + + 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; + window->setCamera(camera.get()); + window->setEditorSceneCamera(nullptr); + window->setEditorControlsEnabled(false); + window->useDeferredRendering(); + + auto sphere = std::make_shared(); + *sphere = createSphere(0.72f, 64, 32, Color::white()); + sphere->castsShadows = false; + objects.push_back(sphere); + window->addObject(sphere.get()); + + auto directional = std::make_unique( + Magnitude3d{-0.4f, -0.55f, -1.0f}, Color::white(), Color::white(), + 1.1f); + scene->addDirectionalLight(directional.get()); + directionalLights.push_back(std::move(directional)); + + auto keyLight = std::make_unique(); + keyLight->position = {-1.4f, 1.25f, 1.8f}; + keyLight->size = {1.6f, 1.0f}; + keyLight->intensity = 5.0f; + keyLight->range = 8.0f; + keyLight->castsBothSides = true; + scene->addAreaLight(keyLight.get()); + areaLights.push_back(std::move(keyLight)); + + auto rimLight = std::make_unique(); + rimLight->position = {1.35f, -0.75f, 0.6f}; + rimLight->size = {0.9f, 1.4f}; + rimLight->intensity = 2.5f; + rimLight->range = 7.0f; + rimLight->castsBothSides = true; + scene->addAreaLight(rimLight.get()); + areaLights.push_back(std::move(rimLight)); + + window->setScene(scene.get()); + return setMaterialPreviewEnvironment(environmentMode) && + setMaterialPreviewMaterial(definition, baseDir); +} + +bool Context::setMaterialPreviewMaterial(const std::string &definition, + const std::string &baseDir) { + if (window == nullptr || !materialPreviewRuntime || objects.empty() || + definition.empty()) { + return false; + } + auto *sphere = dynamic_cast(objects.front().get()); + if (sphere == nullptr) { + return false; + } + try { + WindowActivationScope activeWindow(*window); + applyMaterial(*sphere, + loadMaterialDefinition(json::parse(definition), baseDir)); + return true; + } catch (const std::exception &error) { + RUNTIME_LOG("Material preview could not be updated: " + + std::string(error.what())); + return false; + } +} + +bool Context::setMaterialPreviewEnvironment(int mode) { + if (window == nullptr || scene == nullptr || !materialPreviewRuntime || + directionalLights.empty() || areaLights.size() < 2) { + return false; + } + + WindowActivationScope activeWindow(*window); + std::array colors; + Color ambient; + Color key; + Color rim; + Color background; + float ambientIntensity = 0.0f; + float directionalIntensity = 0.0f; + float keyIntensity = 0.0f; + float rimIntensity = 0.0f; + + 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; + } 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; + } 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}}; + 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}; + background = {0.035f, 0.04f, 0.05f, 1.0f}; + ambientIntensity = 0.6f; + directionalIntensity = 0.95f; + keyIntensity = 5.0f; + rimIntensity = 2.5f; + } + + scene->setAmbientColor(ambient); + scene->setAmbientIntensity(ambientIntensity); + directionalLights.front()->color = key; + directionalLights.front()->shineColor = key; + directionalLights.front()->intensity = directionalIntensity; + areaLights[0]->color = key; + areaLights[0]->shineColor = key; + areaLights[0]->intensity = keyIntensity; + areaLights[1]->color = rim; + areaLights[1]->shineColor = rim; + areaLights[1]->intensity = rimIntensity; + window->setClearColor(background); + + if (auto skybox = scene->getSkybox(); skybox != nullptr) { + skybox->cubemap.updateWithColors(colors); + } else { + scene->setSkybox( + Skybox::create(Cubemap::fromColors(colors, 32), *window)); + } + return true; +} + static json inheritedRigidbodyCollider(GameObject &object) { return json{{"type", "box"}, {"size", editorObjectBoundsSize(object)}}; } @@ -5556,9 +5794,8 @@ int Context::addObjectComponent(int id, const json &component) { } const std::string normalizedType = normalizeToken(type); static const std::unordered_set supported{ - "script", "traitscript", "rigidbody", "audioplayer", - "joint", "fixedjoint", "hingejoint", "springjoint", - "vehicle", + "script", "traitscript", "rigidbody", "audioplayer", "joint", + "fixedjoint", "hingejoint", "springjoint", "vehicle", }; if (!supported.contains(normalizedType)) { return -1; @@ -5568,8 +5805,7 @@ int Context::addObjectComponent(int id, const json &component) { if (normalizedType == "rigidbody") { json &collider = storedComponent["collider"]; const bool inheritObjectSize = - collider.is_object() && - collider.value("inheritObjectSize", false); + collider.is_object() && collider.value("inheritObjectSize", false); if (inheritObjectSize) { collider = inheritedRigidbodyCollider(*object); } @@ -5856,11 +6092,10 @@ bool Context::deleteObject(int id) { window->removeObject(object); } - auto objectIt = std::find_if(objects.begin(), objects.end(), - [&](const auto &renderable) { - return renderable != nullptr && - renderable.get() == object; - }); + auto objectIt = std::find_if( + objects.begin(), objects.end(), [&](const auto &renderable) { + return renderable != nullptr && renderable.get() == object; + }); if (objectIt != objects.end()) { retiredObjects.push_back(std::move(*objectIt)); objects.erase(objectIt); @@ -6066,13 +6301,13 @@ int Context::createObject(const std::string &type, const std::string &name) { registerObjectReference(*this, displayName, object.get()); registerObjectReference(*this, std::to_string(id), object.get()); - editorObjectSourceData[id] = json::object( - {{"id", id}, - {"name", displayName}, - {"type", sceneType}, - {"position", vec3ToJson(position)}, - {"rotation", rotationToJson(object->getRotation())}, - {"scale", vec3ToJson(object->getScale())}}); + editorObjectSourceData[id] = + json::object({{"id", id}, + {"name", displayName}, + {"type", sceneType}, + {"position", vec3ToJson(position)}, + {"rotation", rotationToJson(object->getRotation())}, + {"scale", vec3ToJson(object->getScale())}}); if (!solidType.empty()) { editorObjectSourceData[id]["solid_type"] = solidType; } @@ -6128,8 +6363,7 @@ int Context::pasteObjectDefinition(const std::string &definition) { type == "point" || type == "pointlight" || type == "spot" || type == "spotlight" || type == "directional" || type == "directionallight" || type == "sun" || type == "area" || - type == "arealight" || type == "ambient" || - type == "ambientlight"; + type == "arealight" || type == "ambient" || type == "ambientlight"; const char *collection = isLight ? "lights" : "objects"; if (!sceneData.contains(collection) || !sceneData[collection].is_array()) { @@ -6246,10 +6480,9 @@ bool Context::openSceneFile(const std::string &path) { return false; try { const std::filesystem::path requested(path); - const std::string resolved = - requested.is_absolute() - ? requested.lexically_normal().string() - : resolveRuntimePath(projectDir, path); + const std::string resolved = requested.is_absolute() + ? requested.lexically_normal().string() + : resolveRuntimePath(projectDir, path); json sceneData = loadJsonFile(resolved); if (!sceneData.is_object()) return false; @@ -6269,6 +6502,7 @@ void Context::end() { if (window == nullptr) { return; } + WindowActivationScope activeWindow(*window); window->close(); window->endRunLoop(); } @@ -6327,8 +6561,7 @@ void Context::loadProject() { screenSpaceReflections = (*renderer)["ssr"].value_or(false); screenSpaceReflectionQuality = std::clamp((*renderer)["ssr_quality"].value_or(1), 0, 2); - screenSpaceReflectionDebug = - (*renderer)["ssr_debug"].value_or(false); + screenSpaceReflectionDebug = (*renderer)["ssr_debug"].value_or(false); } if (auto *gameTable = configTable["game"].as_table()) { @@ -6384,10 +6617,9 @@ void RuntimeScene::update(Window &window) { } if (runtimeContext->cameraActions.size() >= 3) { - runtimeContext->camera->updateWithActions(window, - runtimeContext->cameraActions[0], - runtimeContext->cameraActions[1], - runtimeContext->cameraActions[2]); + runtimeContext->camera->updateWithActions( + window, runtimeContext->cameraActions[0], + runtimeContext->cameraActions[1], runtimeContext->cameraActions[2]); } else { runtimeContext->camera->update(window); } @@ -6468,20 +6700,17 @@ void Context::loadScene(Window &window, const json &sceneData) { sceneData.contains("targets") && sceneData["targets"].is_array() ? sceneData["targets"] : json::array(); - editorEnvironmentData = - sceneData.contains("environment") && - sceneData["environment"].is_object() - ? sceneData["environment"] - : json::object(); - editorPropertySyncs = - sceneData.contains("property_syncs") && - sceneData["property_syncs"].is_array() - ? sceneData["property_syncs"] - : json::array(); - editorUIData = - sceneData.contains("ui") && sceneData["ui"].is_array() - ? sceneData["ui"] - : json::array(); + editorEnvironmentData = sceneData.contains("environment") && + sceneData["environment"].is_object() + ? sceneData["environment"] + : json::object(); + editorPropertySyncs = sceneData.contains("property_syncs") && + sceneData["property_syncs"].is_array() + ? sceneData["property_syncs"] + : json::array(); + editorUIData = sceneData.contains("ui") && sceneData["ui"].is_array() + ? sceneData["ui"] + : json::array(); scene->atmosphere.resetRuntimeState(); scene->setUseAtmosphereSkybox(false); @@ -6963,7 +7192,8 @@ void Context::loadScene(Window &window, const json &sceneData) { applyPropertySyncs(*this, false); - auto refreshPendingSyncValues = [this](std::vector &list) { + auto refreshPendingSyncValues = [this]( + std::vector &list) { for (PendingComponent &pending : list) { if (pending.object == nullptr) continue; diff --git a/runtime/lib/runtime.cpp b/runtime/lib/runtime.cpp index 95fffba2..9a715d48 100644 --- a/runtime/lib/runtime.cpp +++ b/runtime/lib/runtime.cpp @@ -17,6 +17,11 @@ void RuntimeScene::initialize(Window &window) { return; } + if (runtimeContext->materialPreviewRuntime) { + window.useDeferredRendering(); + return; + } + if (runtimeContext->config.renderer == "deferred") { window.useDeferredRendering(); diff --git a/shaders/metal/main.vert.metal b/shaders/metal/main.vert.metal index 5c1e234c..560b5603 100644 --- a/shaders/metal/main.vert.metal +++ b/shaders/metal/main.vert.metal @@ -94,7 +94,7 @@ vertex main0_out main0(main0_in in [[stage_in]], constant UBO& uniforms [[buffer float4 _57 = mvp * _56; out.gl_Position = _57; out.FragPos = float3((modelMatrix * float4(in.aPos, 1.0)).xyz); - out.TexCoord = float2(in.aTexCoord.x, 1.0 - in.aTexCoord.y); + out.TexCoord = in.aTexCoord; out.outColor = in.aColor; float3x3 normalMatrix = transpose(spvInverse3x3(float3x3(modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz))); out.Normal = fast::normalize(normalMatrix * in.aNormal); diff --git a/shaders/metal/path_tracing/path.metal b/shaders/metal/path_tracing/path.metal index 99de74a6..9503a5bb 100644 --- a/shaders/metal/path_tracing/path.metal +++ b/shaders/metal/path_tracing/path.metal @@ -30,16 +30,18 @@ struct Material { float ior; float reflectivity; float _pad2; + packed_float2 textureScale; + packed_float2 textureOffset; }; -struct MeshData { - uint vertexOffset; - uint indexOffset; - uint _pad0; - uint _pad1; -}; +static_assert(sizeof(Material) == 112); +static_assert(__builtin_offsetof(Material, emissiveColor) == 32); +static_assert(__builtin_offsetof(Material, albedoTextureIndex) == 48); +static_assert(__builtin_offsetof(Material, transmittance) == 80); +static_assert(__builtin_offsetof(Material, textureScale) == 96); struct VertexData { + packed_float3 position; packed_float3 normal; packed_float2 uv; packed_float3 tangent; @@ -111,9 +113,18 @@ struct SceneData { float3 atmosphereSunDirection; float atmosphereSunIntensity; float3 atmosphereSunColor; - float _pad0; + uint pixelStride; + float3 ambientColor; + uint environmentEnabled; }; +static_assert(sizeof(SceneData) == 144); +static_assert(__builtin_offsetof(SceneData, atmosphereSunDirection) == 48); +static_assert(__builtin_offsetof(SceneData, atmosphereSunIntensity) == 64); +static_assert(__builtin_offsetof(SceneData, atmosphereSunColor) == 80); +static_assert(__builtin_offsetof(SceneData, pixelStride) == 96); +static_assert(__builtin_offsetof(SceneData, ambientColor) == 112); + float pow5(float x) { float x2 = x * x; return x2 * x2 * x; @@ -121,6 +132,12 @@ float pow5(float x) { float luminance(float3 c) { return dot(c, float3(0.2126, 0.7152, 0.0722)); } +float powerHeuristic(float pdfA, float pdfB) { + float a2 = pdfA * pdfA; + float b2 = pdfB * pdfB; + return a2 / max(a2 + b2, 1e-8); +} + float3 clampLuminance(float3 c, float maxL) { float l = luminance(c); if (l > maxL && l > 1e-6) { @@ -160,6 +177,16 @@ float3 skyColor(float3 dir, float intensity, texturecube skybox, sampleDir = float3(0.0, 1.0, 0.0); } float3 sky = skybox.sample(skyboxSampler, sampleDir).xyz; + if (sceneData.atmosphereEnabled != 0) { + float horizon = pow(clamp(1.0 - abs(sampleDir.y), 0.0, 1.0), 4.0); + float daylight = smoothstep(-0.2, 0.15, + sceneData.atmosphereSunDirection.y); + float3 zenith = float3(0.08, 0.28, 0.65); + float3 horizonColor = float3(0.58, 0.72, 0.92); + float3 proceduralSky = mix(zenith, horizonColor, horizon) * + max(daylight, 0.08); + sky = max(sky, proceduralSky); + } if (sceneData.atmosphereEnabled != 0 && sceneData.atmosphereSunDirection.y > -0.15) { float3 sunDirection = sceneData.atmosphereSunDirection; @@ -217,6 +244,28 @@ float3 normalizeOr(float3 v, float3 fallback) { return fallback; } +float rayOffsetDistance(float3 position) { + float positionScale = + max(abs(position.x), max(abs(position.y), abs(position.z))); + return max(0.0002, positionScale * 0.000002); +} + +float3 offsetRayOrigin(float3 position, float3 geometricNormal, + float3 direction) { + float side = dot(direction, geometricNormal) >= 0.0 ? 1.0 : -1.0; + return position + geometricNormal * (rayOffsetDistance(position) * side); +} + +float2 encodeNormal(float3 normal) { + normal /= max(abs(normal.x) + abs(normal.y) + abs(normal.z), 1e-6); + 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; + } + return encoded; +} + constexpr sampler materialTexSampler(coord::normalized, address::repeat, filter::linear, mip_filter::linear); @@ -457,6 +506,20 @@ float4 sampleMaterialTexture( #define PT_MATERIAL_TEXTURE_BINDINGS \ constant MaterialTextureArguments &materialTextureArguments [[buffer(12)]] +float resolveMaterialOpacity(Material mat, float2 uv, uint textureCount, + PT_MATERIAL_TEXTURE_PARAMS) { + float opacity = clamp(mat.albedo.w, 0.0, 1.0); + if (mat.opacityTextureIndex >= 0 && + uint(mat.opacityTextureIndex) < textureCount) { + float4 opacitySample = sampleMaterialTexture( + mat.opacityTextureIndex, uv, PT_MATERIAL_TEXTURE_ARGS); + opacity *= mat.opacityTextureIndex == mat.albedoTextureIndex + ? opacitySample.w + : opacitySample.x; + } + return clamp(opacity, 0.0, 1.0); +} + void resolveMaterialParameters(Material mat, float2 uv, uint textureCount, PT_MATERIAL_TEXTURE_PARAMS, thread float3 &albedo, thread float &metallic, @@ -549,31 +612,61 @@ float3 resolveShadingNormal(Material mat, float2 uv, float3 localN, return N; } -float3 lambert(float3 albedo, float3 N, float3 L, float3 lightColor, - float intensity) { - float ndl = max(dot(N, L), 0.0); - return albedo * lightColor * intensity * ndl; -} - -bool isOccluded(intersector isect, - instance_acceleration_structure sceneAS, float3 P, float3 N, - float3 L, float maxDistance) { - float ndlAbs = abs(dot(N, L)); - float shadowBias = mix(0.003, 0.0008, ndlAbs); +bool isOccluded(intersector isect, + primitive_acceleration_structure sceneAS, float3 P, float3 Ng, + float3 L, float maxDistance, thread uint &rng, + constant Material *materials, + constant uint *primitiveObjects, + constant uint *blasPrimitiveOffsets, + constant VertexData *vertices, constant uint *indices, + constant SceneData &sceneData, PT_MATERIAL_TEXTURE_PARAMS) { + float shadowBias = rayOffsetDistance(P); ray shadowRay; - shadowRay.origin = P + N * shadowBias; + shadowRay.origin = offsetRayOrigin(P, Ng, L); shadowRay.direction = L; - shadowRay.min_distance = shadowBias; + shadowRay.min_distance = 0.0; shadowRay.max_distance = max(maxDistance - shadowBias, shadowBias + 1e-4); - auto shadowHit = isect.intersect(shadowRay, sceneAS, 0xFF); - return shadowHit.type != intersection_type::none; + for (uint alphaStep = 0; alphaStep < 16; ++alphaStep) { + auto shadowHit = isect.intersect(shadowRay, sceneAS); + if (shadowHit.type == intersection_type::none) { + return false; + } + + uint primitiveIndex = + blasPrimitiveOffsets[shadowHit.geometry_id] + shadowHit.primitive_id; + uint objectIndex = primitiveObjects[primitiveIndex]; + Material material = materials[objectIndex]; + uint i0 = indices[primitiveIndex * 3 + 0]; + uint i1 = indices[primitiveIndex * 3 + 1]; + uint i2 = indices[primitiveIndex * 3 + 2]; + float2 bary = shadowHit.triangle_barycentric_coord; + float b0 = 1.0 - bary.x - bary.y; + float2 uv = float2(vertices[i0].uv) * b0 + + float2(vertices[i1].uv) * bary.x + + float2(vertices[i2].uv) * bary.y; + uv = uv * float2(material.textureScale) + + float2(material.textureOffset); + float opacity = resolveMaterialOpacity( + material, uv, sceneData.materialTextureCount, + PT_MATERIAL_TEXTURE_ARGS); + if (opacity >= 0.999 || rand(rng) < opacity) { + return true; + } + + float advance = shadowHit.distance + rayOffsetDistance(shadowRay.origin); + shadowRay.origin += shadowRay.direction * advance; + shadowRay.max_distance -= advance; + if (shadowRay.max_distance <= shadowBias) { + return false; + } + } + + return true; } -bool isOccludedDirectionalLight(DirectionalLightData light, float3 P, float3 N, - thread uint &rng, - intersector isect, - instance_acceleration_structure sceneAS) { +float3 sampleDirectionalLightDirection(DirectionalLightData light, + thread uint &rng) { float3 baseL = normalize(-light.direction); float3x3 basis = buildOrthonormalBasis(baseL); float sunRadius = 0.0025; @@ -582,64 +675,7 @@ bool isOccludedDirectionalLight(DirectionalLightData light, float3 P, float3 N, float phi = 2.0 * M_PI_F * u.y; float3 jittered = baseL + basis[0] * (r * cos(phi)) + basis[1] * (r * sin(phi)); - float3 L = normalize(jittered); - return isOccluded(isect, sceneAS, P, N, L, 1e30); -} - -bool isOccludedPointLight(PointLight light, float3 P, float3 N, - thread uint &rng, - intersector isect, - instance_acceleration_structure sceneAS) { - float lightRadius = clamp(light.range * 0.006, 0.005, 0.04); - float2 u = float2(rand(rng), rand(rng)); - float z = u.x * 2.0 - 1.0; - float r = sqrt(max(0.0, 1.0 - z * z)); - float phi = 2.0 * M_PI_F * u.y; - float3 sphereOffset = - float3(r * cos(phi), r * sin(phi), z) * lightRadius; - float3 sampledLightPos = light.position + sphereOffset; - - float3 toLight = sampledLightPos - P; - float dist2 = max(dot(toLight, toLight), 0.001); - float dist = sqrt(dist2); - float3 L = toLight / dist; - return isOccluded(isect, sceneAS, P, N, L, dist - 0.001); -} - -bool isOccludedSpotLight(SpotLight light, float3 P, float3 N, - thread uint &rng, - intersector isect, - instance_acceleration_structure sceneAS) { - float lightRadius = clamp(light.range * 0.004, 0.004, 0.03); - float2 u = float2(rand(rng), rand(rng)); - float z = u.x * 2.0 - 1.0; - float r = sqrt(max(0.0, 1.0 - z * z)); - float phi = 2.0 * M_PI_F * u.y; - float3 sphereOffset = - float3(r * cos(phi), r * sin(phi), z) * lightRadius; - float3 sampledLightPos = light.position + sphereOffset; - - float3 toLight = sampledLightPos - P; - float dist2 = max(dot(toLight, toLight), 1e-4); - float dist = sqrt(dist2); - float3 L = toLight / dist; - return isOccluded(isect, sceneAS, P, N, L, dist - 0.001); -} - -bool isOccludedAreaLight(AreaLight light, float3 P, float3 N, - thread uint &rng, - intersector isect, - instance_acceleration_structure sceneAS) { - float2 u = float2(rand(rng), rand(rng)); - float2 rect = (u * 2.0 - 1.0) * 0.25; - float3 sampledLightPos = light.position + light.right * rect.x + - light.up * rect.y; - - float3 toLight = sampledLightPos - P; - float dist2 = max(dot(toLight, toLight), 1e-4); - float dist = sqrt(dist2); - float3 L = toLight / dist; - return isOccluded(isect, sceneAS, P, N, L, dist - 0.001); + return normalize(jittered); } // --------------------------------------------------------------------------- @@ -666,6 +702,16 @@ float G_Smith(float NdotV, float NdotL, float roughness) { return gV * gL; } +float G1_SmithGGX(float NdotX, float roughness) { + float alpha = max(roughness * roughness, 1e-4); + float alphaSquared = alpha * alpha; + float cosineSquared = NdotX * NdotX; + return (2.0 * NdotX) / + max(NdotX + + sqrt(alphaSquared + (1.0 - alphaSquared) * cosineSquared), + 1e-6); +} + float disneyDiffuseFactor(float NdotV, float NdotL, float LdotH, float roughness) { float fd90 = 0.5 + 2.0 * LdotH * LdotH * roughness; @@ -683,9 +729,38 @@ float3 sampleGGX(float2 u, float roughness) { return float3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta); } +float3 sampleGGXVNDF(float3 localView, float roughness, float2 u) { + float alpha = max(roughness * roughness, 1e-3); + float3 stretchedView = + normalizeOr(float3(alpha * localView.x, alpha * localView.y, + max(localView.z, 1e-5)), + float3(0.0, 0.0, 1.0)); + float lensq = stretchedView.x * stretchedView.x + + stretchedView.y * stretchedView.y; + float3 tangentX = lensq > 1e-7 + ? float3(-stretchedView.y, stretchedView.x, 0.0) * + rsqrt(lensq) + : float3(1.0, 0.0, 0.0); + float3 tangentY = cross(stretchedView, tangentX); + float radius = sqrt(u.x); + float phi = 2.0 * M_PI_F * u.y; + float diskX = radius * cos(phi); + float diskY = radius * sin(phi); + float blend = 0.5 * (1.0 + stretchedView.z); + diskY = mix(sqrt(max(0.0, 1.0 - diskX * diskX)), diskY, blend); + float diskZ = sqrt(max(0.0, 1.0 - diskX * diskX - diskY * diskY)); + float3 visibleNormal = diskX * tangentX + diskY * tangentY + + diskZ * stretchedView; + return normalizeOr(float3(alpha * visibleNormal.x, + alpha * visibleNormal.y, + max(visibleNormal.z, 0.0)), + float3(0.0, 0.0, 1.0)); +} + // Full Cook-Torrance PBR for a single analytic light -float3 evalPBR(float3 albedo, float metallic, float roughness, float3 N, - float3 V, float3 L, float3 lightColor, float intensity) { +float3 evalPBR(float3 albedo, float metallic, float roughness, + float reflectivity, float3 N, float3 V, float3 L, + float3 lightColor, float intensity) { float3 H = normalize(V + L); float NdotL = max(dot(N, L), 0.0); float NdotV = max(dot(N, V), 1e-4); @@ -693,13 +768,16 @@ float3 evalPBR(float3 albedo, float metallic, float roughness, float3 N, float VdotH = max(dot(V, H), 0.0); float clampedRoughness = clamp(roughness, 0.045, 1.0); - float3 F0 = mix(float3(0.04), albedo, clamp(metallic, 0.0, 1.0)); + float3 baseF0 = mix(float3(0.04), albedo, clamp(metallic, 0.0, 1.0)); + float3 reflectedColor = mix(float3(1.0), albedo, metallic); + float3 F0 = mix(baseF0, reflectedColor, clamp(reflectivity, 0.0, 1.0)); float3 F = F_Schlick(VdotH, F0); float D = D_GGX(NdotH, clampedRoughness); float G = G_Smith(NdotV, NdotL, clampedRoughness); float3 specular = (D * G * F) / max(4.0 * NdotV * NdotL, 1e-4); - float3 kD = (1.0 - F) * (1.0 - clamp(metallic, 0.0, 1.0)); + float3 kD = (1.0 - F) * (1.0 - clamp(metallic, 0.0, 1.0)) * + (1.0 - clamp(reflectivity, 0.0, 1.0)); float diffuseFactor = disneyDiffuseFactor(NdotV, NdotL, max(dot(L, H), 0.0), clampedRoughness); float3 diffuse = (kD * albedo * diffuseFactor) / M_PI_F; @@ -728,41 +806,48 @@ float3 evalSubsurface(float3 albedo, float3 N, float3 V, float3 L, float3 evalTransmission(float3 albedo, float3 N, float3 V, float3 L, float3 lightColor, float intensity, float roughness, float ior) { - float3 H = normalize(V + L); - float NdotL = max(dot(N, -L), 0.0); - float VdotH = max(dot(V, H), 0.0); + float backLighting = max(dot(N, -L), 0.0); + float forwardAlignment = max(dot(-V, L), 0.0); float3 F0 = float3(pow((ior - 1.0) / (ior + 1.0), 2.0)); - float3 F = F_Schlick(VdotH, F0); + float3 F = F_Schlick(max(dot(N, V), 0.0), F0); float3 transmitTint = mix(float3(1.0), albedo, 0.1); - float3 transmitFactor = (1.0 - F) * transmitTint; - float D = D_GGX(max(dot(N, H), 0.0), roughness); - return transmitFactor * lightColor * intensity * D * NdotL * 2.2 / M_PI_F; + float lobeExponent = mix(96.0, 2.0, sqrt(clamp(roughness, 0.0, 1.0))); + float transmissionLobe = pow(forwardAlignment, lobeExponent); + return (1.0 - F) * transmitTint * lightColor * intensity * backLighting * + transmissionLobe; } // --------------------------------------------------------------------------- // Direct lighting with full PBR (replaces old evalDirectLighting) // --------------------------------------------------------------------------- -float3 evalDirectLightingPBR(intersector isect, - instance_acceleration_structure sceneAS, float3 P, - float3 N, float3 V, float3 albedo, float metallic, - float roughness, float ior, float transmittance, - float sssStrength, float sssThickness, +float3 evalDirectLightingPBR(intersector isect, + primitive_acceleration_structure sceneAS, float3 P, + float3 N, float3 Ng, float3 V, float3 albedo, + float metallic, float roughness, float reflectivity, + float ior, float transmittance, float sssStrength, + float sssThickness, thread uint &rng, constant DirectionalLightData &dirLight, constant SceneData &sceneData, constant PointLight *pointLights, constant SpotLight *spotLights, - constant AreaLight *areaLights) { + constant AreaLight *areaLights, + constant Material *materials, + constant uint *primitiveObjects, + constant uint *blasPrimitiveOffsets, + constant VertexData *vertices, + constant uint *indices, + PT_MATERIAL_TEXTURE_PARAMS) { float3 lighting = float3(0.0); float surfaceOpacity = clamp(1.0 - transmittance * (1.0 - metallic), 0.0, 1.0); // Directional if (sceneData.numDirectionalLights > 0) { - float3 L = normalize(-dirLight.direction); - float3 c = evalPBR(albedo, metallic, roughness, N, V, L, dirLight.color, - max(dirLight.intensity, 0.0)); + float3 L = sampleDirectionalLightDirection(dirLight, rng); + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, + dirLight.color, max(dirLight.intensity, 0.0)); float3 s = evalSubsurface(albedo, N, V, L, dirLight.color, max(dirLight.intensity, 0.0), roughness, sssStrength, sssThickness); @@ -770,16 +855,17 @@ float3 evalDirectLightingPBR(intersector isect, evalTransmission(albedo, N, V, L, dirLight.color, max(dirLight.intensity, 0.0), roughness, ior) * transmittance; - if (!isOccludedDirectionalLight(dirLight, P, N, rng, isect, sceneAS)) { - float3 lightContribution = clampLuminance((c + s) * surfaceOpacity, 8.0); - lighting += lightContribution; - lighting += clampLuminance(t, 12.0); + if (!isOccluded(isect, sceneAS, P, Ng, L, 1e30, rng, materials, + primitiveObjects, blasPrimitiveOffsets, vertices, + indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { + lighting += (c + s) * surfaceOpacity + t; } } // Point lights for (uint i = 0; i < sceneData.numPointLights; ++i) { - float3 toLight = pointLights[i].position - P; + float3 sampledPosition = pointLights[i].position; + float3 toLight = sampledPosition - P; float dist = max(length(toLight), 1e-4); float3 L = toLight / dist; float lightRange = max(pointLights[i].range, 1e-4); @@ -788,7 +874,7 @@ float3 evalDirectLightingPBR(intersector isect, float rangeFade = 1.0 - smoothstep(lightRange * 0.75, lightRange, dist); float atten = rangeFade / max(distSq, 1e-4); float intensity = max(pointLights[i].intensity, 0.0) * atten; - float3 c = evalPBR(albedo, metallic, roughness, N, V, L, + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, pointLights[i].color, intensity); float3 s = evalSubsurface(albedo, N, V, L, pointLights[i].color, intensity, @@ -796,16 +882,17 @@ float3 evalDirectLightingPBR(intersector isect, float3 t = evalTransmission(albedo, N, V, L, pointLights[i].color, intensity, roughness, ior) * transmittance; - if (!isOccludedPointLight(pointLights[i], P, N, rng, isect, sceneAS)) { - float3 lightContribution = clampLuminance((c + s) * surfaceOpacity, 8.0); - lighting += lightContribution; - lighting += clampLuminance(t, 12.0); + if (!isOccluded(isect, sceneAS, P, Ng, L, dist, rng, materials, + primitiveObjects, blasPrimitiveOffsets, vertices, + indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { + lighting += (c + s) * surfaceOpacity + t; } } // Spot lights for (uint i = 0; i < sceneData.numSpotLights; ++i) { - float3 toLight = spotLights[i].position - P; + float3 sampledPosition = spotLights[i].position; + float3 toLight = sampledPosition - P; float dist = max(length(toLight), 1e-4); float3 L = toLight / dist; float3 fwd = normalize(spotLights[i].direction); @@ -818,7 +905,7 @@ float3 evalDirectLightingPBR(intersector isect, float rangeFade = 1.0 - smoothstep(lightRange * 0.75, lightRange, dist); float atten = rangeFade / max(distSq, 1e-4); float intensity = max(spotLights[i].intensity, 0.0) * atten * spot; - float3 c = evalPBR(albedo, metallic, roughness, N, V, L, + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, spotLights[i].color, intensity); float3 s = evalSubsurface(albedo, N, V, L, spotLights[i].color, intensity, @@ -826,16 +913,21 @@ float3 evalDirectLightingPBR(intersector isect, float3 t = evalTransmission(albedo, N, V, L, spotLights[i].color, intensity, roughness, ior) * transmittance; - if (!isOccludedSpotLight(spotLights[i], P, N, rng, isect, sceneAS)) { - float3 lightContribution = clampLuminance((c + s) * surfaceOpacity, 8.0); - lighting += lightContribution; - lighting += clampLuminance(t, 12.0); + if (!isOccluded(isect, sceneAS, P, Ng, L, dist, rng, materials, + primitiveObjects, blasPrimitiveOffsets, vertices, + indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { + lighting += (c + s) * surfaceOpacity + t; } } // Area lights for (uint i = 0; i < sceneData.numAreaLights; ++i) { - float3 toLight = areaLights[i].position - P; + float2 lightSample = float2(rand(rng), rand(rng)) * 2.0 - 1.0; + float3 sampledPosition = + areaLights[i].position + + areaLights[i].right * (lightSample.x * areaLights[i].halfWidth) + + areaLights[i].up * (lightSample.y * areaLights[i].halfHeight); + float3 toLight = sampledPosition - P; float dist = max(length(toLight), 1e-4); float3 L = toLight / dist; float3 lightNorm = @@ -844,12 +936,11 @@ float3 evalDirectLightingPBR(intersector isect, ? abs(dot(lightNorm, -L)) : max(dot(lightNorm, -L), 0.0); float area = 4.0 * areaLights[i].halfWidth * areaLights[i].halfHeight; - float minDist = max( - max(areaLights[i].halfWidth, areaLights[i].halfHeight) * 0.5, 0.15); - float distSq = dist * dist + minDist * minDist; - float atten = (cosLight * area) / max(distSq, 1e-4); + float lightPdfArea = 1.0 / max(area, 1e-6); + float distSq = max(dist * dist, 1e-6); + float atten = cosLight / max(distSq * lightPdfArea, 1e-6); float intensity = max(areaLights[i].intensity, 0.0) * atten; - float3 c = evalPBR(albedo, metallic, roughness, N, V, L, + float3 c = evalPBR(albedo, metallic, roughness, reflectivity, N, V, L, areaLights[i].color, intensity); float3 s = evalSubsurface(albedo, N, V, L, areaLights[i].color, intensity, @@ -857,10 +948,10 @@ float3 evalDirectLightingPBR(intersector isect, float3 t = evalTransmission(albedo, N, V, L, areaLights[i].color, intensity, roughness, ior) * transmittance; - if (!isOccludedAreaLight(areaLights[i], P, N, rng, isect, sceneAS)) { - float3 lightContribution = clampLuminance((c + s) * surfaceOpacity, 8.0); - lighting += lightContribution; - lighting += clampLuminance(t, 12.0); + if (!isOccluded(isect, sceneAS, P, Ng, L, dist, rng, materials, + primitiveObjects, blasPrimitiveOffsets, vertices, + indices, sceneData, PT_MATERIAL_TEXTURE_ARGS)) { + lighting += (c + s) * surfaceOpacity + t; } } @@ -868,13 +959,15 @@ float3 evalDirectLightingPBR(intersector isect, } // --------------------------------------------------------------------------- -// sampleRadiance — primary path with GGX importance-sampled indirect bounce +// sampleRadiance — iterative path with GGX importance-sampled indirect bounces // --------------------------------------------------------------------------- float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, - intersector isect, - instance_acceleration_structure sceneAS, ray primaryRay, - constant Material *materials, constant MeshData *meshData, + intersector isect, + primitive_acceleration_structure sceneAS, ray primaryRay, + constant Material *materials, + constant uint *primitiveObjects, + constant uint *blasPrimitiveOffsets, constant VertexData *vertices, constant uint *indices, constant InstanceData *instanceData, constant DirectionalLightData &dirLight, @@ -891,386 +984,339 @@ float3 sampleRadiance(uint2 gid, uint sampleIndex, uint w, thread float &primaryHitDistance, thread uint &primaryObjectId) { uint rng = seedBase(gid, w, sceneData.frameIndex, sampleIndex); - + uint bounceLimit = min(sceneData.maxBounces, 16u); ray surfaceRay = primaryRay; - auto hit = isect.intersect(surfaceRay, sceneAS, 0xFF); - Material mat{}; - MeshData mesh{}; - InstanceData inst{}; - float2 texUV = float2(0.0); - float3 localN = float3(0.0, 1.0, 0.0); - float3 localT = float3(1.0, 0.0, 0.0); - float3 localB = float3(0.0, 0.0, 1.0); - bool foundOpaqueSurface = false; - - for (uint step = 0; step < 8; ++step) { - if (hit.type == intersection_type::none) { - return skyColor(surfaceRay.direction, 0.0, skybox, sceneData); - } - - uint instanceIndex = hit.instance_id; - uint primitiveIndex = hit.primitive_id; - - mat = materials[instanceIndex]; - mesh = meshData[instanceIndex]; - inst = instanceData[instanceIndex]; + float3 radiance = float3(0.0); + float3 throughput = float3(1.0); + float previousBsdfPdf = 0.0; + float previousEnvironmentPdf = 0.0; + bool previousEventWasDelta = true; + + for (uint depth = 0; depth <= bounceLimit; ++depth) { + auto hit = isect.intersect(surfaceRay, sceneAS); + Material mat{}; + InstanceData inst{}; + uint surfaceObjectIndex = 0xFFFFFFFFu; + float2 texUV = float2(0.0); + float3 localN = float3(0.0, 1.0, 0.0); + float3 localT = float3(1.0, 0.0, 0.0); + float3 localB = float3(0.0, 0.0, 1.0); + float3 geometricNormal = float3(0.0, 1.0, 0.0); + bool foundSurface = false; + + for (uint alphaStep = 0; alphaStep < 16; ++alphaStep) { + if (hit.type == intersection_type::none) { + break; + } - uint i0 = indices[mesh.indexOffset + primitiveIndex * 3 + 0]; - uint i1 = indices[mesh.indexOffset + primitiveIndex * 3 + 1]; - uint i2 = indices[mesh.indexOffset + primitiveIndex * 3 + 2]; + uint primitiveIndex = + blasPrimitiveOffsets[hit.geometry_id] + hit.primitive_id; + surfaceObjectIndex = primitiveObjects[primitiveIndex]; + mat = materials[surfaceObjectIndex]; + inst = instanceData[surfaceObjectIndex]; + + uint i0 = indices[primitiveIndex * 3 + 0]; + uint i1 = indices[primitiveIndex * 3 + 1]; + uint i2 = indices[primitiveIndex * 3 + 2]; + float2 bary = hit.triangle_barycentric_coord; + float b0 = 1.0 - bary.x - bary.y; + float b1 = bary.x; + float b2 = bary.y; + + texUV = float2(vertices[i0].uv) * b0 + + float2(vertices[i1].uv) * b1 + + float2(vertices[i2].uv) * b2; + texUV = texUV * float2(mat.textureScale) + + float2(mat.textureOffset); + localN = normalizeOr(float3(vertices[i0].normal) * b0 + + float3(vertices[i1].normal) * b1 + + float3(vertices[i2].normal) * b2, + float3(0.0, 1.0, 0.0)); + localT = normalizeOr(float3(vertices[i0].tangent) * b0 + + float3(vertices[i1].tangent) * b1 + + float3(vertices[i2].tangent) * b2, + float3(1.0, 0.0, 0.0)); + localB = normalizeOr(float3(vertices[i0].bitangent) * b0 + + float3(vertices[i1].bitangent) * b1 + + float3(vertices[i2].bitangent) * b2, + float3(0.0, 0.0, 1.0)); + float3 p0 = float3(vertices[i0].position); + float3 p1 = float3(vertices[i1].position); + float3 p2 = float3(vertices[i2].position); + float3x3 normalMatrix = float3x3( + inst.normalCol0.xyz, inst.normalCol1.xyz, inst.normalCol2.xyz); + geometricNormal = normalizeOr( + cross(p1 - p0, p2 - p0), + normalizeOr(normalMatrix * localN, float3(0.0, 1.0, 0.0))); + + float alpha = resolveMaterialOpacity( + mat, texUV, sceneData.materialTextureCount, + PT_MATERIAL_TEXTURE_ARGS); + if (alpha >= 0.999 || rand(rng) < alpha) { + foundSurface = true; + break; + } - float2 bary = hit.triangle_barycentric_coord; - float b0 = 1.0 - bary.x - bary.y; - float b1 = bary.x; - float b2 = bary.y; - - texUV = float2(vertices[i0].uv) * b0 + float2(vertices[i1].uv) * b1 + - float2(vertices[i2].uv) * b2; - localN = normalizeOr(float3(vertices[i0].normal) * b0 + - float3(vertices[i1].normal) * b1 + - float3(vertices[i2].normal) * b2, - float3(0.0, 1.0, 0.0)); - localT = normalizeOr(float3(vertices[i0].tangent) * b0 + - float3(vertices[i1].tangent) * b1 + - float3(vertices[i2].tangent) * b2, - float3(1.0, 0.0, 0.0)); - localB = normalizeOr(float3(vertices[i0].bitangent) * b0 + - float3(vertices[i1].bitangent) * b1 + - float3(vertices[i2].bitangent) * b2, - float3(0.0, 0.0, 1.0)); - - float alpha = 1.0; - if (mat.opacityTextureIndex >= 0 && - uint(mat.opacityTextureIndex) < sceneData.materialTextureCount) { - alpha = clamp(sampleMaterialTexture(mat.opacityTextureIndex, texUV, - PT_MATERIAL_TEXTURE_ARGS) - .x, - 0.0, 1.0); + float3 rejectedPosition = + surfaceRay.origin + surfaceRay.direction * hit.distance; + surfaceRay.origin = + rejectedPosition + surfaceRay.direction * + rayOffsetDistance(rejectedPosition); + surfaceRay.min_distance = 0.0; + hit = isect.intersect(surfaceRay, sceneAS); } - if (alpha >= 0.1) { - foundOpaqueSurface = true; + if (!foundSurface) { + float misWeight = previousEventWasDelta + ? 1.0 + : powerHeuristic(previousBsdfPdf, + previousEnvironmentPdf); + radiance += throughput * misWeight * + skyColor(surfaceRay.direction, 0.0, skybox, sceneData); break; } - surfaceRay.origin = - surfaceRay.origin + surfaceRay.direction * (hit.distance + 0.001); - surfaceRay.min_distance = 0.0; - surfaceRay.max_distance = primaryRay.max_distance; - hit = isect.intersect(surfaceRay, sceneAS, 0xFF); - } + float3 shadingNormal = resolveShadingNormal( + mat, texUV, localN, localT, localB, inst, + sceneData.materialTextureCount, PT_MATERIAL_TEXTURE_ARGS); + float3 P = surfaceRay.origin + surfaceRay.direction * hit.distance; + float3 V = normalize(-surfaceRay.direction); + bool frontFace = dot(geometricNormal, V) >= 0.0; + float3 Ng = frontFace ? geometricNormal : -geometricNormal; + float3 N = dot(shadingNormal, Ng) >= 0.0 ? shadingNormal : -shadingNormal; + float shadingNormalCosine = dot(N, Ng); + if (shadingNormalCosine < 0.1) { + N = normalizeOr(N + Ng * (0.1 - shadingNormalCosine), Ng); + } - if (!foundOpaqueSurface) { - return skyColor(surfaceRay.direction, 0.0, skybox, sceneData); - } + float3 albedo; + float metallic; + float roughness; + float ao; + float3 emissive; + float ior; + float transmittance; + resolveMaterialParameters(mat, texUV, sceneData.materialTextureCount, + PT_MATERIAL_TEXTURE_ARGS, albedo, metallic, + roughness, ao, emissive, ior, transmittance); + + if (depth == 0) { + primaryAlbedo = albedo; + primaryNormal = N; + primaryPosition = P; + primaryDepth = length(P - primaryRay.origin); + primaryRoughness = roughness; + primaryHitDistance = hit.distance; + primaryObjectId = surfaceObjectIndex; + } - float3 N = resolveShadingNormal(mat, texUV, localN, localT, localB, inst, - sceneData.materialTextureCount, - PT_MATERIAL_TEXTURE_ARGS); - float3 P = surfaceRay.origin + surfaceRay.direction * hit.distance; - float3 V = normalize(-surfaceRay.direction); - if (dot(N, V) < 0.0) { - N = -N; - } + float reflectivity = clamp(mat.reflectivity, 0.0, 1.0); + float sssStrength = 0.0; + float sssThickness = mix(0.25, 1.75, ao); + float3 direct = evalDirectLightingPBR( + isect, sceneAS, P, N, Ng, V, albedo, metallic, roughness, + reflectivity, ior, transmittance, sssStrength, sssThickness, rng, + dirLight, sceneData, pointLights, spotLights, areaLights, materials, + primitiveObjects, blasPrimitiveOffsets, vertices, indices, + PT_MATERIAL_TEXTURE_ARGS); + radiance += throughput * (direct + emissive); + + if (depth == 0 && sceneData.ambientIntensity > 0.0) { + float aoVisibility = mix(0.2, 1.0, ao); + float3 ambientF0 = mix(float3(0.04), albedo, metallic); + float3 ambientF = F_Schlick(max(dot(N, V), 0.0), ambientF0); + float3 ambientDiffuse = (1.0 - ambientF) * (1.0 - metallic) * + albedo * (1.0 - transmittance); + float3 ambientSpecular = + ambientF * mix(1.0, 0.35, roughness); + float3 ambient = (ambientDiffuse + ambientSpecular) * + sceneData.ambientColor * + sceneData.ambientIntensity * aoVisibility; + radiance += throughput * ambient; + } - float3 albedo; - float metallic; - float roughness; - float ao; - float3 emissive; - float ior; - float transmittance; - resolveMaterialParameters(mat, texUV, sceneData.materialTextureCount, - PT_MATERIAL_TEXTURE_ARGS, albedo, metallic, - roughness, ao, emissive, ior, transmittance); - primaryAlbedo = albedo; - primaryNormal = N; - primaryPosition = P; - primaryDepth = length(P - primaryRay.origin); - primaryRoughness = roughness; - primaryHitDistance = hit.distance; - primaryObjectId = hit.instance_id; - float reflectivity = clamp(mat.reflectivity, 0.0, 1.0); - float sssStrength = clamp(1.0 - mat.albedo.w, 0.0, 1.0) * (1.0 - metallic); - float sssThickness = mix(0.25, 1.75, ao); - - float3 direct = evalDirectLightingPBR( - isect, sceneAS, P, N, V, albedo, metallic, roughness, ior, - transmittance, sssStrength, sssThickness, rng, dirLight, sceneData, - pointLights, spotLights, areaLights); - - float3 indirect = float3(0.0); - - if (sceneData.maxBounces > 0) { float3 baseF0 = mix(float3(0.04), albedo, metallic); float3 reflectedColor = mix(float3(1.0), albedo, metallic); float3 F0 = mix(baseF0, reflectedColor, reflectivity); - float3 F_approx = F_Schlick(max(dot(N, V), 0.0), F0); - + float NdotV = max(dot(N, V), 1e-4); float dielectricF0 = pow((ior - 1.0) / (ior + 1.0), 2.0); - float dielectricSpec = - F_Schlick(max(dot(N, V), 0.0), float3(dielectricF0)).x; + float dielectricFresnel = + F_Schlick(NdotV, float3(dielectricF0)).x; float specProb = metallic * mix(0.35, 0.9, 1.0 - roughness) + - (1.0 - metallic) * dielectricSpec; - float transmitProb = - transmittance * (1.0 - metallic) * (1.0 - dielectricSpec); + (1.0 - metallic) * dielectricFresnel; + float transmitProb = transmittance * (1.0 - metallic) * + (1.0 - dielectricFresnel); float diffuseProb = (1.0 - metallic) * (1.0 - transmittance); specProb = mix(specProb, 1.0, reflectivity); transmitProb *= 1.0 - reflectivity; diffuseProb *= 1.0 - reflectivity; - float probSum = max(specProb + transmitProb + diffuseProb, 1e-4); - specProb /= probSum; - transmitProb /= probSum; - diffuseProb /= probSum; + float eta = frontFace ? 1.0 / ior : ior; + float3 idealRefractedDirection = refract(-V, N, eta); + bool totalInternalReflection = + dot(idealRefractedDirection, idealRefractedDirection) < 1e-8; + if (totalInternalReflection) { + specProb += transmitProb; + transmitProb = 0.0; + } + float probabilitySum = + max(specProb + transmitProb + diffuseProb, 1e-4); + specProb /= probabilitySum; + transmitProb /= probabilitySum; + diffuseProb /= probabilitySum; float3x3 basis = buildOrthonormalBasis(N); - ray bounceRay; - bounceRay.origin = P + N * 0.001; - bounceRay.min_distance = 0.0; - bounceRay.max_distance = 1.0e30; - - float3 brdfWeight; - float chooseSplit = rand(rng); - bool choseTransmission = false; - bool choseSpecular = false; - - if (specProb > 1e-4 && chooseSplit < specProb) { - choseSpecular = true; - float2 u = float2(rand(rng), rand(rng)); - float3 localH = sampleGGX(u, max(roughness, 0.001)); - float3 H_world = normalize(basis * localH); - float3 bounceDir = reflect(-V, H_world); - - if (dot(bounceDir, N) <= 0.0) - bounceDir = reflect(-V, N); - - bounceRay.direction = bounceDir; - - float NdotL2 = max(dot(N, bounceDir), 1e-4); - float NdotV2 = max(dot(N, V), 1e-4); - float3 Fs = F_Schlick(max(dot(V, H_world), 0.0), F0); - float Gs = G_Smith(NdotV2, NdotL2, roughness); - - brdfWeight = - (Fs * Gs / max(4.0 * NdotV2, 1e-4)) / max(specProb, 1e-4); - - } else if (transmitProb > 1e-4 && chooseSplit < specProb + transmitProb) { - choseTransmission = true; - bool entering = dot(N, V) > 0.0; - float eta = entering ? (1.0 / ior) : ior; - float3 faceN = entering ? N : -N; - - float3 refractDir = refract(-V, faceN, eta); - - if (length(refractDir) < 1e-5) { - refractDir = reflect(-V, faceN); + if (sceneData.environmentEnabled != 0 && + diffuseProb + specProb > 1e-4) { + float3 localEnvironmentDirection = + cosineSampleHemisphere(float2(rand(rng), rand(rng))); + float3 environmentDirection = + normalizeOr(basis * localEnvironmentDirection, N); + float NdotEnvironment = dot(N, environmentDirection); + if (NdotEnvironment > 0.0 && + dot(Ng, environmentDirection) > 0.0 && + !isOccluded(isect, sceneAS, P, Ng, environmentDirection, 1e30, + rng, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, sceneData, + PT_MATERIAL_TEXTURE_ARGS)) { + float3 H = normalizeOr(V + environmentDirection, N); + float NdotH = max(dot(N, H), 1e-5); + float VdotH = max(dot(V, H), 1e-5); + float3 F = F_Schlick(VdotH, F0); + float3 kD = (1.0 - F) * (1.0 - metallic) * + (1.0 - transmittance) * (1.0 - reflectivity); + float diffuseFactor = disneyDiffuseFactor( + NdotV, NdotEnvironment, + max(dot(environmentDirection, H), 0.0), roughness); + float3 reflectionBsdf = + kD * albedo * diffuseFactor / M_PI_F; + float environmentPdf = NdotEnvironment / M_PI_F; + float bsdfPdf = diffuseProb * environmentPdf; + if (roughness > 0.025 && specProb > 1e-4) { + float D = D_GGX(NdotH, roughness); + float G1V = G1_SmithGGX(NdotV, roughness); + float G1L = + G1_SmithGGX(NdotEnvironment, roughness); + reflectionBsdf += + D * G1V * G1L * F / + max(4.0 * NdotV * NdotEnvironment, 1e-6); + bsdfPdf += specProb * D * G1V / + max(4.0 * NdotV, 1e-6); + } + float competingBsdfPdf = depth < bounceLimit ? bsdfPdf : 0.0; + float misWeight = + powerHeuristic(environmentPdf, competingBsdfPdf); + float3 environmentRadiance = skyColor( + environmentDirection, 0.0, skybox, sceneData); + radiance += throughput * reflectionBsdf * + environmentRadiance * NdotEnvironment * misWeight / + max(environmentPdf, 1e-6); } - - bounceRay.origin = P - faceN * 0.002; - bounceRay.direction = normalize(refractDir); - - float3 F0t = float3(pow((ior - 1.0) / (ior + 1.0), 2.0)); - float3 Ft = F_Schlick(max(dot(V, faceN), 0.0), F0t); - float3 kT = (1.0 - Ft) * mix(float3(1.0), albedo, 0.15); - - float3 absorption = exp(-(1.0 - albedo) * 0.12); - - brdfWeight = (kT * absorption) / max(transmitProb, 1e-4); - - } else { - float2 u = float2(rand(rng), rand(rng)); - float3 localBounce = cosineSampleHemisphere(u); - bounceRay.direction = normalize(basis * localBounce); - - float3 kD = (1.0 - F_approx) * (1.0 - metallic); - brdfWeight = (kD * albedo) / max(diffuseProb, 1e-4); } - brdfWeight = clamp(brdfWeight, float3(0.0), float3(8.0)); - - auto bounceHit = isect.intersect(bounceRay, sceneAS, 0xFF); - auto resolvedBounceHit = bounceHit; - ray resolvedBounceRay = bounceRay; - - if (choseTransmission) { - for (uint shellStep = 0; shellStep < 8; ++shellStep) { - if (resolvedBounceHit.type == intersection_type::none) { - break; - } - - uint ti = resolvedBounceHit.instance_id; - uint tp = resolvedBounceHit.primitive_id; - - Material tmat = materials[ti]; - MeshData tmesh = meshData[ti]; - InstanceData tinst = instanceData[ti]; - - uint tj0 = indices[tmesh.indexOffset + tp * 3 + 0]; - uint tj1 = indices[tmesh.indexOffset + tp * 3 + 1]; - uint tj2 = indices[tmesh.indexOffset + tp * 3 + 2]; - - float2 tbary = resolvedBounceHit.triangle_barycentric_coord; - float tb0 = 1.0 - tbary.x - tbary.y; - float tb1 = tbary.x; - float tb2 = tbary.y; - - float2 tUV = float2(vertices[tj0].uv) * tb0 + - float2(vertices[tj1].uv) * tb1 + - float2(vertices[tj2].uv) * tb2; - float3 tLocalN = normalizeOr(float3(vertices[tj0].normal) * tb0 + - float3(vertices[tj1].normal) * tb1 + - float3(vertices[tj2].normal) * tb2, - float3(0.0, 1.0, 0.0)); - float3 tLocalT = normalizeOr(float3(vertices[tj0].tangent) * tb0 + - float3(vertices[tj1].tangent) * tb1 + - float3(vertices[tj2].tangent) * tb2, - float3(1.0, 0.0, 0.0)); - float3 tLocalB = normalizeOr(float3(vertices[tj0].bitangent) * tb0 + - float3(vertices[tj1].bitangent) * tb1 + - float3(vertices[tj2].bitangent) * tb2, - float3(0.0, 0.0, 1.0)); - - float3 tN = resolveShadingNormal(tmat, tUV, tLocalN, tLocalT, - tLocalB, tinst, - sceneData.materialTextureCount, - PT_MATERIAL_TEXTURE_ARGS); - float3 tP = resolvedBounceRay.origin + - resolvedBounceRay.direction * resolvedBounceHit.distance; - float3 tV = normalize(-resolvedBounceRay.direction); - if (dot(tN, tV) < 0.0) { - tN = -tN; - } - - float3 tAlbedo; - float tMetallic; - float tRoughness; - float tAo; - float3 tEmissive; - float tIor; - float tTransmittance; - resolveMaterialParameters( - tmat, tUV, sceneData.materialTextureCount, - PT_MATERIAL_TEXTURE_ARGS, tAlbedo, tMetallic, tRoughness, tAo, - tEmissive, tIor, tTransmittance); - - if (tTransmittance < 0.5 || tMetallic > 0.5) { - break; - } + if (depth == bounceLimit) { + break; + } - bool enteringShell = dot(tN, tV) > 0.0; - float etaShell = enteringShell ? (1.0 / tIor) : tIor; - float3 faceNShell = enteringShell ? tN : -tN; - float3 refractShell = refract(-tV, faceNShell, etaShell); - if (length(refractShell) < 1e-5) { - refractShell = reflect(-tV, faceNShell); + float choice = rand(rng); + float3 bounceWeight = float3(0.0); + float3 nextDirection = N; + float sampledBsdfPdf = 0.0; + float sampledEnvironmentPdf = 0.0; + bool sampledEventWasDelta = true; + + if (choice < specProb && specProb > 1e-4) { + if (roughness <= 0.025 || totalInternalReflection) { + nextDirection = reflect(-V, N); + float3 F = totalInternalReflection + ? float3(1.0) + : F_Schlick(NdotV, F0); + bounceWeight = F / max(specProb, 1e-4); + } else { + float3 localView = + float3(dot(V, basis[0]), dot(V, basis[1]), dot(V, N)); + float3 localH = sampleGGXVNDF( + localView, roughness, float2(rand(rng), rand(rng))); + float3 H = normalizeOr(basis * localH, N); + float VdotH = max(dot(V, H), 1e-5); + nextDirection = reflect(-V, H); + float NdotL = dot(N, nextDirection); + if (NdotL > 0.0 && dot(nextDirection, Ng) > 0.0) { + float NdotH = max(dot(N, H), 1e-5); + float D = D_GGX(NdotH, roughness); + float G1V = G1_SmithGGX(NdotV, roughness); + float G1L = G1_SmithGGX(NdotL, roughness); + float3 F = F_Schlick(VdotH, F0); + float3 specularBsdf = + D * G1V * G1L * F / + max(4.0 * NdotV * NdotL, 1e-6); + float conditionalPdf = + D * G1V / max(4.0 * NdotV, 1e-6); + float combinedPdf = specProb * conditionalPdf; + bounceWeight = specularBsdf * NdotL / + max(combinedPdf, 1e-6); + sampledBsdfPdf = combinedPdf; + sampledEnvironmentPdf = NdotL / M_PI_F; + sampledEventWasDelta = false; } - - resolvedBounceRay.origin = tP - faceNShell * 0.002; - resolvedBounceRay.direction = normalize(refractShell); - resolvedBounceRay.min_distance = 0.0; - resolvedBounceRay.max_distance = 1.0e30; - resolvedBounceHit = - isect.intersect(resolvedBounceRay, sceneAS, 0xFF); } + } else if (choice < specProb + transmitProb && + transmitProb > 1e-4) { + nextDirection = idealRefractedDirection; + float3 F = F_Schlick(NdotV, float3(dielectricF0)); + float3 tint = mix(float3(1.0), albedo, 0.15); + bounceWeight = (1.0 - F) * tint / + max(transmitProb, 1e-4); + } else { + float3 localDirection = + cosineSampleHemisphere(float2(rand(rng), rand(rng))); + nextDirection = normalizeOr(basis * localDirection, N); + float NdotL = max(dot(N, nextDirection), 0.0); + float3 H = normalizeOr(V + nextDirection, N); + float3 F = F_Schlick(max(dot(V, H), 0.0), F0); + float3 kD = (1.0 - F) * (1.0 - metallic); + float diffuseFactor = disneyDiffuseFactor( + NdotV, NdotL, max(dot(nextDirection, H), 0.0), roughness); + float3 diffuseBsdf = kD * albedo * diffuseFactor / M_PI_F; + float conditionalPdf = NdotL / M_PI_F; + float combinedPdf = diffuseProb * conditionalPdf; + bounceWeight = diffuseBsdf * NdotL / + max(combinedPdf, 1e-6); + sampledBsdfPdf = combinedPdf; + sampledEnvironmentPdf = conditionalPdf; + sampledEventWasDelta = false; } - if (resolvedBounceHit.type == intersection_type::none) { - float bounceStrength = choseSpecular - ? mix(sceneData.indirectStrength, 1.0, - max(metallic, reflectivity)) - : sceneData.indirectStrength; - indirect = brdfWeight * - skyColor(resolvedBounceRay.direction, 0.0, skybox, - sceneData) * - bounceStrength; - } else { - uint bi = resolvedBounceHit.instance_id; - uint bp = resolvedBounceHit.primitive_id; - - Material bmat = materials[bi]; - MeshData bmesh = meshData[bi]; - InstanceData binst = instanceData[bi]; - - uint bj0 = indices[bmesh.indexOffset + bp * 3 + 0]; - uint bj1 = indices[bmesh.indexOffset + bp * 3 + 1]; - uint bj2 = indices[bmesh.indexOffset + bp * 3 + 2]; - - float2 bbary = resolvedBounceHit.triangle_barycentric_coord; - float bb0 = 1.0 - bbary.x - bbary.y; - float bb1 = bbary.x; - float bb2 = bbary.y; - - float2 bUV = float2(vertices[bj0].uv) * bb0 + - float2(vertices[bj1].uv) * bb1 + - float2(vertices[bj2].uv) * bb2; - float3 bLocalN = - normalizeOr(float3(vertices[bj0].normal) * bb0 + - float3(vertices[bj1].normal) * bb1 + - float3(vertices[bj2].normal) * bb2, - float3(0.0, 1.0, 0.0)); - float3 bLocalT = - normalizeOr(float3(vertices[bj0].tangent) * bb0 + - float3(vertices[bj1].tangent) * bb1 + - float3(vertices[bj2].tangent) * bb2, - float3(1.0, 0.0, 0.0)); - float3 bLocalB = - normalizeOr(float3(vertices[bj0].bitangent) * bb0 + - float3(vertices[bj1].bitangent) * bb1 + - float3(vertices[bj2].bitangent) * bb2, - float3(0.0, 0.0, 1.0)); - float3 bN = resolveShadingNormal( - bmat, bUV, bLocalN, bLocalT, bLocalB, binst, - sceneData.materialTextureCount, PT_MATERIAL_TEXTURE_ARGS); - float3 bP = resolvedBounceRay.origin + - resolvedBounceRay.direction * resolvedBounceHit.distance; - float3 bV = normalize(-resolvedBounceRay.direction); - if (dot(bN, bV) < 0.0) { - bN = -bN; - } + throughput *= max(bounceWeight, float3(0.0)); + if (depth == 0) { + throughput *= max(sceneData.indirectStrength, 0.0); + } + if (!all(isfinite(throughput)) || max(throughput.x, + max(throughput.y, throughput.z)) < + 1e-5) { + break; + } - float3 bAlbedo; - float bMetallic; - float bRoughness; - float bAo; - float3 bEmissive; - float bIor; - float bTransmittance; - resolveMaterialParameters(bmat, bUV, sceneData.materialTextureCount, - PT_MATERIAL_TEXTURE_ARGS, bAlbedo, - bMetallic, bRoughness, bAo, bEmissive, - bIor, bTransmittance); - - float bSssStrength = - clamp(1.0 - bmat.albedo.w, 0.0, 1.0) * (1.0 - bMetallic); - float bSssThickness = mix(0.25, 1.75, bAo); - - float3 bounceDirect = evalDirectLightingPBR( - isect, sceneAS, bP, bN, bV, bAlbedo, bMetallic, bRoughness, - bIor, bTransmittance, bSssStrength, bSssThickness, rng, dirLight, - sceneData, pointLights, spotLights, areaLights); - if (choseTransmission) { - float causticFocus = mix(1.0, 4.0, - transmittance * (1.0 - roughness)); - bounceDirect *= causticFocus; + if (depth >= 2) { + float survival = clamp(max(throughput.x, + max(throughput.y, throughput.z)), + 0.05, 0.95); + if (rand(rng) > survival) { + break; } - - float3 bAmbient = bAlbedo * max(sceneData.ambientIntensity, 0.0) * - (1.0 - bMetallic) * bAo; - float bounceStrength = choseSpecular - ? mix(sceneData.indirectStrength, 1.0, - max(metallic, reflectivity)) - : sceneData.indirectStrength; - indirect = brdfWeight * (bAmbient + bounceDirect + bEmissive) * - bounceStrength; + throughput /= survival; } - indirect = clampLuminance(indirect, 16.0); + previousBsdfPdf = sampledBsdfPdf; + previousEnvironmentPdf = sampledEnvironmentPdf; + previousEventWasDelta = sampledEventWasDelta; + + surfaceRay.origin = offsetRayOrigin(P, Ng, nextDirection); + surfaceRay.direction = normalizeOr(nextDirection, N); + surfaceRay.min_distance = 0.0; + surfaceRay.max_distance = 1.0e30; } - float3 ambient = - albedo * max(sceneData.ambientIntensity, 0.0) * (1.0 - metallic) * ao * - (1.0 - transmittance); - return clampLuminance(ambient + direct + emissive + indirect, 24.0); + return radiance; } kernel void main0(texture2d outTex [[texture(0)]], @@ -1281,10 +1327,10 @@ kernel void main0(texture2d outTex [[texture(0)]], texture2d motionObjectTex [[texture(5)]], texture2d momentsHitTex [[texture(6)]], texture2d historyGuideTex [[texture(7)]], - instance_acceleration_structure sceneAS [[buffer(0)]], + primitive_acceleration_structure sceneAS [[buffer(0)]], constant CameraUniforms &cam [[buffer(1)]], constant Material *materials [[buffer(2)]], - constant MeshData *meshData [[buffer(3)]], + constant uint *primitiveObjects [[buffer(3)]], constant VertexData *vertices [[buffer(4)]], constant uint *indices [[buffer(5)]], constant InstanceData *instanceData [[buffer(6)]], @@ -1294,25 +1340,20 @@ kernel void main0(texture2d outTex [[texture(0)]], constant SpotLight *spotLights [[buffer(10)]], constant AreaLight *areaLights [[buffer(11)]], PT_MATERIAL_TEXTURE_BINDINGS, + constant uint *blasPrimitiveOffsets [[buffer(13)]], texturecube skybox [[texture(60)]], uint2 gid [[thread_position_in_grid]]) { uint w = outTex.get_width(); uint h = outTex.get_height(); + uint pixelStride = max(sceneData.pixelStride, 1u); + gid *= pixelStride; if (gid.x >= w || gid.y >= h) return; float2 uv = (float2(gid) + 0.5) / float2(w, h); - float2 ndc = uv * 2.0 - 1.0; - ndc.y = -ndc.y; - - float4 clip = float4(ndc, 1.0, 1.0); - float4 worldH = cam.invViewProj * clip; - float3 worldP = worldH.xyz / worldH.w; - float3 ro = cam.camPos; - float3 rd = normalize(worldP - ro); - intersector isect; + intersector isect; isect.assume_geometry_type(geometry_type::triangle); isect.set_triangle_cull_mode(triangle_cull_mode::none); @@ -1327,22 +1368,54 @@ kernel void main0(texture2d outTex [[texture(0)]], uint spp = max(sceneData.raysPerPixel, 1u); for (uint s = 0; s < spp; ++s) { + uint cameraRng = seedBase(gid, w, sceneData.frameIndex, + s + 0x9E3779B9u); + float2 pixelJitter = + float2(rand(cameraRng), rand(cameraRng)) - 0.5; + float2 sampleUv = (float2(gid) + 0.5 + pixelJitter) / float2(w, h); + float2 sampleNdc = sampleUv * 2.0 - 1.0; + sampleNdc.y = -sampleNdc.y; + float4 sampleClip = float4(sampleNdc, 1.0, 1.0); + float4 sampleWorldH = cam.invViewProj * sampleClip; + float3 sampleWorldP = sampleWorldH.xyz / sampleWorldH.w; + ray primaryRay; primaryRay.origin = ro; - primaryRay.direction = rd; + primaryRay.direction = normalize(sampleWorldP - ro); primaryRay.min_distance = 0.001; primaryRay.max_distance = 1.0e30; + float3 sampleAlbedo = float3(0.0); + float3 sampleNormal = float3(0.0); + float3 samplePosition = float3(0.0); + float sampleDepth = 0.0; + float sampleRoughness = 1.0; + float sampleHitDistance = 0.0; + uint sampleObjectId = 0xFFFFFFFFu; + float3 sample = sampleRadiance( - gid, s, w, isect, sceneAS, primaryRay, materials, meshData, - vertices, indices, instanceData, dirLight, sceneData, pointLights, - spotLights, areaLights, PT_MATERIAL_TEXTURE_ARGS, skybox, - primaryAlbedo, primaryNormal, primaryPosition, primaryDepth, - primaryRoughness, primaryHitDistance, primaryObjectId); - color += clampLuminance(sample, 24.0); + gid, s, w, isect, sceneAS, primaryRay, materials, primitiveObjects, + blasPrimitiveOffsets, vertices, indices, instanceData, dirLight, + sceneData, pointLights, spotLights, areaLights, + PT_MATERIAL_TEXTURE_ARGS, skybox, sampleAlbedo, sampleNormal, + samplePosition, sampleDepth, sampleRoughness, sampleHitDistance, + sampleObjectId); + color += sample; + if (s == 0) { + primaryAlbedo = sampleAlbedo; + primaryNormal = sampleNormal; + primaryPosition = samplePosition; + primaryDepth = sampleDepth; + primaryRoughness = sampleRoughness; + primaryHitDistance = sampleHitDistance; + primaryObjectId = sampleObjectId; + } } color /= float(spp); + if (!all(isfinite(color))) { + color = float3(0.0); + } int frameIndex = int(sceneData.frameIndex); @@ -1351,65 +1424,63 @@ kernel void main0(texture2d outTex [[texture(0)]], float objectIdValue = primaryObjectId == 0xFFFFFFFFu ? -1.0 : float(primaryObjectId); + float2 encodedNormal = encodeNormal(primaryNormal); float4 currentGuide = - float4(primaryNormal.xy, primaryDepth, objectIdValue); + float4(encodedNormal, primaryDepth, objectIdValue); bool historyValid = frameIndex > 0 && abs(previousGuide.z - primaryDepth) < max(0.05, primaryDepth * 0.02) && - distance(previousGuide.xy, primaryNormal.xy) < 0.12 && + distance(previousGuide.xy, encodedNormal) < 0.08 && abs(previousGuide.w - objectIdValue) < 0.5; if (frameIndex == 0) prevColor = float4(0, 0, 0, 1); + float sampleLuminanceLimit = + historyValid ? max(8.0, luminance(prevColor.xyz) * 6.0 + 2.0) : 128.0; + color = clampLuminance(color, sampleLuminanceLimit); if (!historyValid) prevColor = float4(color, 1.0); - if (frameIndex > 2) { - float prevL = luminance(prevColor.xyz); - float currL = luminance(color); - float maxAllowed = max(prevL * 1.6 + 0.15, 0.75); - if (currL > maxAllowed && currL > 1e-6) { - color *= maxAllowed / currL; - } - } - - float historyLength = historyValid ? min(float(frameIndex), 31.0) : 0.0; + float historyLength = historyValid ? min(float(frameIndex), 255.0) : 0.0; float3 lower = min(prevColor.xyz, color) - float3(0.35); float3 upper = max(prevColor.xyz, color) + float3(0.35); float3 clippedHistory = clamp(prevColor.xyz, lower, upper); float3 accum = mix(color, clippedHistory, historyLength / (historyLength + 1.0)); - accum = clampLuminance(accum, 24.0); - - constexpr float bloomThreshold = 1.0; - constexpr float bloomKnee = 0.5; - - float brightness = luminance(accum); - float soft = clamp( - brightness - bloomThreshold + bloomKnee, - 0.0, - bloomKnee * 2.0 - ); - - soft = soft * soft / max(bloomKnee * 4.0, 0.00001); - - float contribution = - max(brightness - bloomThreshold, soft) / - max(brightness, 0.00001); - - float3 brightColor = accum * contribution; + accum = clampLuminance(accum, 256.0); + + constexpr float bloomThreshold = 0.8; + constexpr float bloomKnee = 0.35; + + float brightness = luminance(accum); + float soft = clamp(brightness - bloomThreshold + bloomKnee, 0.0, + bloomKnee * 2.0); + soft = soft * soft / max(bloomKnee * 4.0, 0.00001); + float contribution = max(brightness - bloomThreshold, soft) / + max(brightness, 0.00001); + float3 brightColor = accum * contribution; float4 previousClip = cam.prevViewProj * float4(primaryPosition, 1.0); float2 previousUv = previousClip.xy / max(abs(previousClip.w), 0.0001); previousUv = previousUv * 0.5 + 0.5; float2 motion = uv - previousUv; float moment = luminance(color); - historyTex.write(float4(accum, 1.0), gid); - historyGuideTex.write(currentGuide, gid); - albedoRoughnessTex.write(float4(primaryAlbedo, primaryRoughness), gid); - normalDepthTex.write(float4(primaryNormal, primaryDepth), gid); - motionObjectTex.write(float4(motion, objectIdValue, 1.0), gid); - momentsHitTex.write(float4(moment, moment * moment, - primaryRoughness, primaryHitDistance), gid); - outTex.write(float4(accum, 1.0), gid); - brightTex.write(float4(brightColor, 1.0), gid); + for (uint y = 0; y < pixelStride; ++y) { + for (uint x = 0; x < pixelStride; ++x) { + uint2 pixel = gid + uint2(x, y); + if (pixel.x >= w || pixel.y >= h) { + continue; + } + historyTex.write(float4(accum, 1.0), pixel); + historyGuideTex.write(currentGuide, pixel); + albedoRoughnessTex.write(float4(primaryAlbedo, primaryRoughness), + pixel); + normalDepthTex.write(float4(primaryNormal, primaryDepth), pixel); + motionObjectTex.write(float4(motion, objectIdValue, 1.0), pixel); + momentsHitTex.write(float4(moment, moment * moment, + primaryRoughness, primaryHitDistance), + pixel); + outTex.write(float4(accum, 1.0), pixel); + brightTex.write(float4(brightColor, 1.0), pixel); + } + } } diff --git a/shaders/metal/path_tracing/path_denoise.metal b/shaders/metal/path_tracing/path_denoise.metal index 05bbd952..b855898c 100644 --- a/shaders/metal/path_tracing/path_denoise.metal +++ b/shaders/metal/path_tracing/path_denoise.metal @@ -9,40 +9,80 @@ kernel void main0(texture2d inputTexture [[texture(0)]], texture2d outputTexture [[texture(1)]], texture2d brightTexture [[texture(2)]], texture2d guideTexture [[texture(3)]], + texture2d albedoRoughnessTexture + [[texture(4)]], constant DenoiseParameters ¶meters [[buffer(0)]], uint2 gid [[thread_position_in_grid]]) { uint width = outputTexture.get_width(); uint height = outputTexture.get_height(); - if (gid.x >= width || gid.y >= height) return; + if (gid.x >= width || gid.y >= height) + return; - constexpr int2 offsets[9] = { - int2(0, 0), int2(1, 0), int2(-1, 0), int2(0, 1), int2(0, -1), - int2(1, 1), int2(-1, 1), int2(1, -1), int2(-1, -1)}; + constexpr int2 offsets[9] = {int2(0, 0), int2(1, 0), int2(-1, 0), + int2(0, 1), int2(0, -1), int2(1, 1), + int2(-1, 1), int2(1, -1), int2(-1, -1)}; constexpr float weights[9] = {0.28, 0.12, 0.12, 0.12, 0.12, 0.06, 0.06, 0.06, 0.06}; float3 center = inputTexture.read(gid).xyz; float4 centerGuide = guideTexture.read(gid); + float4 centerAlbedoRoughness = albedoRoughnessTexture.read(gid); + bool centerSurface = centerGuide.w > 0.0; + float centerNormalLength = dot(centerGuide.xyz, centerGuide.xyz); float centerLuminance = dot(center, float3(0.2126, 0.7152, 0.0722)); float3 filtered = float3(0.0); float totalWeight = 0.0; for (int i = 0; i < 9; ++i) { - int2 samplePosition = clamp(int2(gid) + offsets[i] * parameters.stepWidth, - int2(0), int2(width - 1, height - 1)); + int2 samplePosition = + clamp(int2(gid) + offsets[i] * parameters.stepWidth, int2(0), + int2(width - 1, height - 1)); float3 sampleColor = inputTexture.read(uint2(samplePosition)).xyz; float4 sampleGuide = guideTexture.read(uint2(samplePosition)); - float sampleLuminance = dot(sampleColor, float3(0.2126, 0.7152, 0.0722)); - float edgeWeight = exp(-abs(sampleLuminance - centerLuminance) * 6.0); - float normalWeight = - pow(max(dot(centerGuide.xyz, sampleGuide.xyz), 0.0), 24.0); - float depthWeight = exp(-abs(sampleGuide.w - centerGuide.w) / - max(0.05, centerGuide.w * 0.02)); - float weight = weights[i] * edgeWeight * normalWeight * depthWeight; + float4 sampleAlbedoRoughness = + albedoRoughnessTexture.read(uint2(samplePosition)); + float sampleLuminance = + dot(sampleColor, float3(0.2126, 0.7152, 0.0722)); + float roughness = clamp(centerAlbedoRoughness.w, 0.0, 1.0); + float luminanceScale = mix(2.5, 7.0, roughness); + float luminanceDifference = + abs(sampleLuminance - centerLuminance) / + max(1.0, max(sampleLuminance, centerLuminance)); + float edgeWeight = exp(-luminanceDifference * luminanceScale); + bool sampleSurface = sampleGuide.w > 0.0; + float normalWeight = centerSurface == sampleSurface ? 1.0 : 0.0; + float depthWeight = normalWeight; + float albedoWeight = normalWeight; + if (centerSurface && sampleSurface) { + float sampleNormalLength = dot(sampleGuide.xyz, sampleGuide.xyz); + if (centerNormalLength > 1e-6 && sampleNormalLength > 1e-6) { + float normalSimilarity = + dot(centerGuide.xyz * rsqrt(centerNormalLength), + sampleGuide.xyz * rsqrt(sampleNormalLength)); + normalWeight *= pow(max(normalSimilarity, 0.0), 24.0); + } else if (i != 0) { + normalWeight = 0.0; + } + float depthScale = max(0.01, abs(centerGuide.w) * 0.01) * + max(float(parameters.stepWidth), 1.0); + depthWeight *= + exp(-abs(sampleGuide.w - centerGuide.w) / depthScale); + albedoWeight *= exp(-length(sampleAlbedoRoughness.xyz - + centerAlbedoRoughness.xyz) * + 8.0); + } + float weight = weights[i] * edgeWeight * normalWeight * depthWeight * + albedoWeight; filtered += sampleColor * weight; totalWeight += weight; } - float3 result = filtered / max(totalWeight, 0.0001); + float3 result = totalWeight > 0.0001 ? filtered / totalWeight : center; float brightness = dot(result, float3(0.2126, 0.7152, 0.0722)); - float contribution = smoothstep(0.5, 1.5, brightness); + constexpr float bloomThreshold = 0.8; + constexpr float bloomKnee = 0.35; + float soft = clamp(brightness - bloomThreshold + bloomKnee, 0.0, + bloomKnee * 2.0); + soft = soft * soft / max(bloomKnee * 4.0, 0.00001); + float contribution = max(brightness - bloomThreshold, soft) / + max(brightness, 0.00001); outputTexture.write(float4(result, 1.0), gid); brightTexture.write(float4(result * contribution, 1.0), gid); } diff --git a/tests/path-tracing/assets/materials/BoxGreen.amat b/tests/path-tracing/assets/materials/BoxGreen.amat new file mode 100644 index 00000000..f393181b --- /dev/null +++ b/tests/path-tracing/assets/materials/BoxGreen.amat @@ -0,0 +1,33 @@ +{ + "material": { + "albedo": [ + 0.1881742626428604, + 0.8374150991439819, + 0.1580987274646759, + 1 + ], + "ao": 1, + "emissiveColor": [ + 0, + 0, + 0, + 1 + ], + "emissiveIntensity": 0, + "ior": 1.45, + "metallic": 0, + "normalMapStrength": 1, + "reflectivity": 0.5, + "roughness": 0.5, + "textureOffset": [ + 0, + 0 + ], + "textureScale": [ + 1, + 1 + ], + "transmittance": 0, + "useNormalMap": true + } +} diff --git a/tests/path-tracing/assets/materials/BoxRed.amat b/tests/path-tracing/assets/materials/BoxRed.amat new file mode 100644 index 00000000..884788f7 --- /dev/null +++ b/tests/path-tracing/assets/materials/BoxRed.amat @@ -0,0 +1,33 @@ +{ + "material": { + "albedo": [ + 0.8374150991439819, + 0.07466239482164383, + 0.024429693818092346, + 1 + ], + "ao": 1, + "emissiveColor": [ + 0, + 0, + 0, + 1 + ], + "emissiveIntensity": 0, + "ior": 1.45, + "metallic": 0, + "normalMapStrength": 1, + "reflectivity": 0.5, + "roughness": 0.5, + "textureOffset": [ + 0, + 0 + ], + "textureScale": [ + 1, + 1 + ], + "transmittance": 0, + "useNormalMap": true + } +} diff --git a/tests/path-tracing/assets/materials/BoxWhite.amat b/tests/path-tracing/assets/materials/BoxWhite.amat new file mode 100644 index 00000000..de1de88c --- /dev/null +++ b/tests/path-tracing/assets/materials/BoxWhite.amat @@ -0,0 +1,33 @@ +{ + "material": { + "albedo": [ + 0.887647807598114, + 0.887647807598114, + 0.887647807598114, + 1 + ], + "ao": 1, + "emissiveColor": [ + 0, + 0, + 0, + 1 + ], + "emissiveIntensity": 0, + "ior": 1.45, + "metallic": 0, + "normalMapStrength": 1, + "reflectivity": 0.5, + "roughness": 0.5, + "textureOffset": [ + 0, + 0 + ], + "textureScale": [ + 1, + 1 + ], + "transmittance": 0, + "useNormalMap": true + } +} diff --git a/tests/path-tracing/assets/materials/Emissive Ball.amat b/tests/path-tracing/assets/materials/Emissive Ball.amat index d1141fc8..33123ae6 100644 --- a/tests/path-tracing/assets/materials/Emissive Ball.amat +++ b/tests/path-tracing/assets/materials/Emissive Ball.amat @@ -13,12 +13,20 @@ 1, 1 ], - "emissiveIntensity": 100, + "emissiveIntensity": 10, "ior": 1.45, "metallic": 0, "normalMapStrength": 1, "reflectivity": 0.5, "roughness": 0.5, + "textureOffset": [ + 0, + 0 + ], + "textureScale": [ + 1, + 1 + ], "transmittance": 0, "useNormalMap": true } diff --git a/tests/path-tracing/assets/materials/Glass.amat b/tests/path-tracing/assets/materials/Glass.amat new file mode 100644 index 00000000..f9fc6380 --- /dev/null +++ b/tests/path-tracing/assets/materials/Glass.amat @@ -0,0 +1,33 @@ +{ + "material": { + "albedo": [ + 0.8, + 0.8, + 0.8, + 1 + ], + "ao": 1, + "emissiveColor": [ + 0, + 0, + 0, + 1 + ], + "emissiveIntensity": 0, + "ior": 1.45, + "metallic": 0, + "normalMapStrength": 1, + "reflectivity": 0.5, + "roughness": 0.5, + "textureOffset": [ + 0, + 0 + ], + "textureScale": [ + 1, + 1 + ], + "transmittance": 1, + "useNormalMap": true + } +} diff --git a/tests/path-tracing/main.ascene b/tests/path-tracing/main.ascene index 8f83af34..e57df0dd 100644 --- a/tests/path-tracing/main.ascene +++ b/tests/path-tracing/main.ascene @@ -14,82 +14,85 @@ "orthoSize": 5.0, "orthographic": false, "position": [ - -2.715547800064087, - 1.4940226078033447, - 4.527331352233887 + 0.4004000127315521, + 1.9144999980926514, + -4.536300182342529 ], "target": [ - -0.3924787640571594, - -0.1557551771402359, - 1.7118041515350342 + 0.0, + 0.5273000001907349, + 1.0 ] }, "environment": { "atmosphere": { - "enabled": true, + "enabled": false, "globalLight": { "castsShadows": true, "enabled": false, "shadowResolution": 4096 } }, - "atmosphereSky": true, + "atmosphereSky": false, "automaticAmbient": false }, "id": "main_scene", - "lights": [ + "lights": [], + "name": "Main Scene", + "objects": [ { - "color": [ - 1.0, - 1.0, - 1.0, - 1.0 - ], - "id": "ambientLight_0", - "intensity": 0.05, - "name": "ambientLight_0", + "components": [], + "id": 157052340, + "material": "assets/materials/BoxWhite.amat", + "name": "Floor", "position": [ + -0.31472718715667725, + -0.15528558194637299, + 0.15461790561676025 + ], + "rotation": [ 0.0, 0.0, 0.0 ], - "type": "ambientLight" - } - ], - "name": "Main Scene", - "objects": [ + "scale": [ + 4.174081802368164, + 0.05000000074505806, + 4.174081802368164 + ], + "solid_type": "cube", + "type": "solid" + }, { "components": [], - "material": "", - "name": "Cube", + "material": "assets/materials/BoxRed.amat", + "name": "Floor 2", "position": [ - 0.0, - 0.0, - 0.0 + -0.3160567581653595, + 1.5384644269943237, + 2.2118043899536133 ], "rotation": [ - 0.0, + -89.787841796875, 0.0, 0.0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 4.174081802368164, + 0.05000000074505806, + 7.269232749938965 ], "solid_type": "cube", "type": "solid" }, { "components": [], - "id": 262366761, - "material": "assets/materials/New Material.amat", - "name": "Cube 2", - "parent": "Cube", + "material": "assets/materials/BoxWhite.amat", + "name": "Floor 3", "position": [ - 1.25, - 0.0, - 0.0 + -0.2613140940666199, + 5.090397834777832, + 0.2858502268791199 ], "rotation": [ 0.0, @@ -97,22 +100,22 @@ 0.0 ], "scale": [ - 1.0, - 1.0, - 1.0 + 4.174081802368164, + 0.05000000074505806, + 4.174081802368164 ], "solid_type": "cube", "type": "solid" }, { "components": [], - "id": 1966546454, - "material": "assets/materials/Emissive Ball.amat", + "id": 6739391, + "material": "assets/materials/Glass.amat", "name": "Sphere", "position": [ - 0.6134507656097412, - 0.17499999701976776, - 1.8578729629516602 + -0.22421985864639282, + 1.5384645462036133, + 0.5111536979675293 ], "rotation": [ 0.0, @@ -126,6 +129,29 @@ ], "solid_type": "sphere", "type": "solid" + }, + { + "components": [], + "id": 2097299432, + "material": "assets/materials/Emissive Ball.amat", + "name": "Plane", + "position": [ + 0.0, + 2.497310161590576, + -5.3531646728515625 + ], + "rotation": [ + 20.657730102539063, + 0.0, + 0.0 + ], + "scale": [ + 3.9725027084350586, + 2.9564456939697266, + 3.064767360687256 + ], + "solid_type": "plane", + "type": "solid" } ], "property_syncs": [], @@ -137,5 +163,6 @@ "render": true, "type": "scene" } - ] + ], + "ui": [] }