diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index f9cbf4e3..dcaa3ca4 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -141,7 +141,7 @@ jobs: CC: gcc CXX: g++ run: | - sudo apt-get install ninja-build libspdlog-dev libglm-dev libgl1-mesa-dev libegl1-mesa-dev libtiff-dev libzstd-dev nasm + sudo apt-get install ninja-build libspdlog-dev libglm-dev libgl1-mesa-dev libegl1-mesa-dev mesa-vulkan-drivers libtiff-dev libzstd-dev nasm - name: linux build and test if: matrix.os == 'ubuntu-latest' env: diff --git a/.gitignore b/.gitignore index 769963ef..88e325d7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ webclient/node_modules/ webclient/es/ build/ +out/ renderlib/version.h docs/_build/ GeneratedFiles/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 70144c9e..29b69ce3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,6 +119,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # THE COMMON CORE LIBRARIES # ##################### add_subdirectory(renderlib) +add_subdirectory(renderlib_wgpu) set(INSTALLDIR "${CMAKE_PROJECT_NAME}-install") diff --git a/CMakeSettings.json b/CMakeSettings.json new file mode 100644 index 00000000..6d729c8e --- /dev/null +++ b/CMakeSettings.json @@ -0,0 +1,16 @@ +{ + "configurations": [ + { + "name": "x64-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "inheritEnvironments": [ "msvc_x64_x64" ], + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "-DVCPKG_TARGET_TRIPLET=x64-windows", + "buildCommandArgs": "", + "ctestCommandArgs": "", + "cmakeToolchain": "C:\\Users\\dmt\\source\\repos\\vcpkg\\scripts\\buildsystems\\vcpkg.cmake" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index ecafc08e..b7b18e02 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ Make sure you are in an environment where vsvarsall has been run, e.g. a "VS2022 A convenient way to install Perl, NASM, and GNU Patch is with chocolatey. +wgpu-native requires: +Rust +LLVM and clang + ``` choco install strawberryperl nasm patch ``` @@ -88,6 +92,8 @@ sudo make install ### For LINUX: +Make sure you have Rust 1.59 or greater installed for the wgpu-native dependency. + Install Qt 6.8.3 in your directory of choice and tell the build where to find it. In your favorite Python virtual environment: @@ -100,10 +106,13 @@ aqt install-qt --outputdir ~/Qt linux desktop 6.8.3 -m qtwebsockets qtimageforma export Qt6_DIR=~/Qt/6.8.3/gcc_64 +sudo apt install libclang-dev # for rust / wgpu-native sudo apt install libtiff-dev sudo apt install libglm-dev sudo apt install libgl1-mesa-dev sudo apt install libegl1-mesa-dev +sudo apt install libxkbcommon-dev +sudo apt install mesa-vulkan-drivers sudo apt install libspdlog-dev sudo apt install nasm sudo apt install libxcb-xkb-dev diff --git a/agave_app/CMakeLists.txt b/agave_app/CMakeLists.txt index 1a160faf..d34c1564 100644 --- a/agave_app/CMakeLists.txt +++ b/agave_app/CMakeLists.txt @@ -75,6 +75,8 @@ target_sources(agaveapp PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/TimelineDockWidget.h" "${CMAKE_CURRENT_SOURCE_DIR}/ViewerState.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/ViewerState.h" + "${CMAKE_CURRENT_SOURCE_DIR}/wgpuView3D.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/wgpuView3D.h" "${CMAKE_CURRENT_SOURCE_DIR}/ViewToolbar.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/ViewToolbar.h" ) @@ -98,6 +100,7 @@ endif(MSVC) target_link_libraries(agaveapp PRIVATE renderlib + renderlib_wgpu Qt::Widgets Qt::Core Qt::Gui Qt::Network Qt::OpenGL Qt::OpenGLWidgets Qt::WebSockets Qt::Xml Qt::Svg ) diff --git a/agave_app/StatisticsWidget.cpp b/agave_app/StatisticsWidget.cpp index 815a0aef..3e8f7fb8 100644 --- a/agave_app/StatisticsWidget.cpp +++ b/agave_app/StatisticsWidget.cpp @@ -50,7 +50,9 @@ QStatisticsWidget::set(std::shared_ptr status) mStatusObject->removeObserver(&mStatusObserver); } mStatusObject = status; - mStatusObject->addObserver(&mStatusObserver); + if (mStatusObject) { + mStatusObject->addObserver(&mStatusObserver); + } } QSize diff --git a/agave_app/agaveGui.cpp b/agave_app/agaveGui.cpp index f1ea3aa3..5623a83e 100644 --- a/agave_app/agaveGui.cpp +++ b/agave_app/agaveGui.cpp @@ -111,7 +111,7 @@ agaveGui::agaveGui(QWidget* parent) connect(m_tabs, SIGNAL(currentChanged(int)), this, SLOT(tabChanged(int))); // add the single gl view as a tab - m_glView = new GLView3D(&m_qcamera, &m_qrendersettings, &m_renderSettings, this); + m_glView = new WgpuCanvas(&m_qcamera, &m_qrendersettings, &m_renderSettings, this); QObject::connect(m_glView, SIGNAL(ChangedRenderer()), this, SLOT(OnUpdateRenderer())); m_glView->setObjectName("glcontainer"); @@ -725,7 +725,9 @@ agaveGui::onImageLoaded(std::shared_ptr image, std::shared_ptr s = m_glView->getStatus(); // set up the m_statisticsDockWidget as a CStatus IStatusObserver m_statisticsDockWidget->setStatus(s); - s->onNewImage(filename, &m_appScene); + if (s) { + s->onNewImage(filename, &m_appScene); + } m_currentFilePath = loadSpec.filepath; agaveGui::prependToRecentFiles(QString::fromStdString(loadSpec.filepath)); @@ -853,7 +855,7 @@ agaveGui::openMesh(const QString& file) } void -agaveGui::viewFocusChanged(GLView3D* newGlView) +agaveGui::viewFocusChanged(WgpuCanvas* newGlView) { if (m_glView == newGlView) return; @@ -870,14 +872,14 @@ agaveGui::viewFocusChanged(GLView3D* newGlView) void agaveGui::tabChanged(int index) { - GLView3D* current = 0; + WgpuCanvas* current = 0; if (index >= 0) { QWidget* w = m_tabs->currentWidget(); if (w) { QLayout* layout = w->layout(); // ASSUMES THAT THE GLVIEW IS THE SECOND WIDGET IN THE LAYOUT // TODO could use a QWidget wrapper class to get the glview out - current = static_cast(layout->itemAt(1)->widget()); + current = static_cast(layout->itemAt(1)->widget()); } } viewFocusChanged(current); diff --git a/agave_app/agaveGui.h b/agave_app/agaveGui.h index b6a93ea2..1c5c2d1f 100644 --- a/agave_app/agaveGui.h +++ b/agave_app/agaveGui.h @@ -3,10 +3,10 @@ #include "ui_agaveGui.h" #include "Camera.h" -#include "GLView3D.h" #include "QRenderSettings.h" #include "ViewerState.h" #include "renderDialog.h" +#include "wgpuView3D.h" #include "renderlib/AppScene.h" #include "renderlib/RenderSettings.h" @@ -66,7 +66,7 @@ private slots: void view_frame(); void view_toggleProjection(); void showAxisHelper(); - void viewFocusChanged(GLView3D* glView); + void viewFocusChanged(WgpuCanvas* glView); void tabChanged(int index); void openMeshDialog(); void openMesh(const QString& file); @@ -150,7 +150,7 @@ private slots: QStatisticsDockWidget* m_statisticsDockWidget; QTabWidget* m_tabs; - GLView3D* m_glView; + WgpuCanvas* m_glView; ViewToolbar* m_viewToolbar; QWidget* m_viewWithToolbar; diff --git a/agave_app/loadDialog.h b/agave_app/loadDialog.h index 4f2eb336..1c80f811 100644 --- a/agave_app/loadDialog.h +++ b/agave_app/loadDialog.h @@ -60,7 +60,6 @@ private slots: // select any set of channels QListWidget* mChannels; Section* mChannelsSection; - QTreeWidget* mMetadataTree; QLabel* mVolumeLabel; QLabel* mMemoryEstimateLabel; // select region of interest in zyx diff --git a/agave_app/main.cpp b/agave_app/main.cpp index 581cdea3..c81a60f7 100644 --- a/agave_app/main.cpp +++ b/agave_app/main.cpp @@ -5,6 +5,7 @@ #include "renderlib/io/FileReader.h" #include "renderlib/renderlib.h" #include "renderlib/version.h" +#include "renderlib_wgpu/renderlib_wgpu.h" #include "streamserver.h" #include @@ -145,16 +146,24 @@ main(int argc, char* argv[]) Logging::Init(); QApplication::setAttribute(Qt::AA_UseDesktopOpenGL); + QApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + // TODO remove this for qt 6.6 and up? QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setStyle("fusion"); + // note that this is called before renderlib::initialize + QSurfaceFormat format = renderlib::getQSurfaceFormat(); + QSurfaceFormat::setDefaultFormat(format); + AgaveApplication a(argc, argv); a.setOrganizationName("Allen Institute for Cell Science"); a.setOrganizationDomain("allencell.org"); a.setApplicationName("AGAVE"); a.setApplicationVersion(AICS_VERSION_STRING); - LOG_INFO << a.organizationName().toStdString() << " " << a.applicationName().toStdString() << " " - << a.applicationVersion().toStdString(); + std::string orgname = a.organizationName().toStdString(); + std::string appname = a.applicationName().toStdString(); + std::string appversion = a.applicationVersion().toStdString(); + LOG_INFO << orgname << " " << appname << " " << appversion; QCommandLineParser parser; parser.setApplicationDescription("Advanced GPU Accelerated Volume Explorer"); @@ -211,6 +220,10 @@ main(int argc, char* argv[]) renderlib::cleanup(); return 0; } + if (!renderlib_wgpu::initialize(isServer, listDevices, selectedGpu)) { + renderlib_wgpu::cleanup(); + return 0; + } int result = 0; diff --git a/agave_app/wgpuView3D.cpp b/agave_app/wgpuView3D.cpp new file mode 100644 index 00000000..b547d9fe --- /dev/null +++ b/agave_app/wgpuView3D.cpp @@ -0,0 +1,837 @@ +#include "wgpuView3D.h" + +#include "Camera.h" +#include "QRenderSettings.h" +#include "ViewerState.h" + +#include "renderlib/AppScene.h" +#include "renderlib/ImageXYZC.h" +#include "renderlib/Logging.h" +#include "renderlib/MoveTool.h" +#include "renderlib/RenderSettings.h" +#include "renderlib/RotateTool.h" +#include "renderlib/graphics/IRenderWindow.h" +#include "renderlib/graphics/RenderGL.h" +#include "renderlib/graphics/RenderGLPT.h" +#include "renderlib/graphics/gl/Image3D.h" +#include "renderlib/graphics/gl/Util.h" +#include "renderlib_wgpu/getsurface_wgpu.h" +#include "renderlib_wgpu/wgpu_util.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// Only Microsoft issue warnings about correct behaviour... +#ifdef _MSVC_VER +#pragma warning(disable : 4351) +#endif + +WgpuView3D::WgpuView3D(QCamera* cam, QRenderSettings* qrs, RenderSettings* rs, QWidget* parent) + : QWidget(parent) + , m_lastPos(0, 0) + , m_initialized(false) + , m_fakeHidden(false) + , m_qrendersettings(qrs) + , m_windowContext(nullptr) +{ + m_viewerWindow = new ViewerWindow(rs); + m_viewerWindow->gesture.input.setDoubleClickTime((double)QApplication::doubleClickInterval() / 1000.0); + + setAutoFillBackground(false); + setAttribute(Qt::WA_PaintOnScreen); + setAttribute(Qt::WA_DeleteOnClose); + setFocusPolicy(Qt::StrongFocus); + setMouseTracking(true); + winId(); // create window handle + + m_qrendersettings->setRenderSettings(*rs); + + // IMPORTANT this is where the QT gui container classes send their values down into the + // CScene object. GUI updates --> QT Object Changed() --> cam->Changed() --> + // WgpuView3D->OnUpdateCamera + QObject::connect(cam, SIGNAL(Changed()), this, SLOT(OnUpdateCamera())); + QObject::connect(qrs, SIGNAL(Changed()), this, SLOT(OnUpdateQRenderSettings())); + QObject::connect(qrs, SIGNAL(ChangedRenderer(int)), this, SLOT(OnUpdateRenderer(int))); + + // run a timer to update the clock + // TODO is this different than using this->startTimer and QTimerEvent? + m_etimer = new QTimer(parent); + m_etimer->setTimerType(Qt::PreciseTimer); + connect(m_etimer, &QTimer::timeout, this, [this] { + // assume that in between QTimer events, true processEvents is called by Qt itself + // QCoreApplication::processEvents(); + if (isEnabled()) { + update(); + } + }); + m_etimer->start(); +} + +void +WgpuView3D::initCameraFromImage(Scene* scene) +{ + // Tell the camera about the volume's bounding box + m_viewerWindow->m_CCamera.m_SceneBoundingBox.m_MinP = scene->m_boundingBox.GetMinP(); + m_viewerWindow->m_CCamera.m_SceneBoundingBox.m_MaxP = scene->m_boundingBox.GetMaxP(); + // reposition to face image + m_viewerWindow->m_CCamera.SetViewMode(ViewModeFront); + + RenderSettings* rs = m_viewerWindow->m_renderSettings; + rs->m_DirtyFlags.SetFlag(CameraDirty); +} + +void +WgpuView3D::retargetCameraForNewVolume(Scene* scene) +{ + // ASSUMPTION camera's sceneboundingbox has not yet been updated! + + glm::vec3 oldctr = m_viewerWindow->m_CCamera.m_SceneBoundingBox.GetCenter(); + // Tell the camera about the volume's bounding box + m_viewerWindow->m_CCamera.m_SceneBoundingBox.m_MinP = scene->m_boundingBox.GetMinP(); + m_viewerWindow->m_CCamera.m_SceneBoundingBox.m_MaxP = scene->m_boundingBox.GetMaxP(); + // reposition target to center of image + glm::vec3 ctr = m_viewerWindow->m_CCamera.m_SceneBoundingBox.GetCenter(); + // offset target by delta of prev bounds and new bounds. + m_viewerWindow->m_CCamera.m_Target += (ctr - oldctr); + + RenderSettings* rs = m_viewerWindow->m_renderSettings; + rs->m_DirtyFlags.SetFlag(CameraDirty); +} + +void +WgpuView3D::toggleCameraProjection() +{ + ProjectionMode p = m_viewerWindow->m_CCamera.m_Projection; + m_viewerWindow->m_CCamera.SetProjectionMode((p == PERSPECTIVE) ? ORTHOGRAPHIC : PERSPECTIVE); + + RenderSettings* rs = m_viewerWindow->m_renderSettings; + rs->m_DirtyFlags.SetFlag(CameraDirty); +} + +void +WgpuView3D::onNewImage(Scene* scene) +{ + m_viewerWindow->m_renderer->setScene(scene); + // costly teardown and rebuild. + this->OnUpdateRenderer(m_viewerWindow->m_rendererType); + // would be better to preserve renderer and just change the scene data to include the new image. + // how tightly coupled is renderer and scene???? +} + +WgpuView3D::~WgpuView3D() +{ + delete m_windowContext; + wgpuQueueRelease(m_queue); + wgpuDeviceRelease(m_device); +} + +QSize +WgpuView3D::minimumSizeHint() const +{ + return QSize(800, 600); +} + +QSize +WgpuView3D::sizeHint() const +{ + return QSize(800, 600); +} + +void +WgpuView3D::initializeGL() +{ + if (m_initialized) { + return; + } + float dpr = devicePixelRatioF(); + + LOG_INFO << "calling get_surface_from_canvas"; + + WGPUSurface surface = renderlib_wgpu::getSurfaceFromCanvas((void*)winId()); + + LOG_INFO << "calling getAdapter"; + WGPUAdapter adapter = renderlib_wgpu::getAdapter(surface); + LOG_INFO << "calling requestDevice"; + + m_device = renderlib_wgpu::requestDevice(adapter); + m_queue = wgpuDeviceGetQueue(m_device); + + LOG_INFO << "set up swap chain"; + m_windowContext = renderlib_wgpu::setupWindowContext(surface, m_device, width() * dpr, height() * dpr); + + // Release the adapter only after it has been fully utilized + wgpuAdapterRelease(adapter); + + // The WgpuView3D owns one CScene +#if 0 + WGPUPipelineLayoutDescriptor pipelineLayoutDescriptor = {}; + pipelineLayoutDescriptor.bindGroupLayoutCount = 0; + pipelineLayoutDescriptor.bindGroupLayouts = NULL; + + WGPUPipelineLayout pipelineLayout = wgpuDeviceCreatePipelineLayout(m_device, &pipelineLayoutDescriptor); + WGPUBlendComponent blendComponentColor = {}; + blendComponentColor.operation = WGPUBlendOperation_Add; + blendComponentColor.srcFactor = WGPUBlendFactor_One; + blendComponentColor.dstFactor = WGPUBlendFactor_Zero; + WGPUBlendComponent blendComponentAlpha = {}; + blendComponentAlpha.operation = WGPUBlendOperation_Add; + blendComponentAlpha.srcFactor = WGPUBlendFactor_One; + blendComponentAlpha.dstFactor = WGPUBlendFactor_Zero; + + WGPUBlendState blendState = {}; + blendState.color = blendComponentColor; + blendState.alpha = blendComponentAlpha; + + WGPUColorTargetState colorTargetState = {}; + colorTargetState.format = m_surfaceFormat; + colorTargetState.blend = &blendState; + colorTargetState.writeMask = WGPUColorWriteMask_All; + WGPUFragmentState fragmentState = {}; + fragmentState.module = nullptr; // shader, + fragmentState.entryPoint = "fs_main"; + fragmentState.targetCount = 1; + fragmentState.targets = &colorTargetState; + + WGPUVertexState vertexState = {}; + vertexState.module = nullptr; // shader, + vertexState.entryPoint = nullptr; + vertexState.bufferCount = 0; + vertexState.buffers = NULL; + + WGPUPrimitiveState primitiveState = {}; + primitiveState.topology = WGPUPrimitiveTopology_TriangleList; + primitiveState.stripIndexFormat = WGPUIndexFormat_Undefined; + primitiveState.frontFace = WGPUFrontFace_CCW; + primitiveState.cullMode = WGPUCullMode_None; + + WGPUMultisampleState multisampleState = {}; + multisampleState.count = 1; + multisampleState.mask = 0xFFFFFFFF; + multisampleState.alphaToCoverageEnabled = false; + + WGPURenderPipelineDescriptor renderPipelineDescriptor = {}; + renderPipelineDescriptor.label = "Render pipeline", renderPipelineDescriptor.layout = pipelineLayout, + renderPipelineDescriptor.vertex = vertexState; + renderPipelineDescriptor.primitive = primitiveState; + renderPipelineDescriptor.depthStencil = NULL, renderPipelineDescriptor.multisample = multisampleState; + renderPipelineDescriptor.fragment = nullptr; //&fragmentState, + + // m_pipeline = wgpuDeviceCreateRenderPipeline(m_device, &renderPipelineDescriptor); +#endif + m_initialized = true; + + QSize newsize = size(); + m_viewerWindow->m_renderer->initialize(newsize.width() * dpr, newsize.height() * dpr); + + // Start timers + m_etimer->start(); + + // // Size viewport + // resizeGL(newsize.width(), newsize.height()); +} + +void +WgpuView3D::paintEvent(QPaintEvent* e) +{ + if (!m_initialized) { + return; + } + if (updatesEnabled()) { + render(); + // the above render call should include this viewerwindow redraw + // m_viewerWindow->redraw(); + } +} + +void +WgpuView3D::resizeGL(int w, int h) +{ + QResizeEvent e(QSize(w, h), QSize(w, h)); + resizeEvent(&e); +} + +void +WgpuView3D::resizeEvent(QResizeEvent* event) +{ + if (event->size().isEmpty()) { + m_fakeHidden = true; + return; + } + m_fakeHidden = false; + initializeGL(); + if (!m_initialized) { + return; + } + + float dpr = devicePixelRatioF(); + int w = event->size().width(); + int h = event->size().height(); + + // (if w or h actually changed...) + m_windowContext->resize(w * dpr, h * dpr); + + m_viewerWindow->setSize(w * dpr, h * dpr); + m_viewerWindow->forEachTool( + [this](ManipulationTool* tool) { tool->setSize(ManipulationTool::s_manipulatorSize * devicePixelRatioF()); }); + + // update(); + // invokeUserPaint(); +} + +static Gesture::Input::ButtonId +getButton(QMouseEvent* event) +{ + Gesture::Input::ButtonId btn; + switch (event->button()) { + case Qt::LeftButton: + btn = Gesture::Input::ButtonId::kButtonLeft; + break; + case Qt::RightButton: + btn = Gesture::Input::ButtonId::kButtonRight; + break; + case Qt::MiddleButton: + btn = Gesture::Input::ButtonId::kButtonMiddle; + break; + default: + btn = Gesture::Input::ButtonId::kButtonLeft; + break; + }; + return btn; +} +static int +getGestureMods(QMouseEvent* event) +{ + int mods = 0; + if (event->modifiers() & Qt::ShiftModifier) { + mods |= Gesture::Input::Mods::kShift; + } + if (event->modifiers() & Qt::ControlModifier) { + mods |= Gesture::Input::Mods::kCtrl; + } + if (event->modifiers() & Qt::AltModifier) { + mods |= Gesture::Input::Mods::kAlt; + } + if (event->modifiers() & Qt::MetaModifier) { + mods |= Gesture::Input::Mods::kSuper; + } + return mods; +} + +void +WgpuView3D::render() +{ + if (m_fakeHidden || !m_initialized) { + return; + } + + QWindow* win = windowHandle(); + if (!win || !win->isExposed()) { + return; + } + WGPUSurfaceTexture nextTexture; + + wgpuSurfaceGetCurrentTexture(m_windowContext->m_surface, &nextTexture); + switch (nextTexture.status) { + case WGPUSurfaceGetCurrentTextureStatus_Success: + // All good, could check for `surface_texture.suboptimal` here. + break; + case WGPUSurfaceGetCurrentTextureStatus_Timeout: + case WGPUSurfaceGetCurrentTextureStatus_Outdated: + case WGPUSurfaceGetCurrentTextureStatus_Lost: { + // Skip this frame, and re-configure surface. + if (nextTexture.texture) { + wgpuTextureRelease(nextTexture.texture); + } + if (width() != 0 && height() != 0) { + const float dpr = devicePixelRatioF(); + m_windowContext->resize(width() * dpr, height() * dpr); + } + return; + } + case WGPUSurfaceGetCurrentTextureStatus_OutOfMemory: + case WGPUSurfaceGetCurrentTextureStatus_DeviceLost: + case WGPUSurfaceGetCurrentTextureStatus_Force32: + // Fatal error + LOG_ERROR << "get_current_texture status=" << nextTexture.status; + abort(); + } + assert(nextTexture.texture); + + WGPUTextureView frame = wgpuTextureCreateView(nextTexture.texture, NULL); + assert(frame); + + renderWindowContents(frame); + + m_windowContext->present(); + + // LOG_DEBUG << "surface presented"; + // TODO loop to poll a few times? + // while (!queueDone) { + wgpuDevicePoll(m_device, false, nullptr); + //} + + wgpuTextureViewRelease(frame); + wgpuTextureRelease(nextTexture.texture); +} + +void +WgpuView3D::renderWindowContents(WGPUTextureView nextTexture) +{ + if (!isEnabled()) { + return; + } + // LOG_DEBUG << "render: pre-create command encoder"; + + WGPUCommandEncoderDescriptor commandEncoderDescriptor = {}; + commandEncoderDescriptor.label = "Command Encoder"; + WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(m_device, &commandEncoderDescriptor); + Scene* sc = (m_viewerWindow && m_viewerWindow->m_renderer) ? m_viewerWindow->m_renderer->scene() : nullptr; + WGPUColor clearColor = {}; + clearColor.r = sc ? sc->m_material.m_backgroundColor[0] : 0.0f; + clearColor.g = sc ? sc->m_material.m_backgroundColor[1] : 0.0f; + clearColor.b = sc ? sc->m_material.m_backgroundColor[2] : 0.0f; + clearColor.a = 1.0f; + WGPURenderPassColorAttachment renderPassColorAttachment = {}; + renderPassColorAttachment.view = nextTexture; + renderPassColorAttachment.resolveTarget = nullptr; + renderPassColorAttachment.loadOp = WGPULoadOp_Clear; + renderPassColorAttachment.storeOp = WGPUStoreOp_Store; + renderPassColorAttachment.clearValue = clearColor; + WGPURenderPassDescriptor renderPassDescriptor = {}; + renderPassDescriptor.nextInChain = NULL; + renderPassDescriptor.label = "Render Pass"; + renderPassDescriptor.colorAttachmentCount = 1; + renderPassDescriptor.colorAttachments = &renderPassColorAttachment; + renderPassDescriptor.depthStencilAttachment = NULL; + renderPassDescriptor.occlusionQuerySet = 0; + renderPassDescriptor.timestampWrites = NULL; + WGPURenderPassEncoder renderPass = wgpuCommandEncoderBeginRenderPass(encoder, &renderPassDescriptor); + wgpuRenderPassEncoderSetViewport(renderPass, 0, 0, width(), height(), 0, 1); + // wgpuRenderPassEncoderSetPipeline(renderPass, pipeline); + // wgpuRenderPassEncoderDraw(renderPass, 3, 1, 0, 0); + wgpuRenderPassEncoderEnd(renderPass); + wgpuRenderPassEncoderRelease(renderPass); + + static bool queueDone = false; + queueDone = false; + + // typedef void (*WGPUQueueOnSubmittedWorkDoneCallback)(WGPUQueueWorkDoneStatus status, WGPU_NULLABLE void * + // userdata) WGPU_FUNCTION_ATTRIBUTE; + + auto onQueueWorkDone = [](WGPUQueueWorkDoneStatus status, void* pUserData) { + // LOG_DEBUG << "Queued work finished with status: " << status; + bool* squeueDone = (bool*)pUserData; + *squeueDone = true; + }; + wgpuQueueOnSubmittedWorkDone(m_queue, onQueueWorkDone, &queueDone /* pUserData */); + + WGPUCommandBufferDescriptor commandBufferDescriptor = {}; + commandBufferDescriptor.label = NULL; + WGPUCommandBuffer cmdBuffer = wgpuCommandEncoderFinish(encoder, &commandBufferDescriptor); + wgpuCommandEncoderRelease(encoder); // release encoder after it's finished + wgpuQueueSubmit(m_queue, 1, &cmdBuffer); + // release command buffer once submitted + wgpuCommandBufferRelease(cmdBuffer); + + // wgpuCommandEncoderRelease(encoder); + + // TODO ENABLE THIS!!! + // m_viewerWindow->redraw(); +} + +void +WgpuView3D::mousePressEvent(QMouseEvent* event) +{ + if (!isEnabled()) { + return; + } + + double time = Clock::now(); + const float dpr = devicePixelRatioF(); + m_viewerWindow->gesture.input.setButtonEvent(getButton(event), + Gesture::Input::Action::kPress, + getGestureMods(event), + glm::vec2(event->x() * dpr, event->y() * dpr), + time); +} + +void +WgpuView3D::mouseReleaseEvent(QMouseEvent* event) +{ + if (!isEnabled()) { + return; + } + + double time = Clock::now(); + const float dpr = devicePixelRatioF(); + m_viewerWindow->gesture.input.setButtonEvent(getButton(event), + Gesture::Input::Action::kRelease, + getGestureMods(event), + glm::vec2(event->x() * dpr, event->y() * dpr), + time); +} + +// No switch default to avoid -Wunreachable-code errors. +// However, this then makes -Wswitch-default complain. Disable +// temporarily. +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch-default" +#endif + +void +WgpuView3D::mouseMoveEvent(QMouseEvent* event) +{ + if (!isEnabled()) { + return; + } + const float dpr = devicePixelRatioF(); + + m_viewerWindow->gesture.input.setPointerPosition(glm::vec2(event->x() * dpr, event->y() * dpr)); +} + +void +WgpuView3D::wheelEvent(QWheelEvent* event) +{ + if (!isEnabled()) { + return; + } + const float dpr = devicePixelRatioF(); + + // tell gesture there was a wheel event + // m_viewerWindow->gesture.input.setPointerPosition(glm::vec2(event->x() * dpr, event->y() * dpr)); +} + +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +void +WgpuView3D::FitToScene(float transitionDurationSeconds) +{ + Scene* sc = m_viewerWindow->m_renderer->scene(); + + glm::vec3 newPosition, newTarget; + m_viewerWindow->m_CCamera.ComputeFitToBounds(sc->m_boundingBox, newPosition, newTarget); + CameraAnimation anim = {}; + anim.duration = transitionDurationSeconds; + anim.mod.position = newPosition - m_viewerWindow->m_CCamera.m_From; + anim.mod.target = newTarget - m_viewerWindow->m_CCamera.m_Target; + m_viewerWindow->m_cameraAnim.push_back(anim); +} + +void +WgpuView3D::OnSelectionChanged(SceneObject* so) +{ + // null ptr is valid here to deselect + m_viewerWindow->select(so); + + // Toggling the manipulator mode like this + // has the effect of re-creating the manipulator tool, + // which will effectively call origins.update to get the new + // selection into the tool + setManipulatorMode(MANIPULATOR_MODE::NONE); + setManipulatorMode(m_manipulatorMode); +} + +void +WgpuView3D::setManipulatorMode(MANIPULATOR_MODE mode) +{ + // setting same mode does nothing + if (m_manipulatorMode == mode) { + return; + } + m_manipulatorMode = mode; + switch (mode) { + case MANIPULATOR_MODE::NONE: + m_viewerWindow->setTool(nullptr); + break; + case MANIPULATOR_MODE::ROT: + m_viewerWindow->setTool(new RotateTool(m_viewerWindow->m_toolsUseLocalSpace, + ManipulationTool::s_manipulatorSize * devicePixelRatioF())); + m_viewerWindow->forEachTool( + [this](ManipulationTool* tool) { tool->setUseLocalSpace(m_viewerWindow->m_toolsUseLocalSpace); }); + break; + case MANIPULATOR_MODE::TRANS: + m_viewerWindow->setTool( + new MoveTool(m_viewerWindow->m_toolsUseLocalSpace, ManipulationTool::s_manipulatorSize * devicePixelRatioF())); + m_viewerWindow->forEachTool( + [this](ManipulationTool* tool) { tool->setUseLocalSpace(m_viewerWindow->m_toolsUseLocalSpace); }); + break; + default: + break; + } +} + +void +WgpuView3D::showRotateControls(bool show) +{ + if (show) { + setManipulatorMode(MANIPULATOR_MODE::ROT); + } else { + setManipulatorMode(MANIPULATOR_MODE::NONE); + } +} + +void +WgpuView3D::showTranslateControls(bool show) +{ + if (show) { + setManipulatorMode(MANIPULATOR_MODE::TRANS); + } else { + setManipulatorMode(MANIPULATOR_MODE::NONE); + } +} + +void +WgpuView3D::toggleRotateControls() +{ + // if nothing selected, then switch off + if (!m_viewerWindow->sceneView.getSelectedObject()) { + setManipulatorMode(MANIPULATOR_MODE::NONE); + } + // toggle rotate tool + else if (m_manipulatorMode == MANIPULATOR_MODE::ROT) { + setManipulatorMode(MANIPULATOR_MODE::NONE); + } else { + setManipulatorMode(MANIPULATOR_MODE::ROT); + } +} + +// TODO currently this function is not wired up to any gui at all. +// This is because translation of area light source still needs work. +// (Currently rotation is sufficient.) +void +WgpuView3D::toggleTranslateControls() +{ + // if nothing selected, then switch off + if (!m_viewerWindow->sceneView.getSelectedObject()) { + setManipulatorMode(MANIPULATOR_MODE::NONE); + } + // toggle translate tool + else if (m_manipulatorMode == MANIPULATOR_MODE::TRANS) { + setManipulatorMode(MANIPULATOR_MODE::NONE); + } else { + setManipulatorMode(MANIPULATOR_MODE::TRANS); + } +} + +void +WgpuView3D::keyPressEvent(QKeyEvent* event) +{ + if (event->key() == Qt::Key_A) { + FitToScene(0.5f); + } else if (event->key() == Qt::Key_L) { + // toggle local/global coordinates for transforms + m_viewerWindow->m_toolsUseLocalSpace = !m_viewerWindow->m_toolsUseLocalSpace; + m_viewerWindow->forEachTool( + [this](ManipulationTool* tool) { tool->setUseLocalSpace(m_viewerWindow->m_toolsUseLocalSpace); }); + } else { + QWidget::keyPressEvent(event); + } +} + +void +WgpuView3D::OnUpdateCamera() +{ + // QMutexLocker Locker(&gSceneMutex); + RenderSettings* rs = m_viewerWindow->m_renderSettings; + m_viewerWindow->m_CCamera.m_Film.m_Exposure = 1.0f - m_qcamera->GetFilm().GetExposure(); + m_viewerWindow->m_CCamera.m_Film.m_ExposureIterations = m_qcamera->GetFilm().GetExposureIterations(); + + if (m_qcamera->GetFilm().IsDirty()) { + const int FilmWidth = m_qcamera->GetFilm().GetWidth(); + const int FilmHeight = m_qcamera->GetFilm().GetHeight(); + + m_viewerWindow->m_CCamera.m_Film.m_Resolution.SetResX(FilmWidth); + m_viewerWindow->m_CCamera.m_Film.m_Resolution.SetResY(FilmHeight); + m_viewerWindow->m_CCamera.Update(); + m_qcamera->GetFilm().UnDirty(); + + rs->m_DirtyFlags.SetFlag(FilmResolutionDirty); + } + + m_viewerWindow->m_CCamera.Update(); + + // Aperture + m_viewerWindow->m_CCamera.m_Aperture.m_Size = m_qcamera->GetAperture().GetSize(); + + // Projection + m_viewerWindow->m_CCamera.m_FovV = m_qcamera->GetProjection().GetFieldOfView(); + + // Focus + m_viewerWindow->m_CCamera.m_Focus.m_Type = (Focus::EType)m_qcamera->GetFocus().GetType(); + m_viewerWindow->m_CCamera.m_Focus.m_FocalDistance = m_qcamera->GetFocus().GetFocalDistance(); + + rs->m_DenoiseParams.m_Enabled = m_qcamera->GetFilm().GetNoiseReduction(); + + rs->m_DirtyFlags.SetFlag(CameraDirty); +} + +void +WgpuView3D::OnUpdateQRenderSettings(void) +{ + // QMutexLocker Locker(&gSceneMutex); + RenderSettings* rs = m_viewerWindow->m_renderSettings; + + rs->m_RenderSettings.m_DensityScale = m_qrendersettings->GetDensityScale(); + rs->m_RenderSettings.m_ShadingType = m_qrendersettings->GetShadingType(); + rs->m_RenderSettings.m_GradientFactor = m_qrendersettings->GetGradientFactor(); + + // update window/levels / transfer function here!!!! + + rs->m_DirtyFlags.SetFlag(TransferFunctionDirty); +} + +std::shared_ptr +WgpuView3D::getStatus() +{ + return m_viewerWindow->m_renderer->getStatusInterface(); +} + +void +WgpuView3D::OnUpdateRenderer(int rendererType) +{ + if (!isEnabled()) { + LOG_ERROR << "attempted to update GLView3D renderer when view is disabled"; + return; + } + + m_viewerWindow->setRenderer(rendererType); + + emit ChangedRenderer(); +} + +void +WgpuView3D::fromViewerState(const Serialize::ViewerState& s) +{ + m_qrendersettings->SetRendererType(s.rendererType == Serialize::RendererType_PID::PATHTRACE ? 1 : 0); + + // syntactic sugar + CCamera& camera = m_viewerWindow->m_CCamera; + + camera.m_From = glm::vec3(s.camera.eye[0], s.camera.eye[1], s.camera.eye[2]); + camera.m_Target = glm::vec3(s.camera.target[0], s.camera.target[1], s.camera.target[2]); + camera.m_Up = glm::vec3(s.camera.up[0], s.camera.up[1], s.camera.up[2]); + camera.m_FovV = s.camera.fovY; + camera.SetProjectionMode(s.camera.projection == Serialize::Projection_PID::PERSPECTIVE ? PERSPECTIVE : ORTHOGRAPHIC); + camera.m_OrthoScale = s.camera.orthoScale; + + camera.m_Film.m_Exposure = s.camera.exposure; + camera.m_Aperture.m_Size = s.camera.aperture; + camera.m_Focus.m_FocalDistance = s.camera.focalDistance; + + // TODO disentangle these QCamera* _camera and CCamera mCamera objects. Only CCamera should be necessary, I think. + m_qcamera->GetProjection().SetFieldOfView(s.camera.fovY); + m_qcamera->GetFilm().SetExposure(s.camera.exposure); + m_qcamera->GetAperture().SetSize(s.camera.aperture); + m_qcamera->GetFocus().SetFocalDistance(s.camera.focalDistance); +} + +QPixmap +WgpuView3D::capture() +{ + // get the current QScreen + QScreen* screen = QGuiApplication::primaryScreen(); + if (const QWindow* window = windowHandle()) { + screen = window->screen(); + } + if (!screen) { + qWarning("Couldn't capture screen to save image file."); + return QPixmap(); + } + // simply grab the glview window + return screen->grabWindow(winId()); +} + +QImage +WgpuView3D::captureQimage() +{ + if (!isEnabled()) { + return QImage(); + } + +#if 0 + makeCurrent(); + + // Create a one-time FBO to receive the image + QOpenGLFramebufferObjectFormat fboFormat; + fboFormat.setAttachment(QOpenGLFramebufferObject::NoAttachment); + fboFormat.setMipmap(false); + fboFormat.setSamples(0); + fboFormat.setTextureTarget(GL_TEXTURE_2D); + + // NOTE NO ALPHA. if alpha then this will get premultiplied and wash out colors + // TODO : allow user option for transparent qimage, and then put GL_RGBA8 back here + fboFormat.setInternalTextureFormat(GL_RGB8); + check_gl("pre screen capture"); + + const float dpr = devicePixelRatioF(); + QOpenGLFramebufferObject* fbo = + new QOpenGLFramebufferObject(width() * dpr, height() * dpr, fboFormat); + check_gl("create fbo"); + + fbo->bind(); + check_glfb("bind framebuffer for screen capture"); + + // do a render into the temp framebuffer + glViewport(0, 0, fbo->width(), fbo->height()); + m_viewerWindow->m_renderer->render(m_viewerWindow->m_CCamera); + fbo->release(); + + QImage img(fbo->toImage()); + delete fbo; + + return img; +#endif + return QImage(); +} + +void +WgpuView3D::pauseRenderLoop() +{ + std::shared_ptr s = getStatus(); + // the CStatus updates can cause Qt GUI work to happen, + // which can not be called from a separate thread. + // so when we start rendering from another thread, + // we need to either make status updates thread safe, + // or just disable them here. + s->EnableUpdates(false); + m_etimer->stop(); +} + +void +WgpuView3D::restartRenderLoop() +{ + m_etimer->start(); + std::shared_ptr s = getStatus(); + s->EnableUpdates(true); +} + +WgpuCanvas::WgpuCanvas(QCamera* cam, QRenderSettings* qrs, RenderSettings* rs, QWidget* parent) +{ + setAttribute(Qt::WA_DeleteOnClose); + setAttribute(Qt::WA_TranslucentBackground); + setMouseTracking(true); + + m_view = new WgpuView3D(cam, qrs, rs, this); + connect(m_view, SIGNAL(ChangedRenderer()), this, SLOT(OnChangedRenderer())); + m_view->winId(); + + m_layout = new QHBoxLayout(this); + m_layout->setContentsMargins(0, 0, 0, 0); + setLayout(m_layout); + m_layout->addWidget(m_view); + + show(); +} diff --git a/agave_app/wgpuView3D.h b/agave_app/wgpuView3D.h new file mode 100644 index 00000000..c0886ed6 --- /dev/null +++ b/agave_app/wgpuView3D.h @@ -0,0 +1,187 @@ +#pragma once + +#include + +#include "glm.h" + +#include "renderlib/ViewerWindow.h" +#include "renderlib/gesture/gesture.h" +#include "renderlib_wgpu/renderlib_wgpu.h" + +#include +#include +#include + +class QOpenGLContext; + +class CStatus; +class ImageXYZC; +class QCamera; +class IRenderWindow; +class QRenderSettings; +class Scene; +namespace Serialize { +struct ViewerState; +}; + +// does the actual wgpu stuff +// for some reason we need this to be a child of the main view + +class WgpuView3D : public QWidget +{ + Q_OBJECT; + +public: + WgpuView3D(QCamera* cam, QRenderSettings* qrs, RenderSettings* rs, QWidget* parent = 0); + ~WgpuView3D(); + /** + * Get window minimum size hint. + * + * @returns the size hint. + */ + QSize minimumSizeHint() const override; + + /** + * Get window size hint. + * + * @returns the size hint. + */ + QSize sizeHint() const override; + + void initCameraFromImage(Scene* scene); + void retargetCameraForNewVolume(Scene* scene); + void toggleCameraProjection(); + enum MANIPULATOR_MODE + { + NONE, + ROT, + TRANS + }; + void setManipulatorMode(MANIPULATOR_MODE mode); + + void toggleRotateControls(); + void toggleTranslateControls(); + void showRotateControls(bool show); + void showTranslateControls(bool show); + + void onNewImage(Scene* scene); + + const CCamera& getCamera() { return m_viewerWindow->m_CCamera; } + + void fromViewerState(const Serialize::ViewerState& s); + + QPixmap capture(); + QImage captureQimage(); + + // DANGER this must NOT outlive the GLView3D + ViewerWindow* borrowRenderer() { return m_viewerWindow; } + + void pauseRenderLoop(); + void restartRenderLoop(); + + // dummy to make agaveGui happy + void doneCurrent() {} + QOpenGLContext* context() { return nullptr; } + void resizeGL(int w, int h); + void FitToScene(float transitionDurationSeconds = 0.0f); + +signals: + void ChangedRenderer(); + +public slots: + + void OnUpdateCamera(); + void OnUpdateQRenderSettings(void); + void OnUpdateRenderer(int); + void OnSelectionChanged(SceneObject* so); + +public: + std::shared_ptr getStatus(); + +protected: + void mousePressEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + void keyPressEvent(QKeyEvent* event) override; + +private: + QCamera* m_qcamera; + QRenderSettings* m_qrendersettings; + + ViewerWindow* m_viewerWindow; + + /// Rendering timer. + QTimer* m_etimer; + + /// Last mouse position. + QPoint m_lastPos; + + bool m_initialized; + bool m_fakeHidden; + void render(); + void renderWindowContents(WGPUTextureView nextTexture); + WGPUDevice m_device; + WGPUQueue m_queue; + + WgpuWindowContext* m_windowContext; + + WGPURenderPipeline m_pipeline; + + QWidget* m_canvas; + + MANIPULATOR_MODE m_manipulatorMode = MANIPULATOR_MODE::NONE; + +protected: + /// Set up GL context and subsidiary objects. + void initializeGL(); + + void resizeEvent(QResizeEvent* event) override; + void paintEvent(QPaintEvent* event) override; + + virtual QPaintEngine* paintEngine() const override { return nullptr; } +}; + +class WgpuCanvas : public QWidget +{ + Q_OBJECT; + +public: + WgpuCanvas(QCamera* cam, QRenderSettings* qrs, RenderSettings* rs, QWidget* parent = 0); + ~WgpuCanvas() { delete m_view; } + + // make sure every time this updates, the child updates + // void update() override { m_view->update(); } + + std::shared_ptr getStatus() { return m_view->getStatus(); } + QImage captureQimage() { return m_view->captureQimage(); } + void pauseRenderLoop() { m_view->pauseRenderLoop(); } + void restartRenderLoop() { m_view->restartRenderLoop(); } + void doneCurrent() {} + // DANGER this must NOT outlive the WgpuCanvas + ViewerWindow* borrowRenderer() { return m_view->borrowRenderer(); } + const CCamera& getCamera() { return m_view->getCamera(); } + QOpenGLContext* context() { return nullptr; } + void resizeGL(int w, int h) { m_view->resizeGL(w, h); } + void FitToScene(float transitionDurationSeconds = 0.0f) { m_view->FitToScene(transitionDurationSeconds); } + void initCameraFromImage(Scene* scene) { m_view->initCameraFromImage(scene); } + void retargetCameraForNewVolume(Scene* scene) { m_view->retargetCameraForNewVolume(scene); } + + void onNewImage(Scene* scene) { m_view->onNewImage(scene); } + void toggleCameraProjection() { m_view->toggleCameraProjection(); } + void toggleRotateControls() { m_view->toggleRotateControls(); } + void toggleTranslateControls() { m_view->toggleTranslateControls(); } + void fromViewerState(const Serialize::ViewerState& s) { m_view->fromViewerState(s); } + void showRotateControls(bool show) { m_view->showRotateControls(show); } + void showTranslateControls(bool show) { m_view->showTranslateControls(show); } + +signals: + void ChangedRenderer(); + +public slots: + void OnChangedRenderer() { emit ChangedRenderer(); } + +private: + WgpuView3D* m_view; + QHBoxLayout* m_layout; +}; diff --git a/renderlib/Logging.cpp b/renderlib/Logging.cpp index 13bd6041..9d54317c 100644 --- a/renderlib/Logging.cpp +++ b/renderlib/Logging.cpp @@ -50,7 +50,7 @@ getLogPath() // use user home directory instead return std::filesystem::path(getenv("USERPROFILE")) / "AllenInstitute" / "agave"; } -#elif __APPLE__ +#elif defined(__APPLE__) const char* rootdir = getenv("HOME"); if (!rootdir) { struct passwd* pwd = getpwuid(getuid()); @@ -58,7 +58,7 @@ getLogPath() rootdir = pwd->pw_dir; } return std::filesystem::path(rootdir) / "Library" / "Logs" / "AllenInstitute" / "agave"; -#elif __linux__ +#elif defined(__linux__) const char* rootdir = getenv("HOME"); if (!rootdir) { struct passwd* pwd = getpwuid(getuid()); diff --git a/renderlib/ViewerWindow.cpp b/renderlib/ViewerWindow.cpp index 1a351a2b..cb7a65a2 100644 --- a/renderlib/ViewerWindow.cpp +++ b/renderlib/ViewerWindow.cpp @@ -1,5 +1,6 @@ #include "ViewerWindow.h" +#include "../renderlib_wgpu/RenderWgpuPT.h" #include "AreaLightTool.h" #include "AxisHelperTool.h" #include "IRenderWindow.h" @@ -14,7 +15,7 @@ ViewerWindow::ViewerWindow(RenderSettings* rs) : m_renderSettings(rs) - , m_renderer(new RenderGLPT(rs)) + , m_renderer(new RenderWgpuPT(rs)) , m_gestureRenderer(new GestureRendererGL()) , m_rendererType(1) { @@ -291,17 +292,17 @@ ViewerWindow::setRenderer(int rendererType) switch (rendererType) { case 1: LOG_DEBUG << "Set OpenGL pathtrace Renderer"; - m_renderer.reset(new RenderGLPT(m_renderSettings)); + m_renderer.reset(new RenderWgpuPT(m_renderSettings)); m_renderSettings->m_DirtyFlags.SetFlag(TransferFunctionDirty); break; case 2: LOG_DEBUG << "Set OpenGL pathtrace Renderer"; - m_renderer.reset(new RenderGLPT(m_renderSettings)); + m_renderer.reset(new RenderWgpuPT(m_renderSettings)); m_renderSettings->m_DirtyFlags.SetFlag(TransferFunctionDirty); break; default: LOG_DEBUG << "Set OpenGL single pass Renderer"; - m_renderer.reset(new RenderGL(m_renderSettings)); + m_renderer.reset(new RenderWgpuPT(m_renderSettings)); }; m_rendererType = rendererType; diff --git a/renderlib/graphics/IRenderWindow.h b/renderlib/graphics/IRenderWindow.h index 36c815a5..e44cab83 100644 --- a/renderlib/graphics/IRenderWindow.h +++ b/renderlib/graphics/IRenderWindow.h @@ -10,6 +10,17 @@ class Scene; #include +class IRenderTarget +{ +public: + virtual ~IRenderTarget() {} + + virtual void bind() = 0; + virtual void release() = 0; + virtual int width() const = 0; + virtual int height() const = 0; +}; + class IRenderWindow { public: @@ -18,7 +29,7 @@ class IRenderWindow virtual void initialize(uint32_t w, uint32_t h) = 0; virtual void render(const CCamera& camera) = 0; - virtual void renderTo(const CCamera& camera, GLFramebufferObject* fbo) = 0; + virtual void renderTo(const CCamera& camera, IRenderTarget* fbo) = 0; virtual void resize(uint32_t w, uint32_t h) = 0; virtual void getSize(uint32_t& w, uint32_t& h) = 0; virtual void cleanUpResources() {} diff --git a/renderlib/graphics/RenderGL.cpp b/renderlib/graphics/RenderGL.cpp index 59352317..89b47313 100644 --- a/renderlib/graphics/RenderGL.cpp +++ b/renderlib/graphics/RenderGL.cpp @@ -91,7 +91,7 @@ RenderGL::doClear() } void -RenderGL::renderTo(const CCamera& camera, GLFramebufferObject* fbo) +RenderGL::renderTo(const CCamera& camera, IRenderTarget* fbo) { bool haveScene = prepareToRender(); diff --git a/renderlib/graphics/RenderGL.h b/renderlib/graphics/RenderGL.h index 8abb1235..ac06598f 100644 --- a/renderlib/graphics/RenderGL.h +++ b/renderlib/graphics/RenderGL.h @@ -21,7 +21,7 @@ class RenderGL : public IRenderWindow virtual void initialize(uint32_t w, uint32_t h); virtual void render(const CCamera& camera); - virtual void renderTo(const CCamera& camera, GLFramebufferObject* fbo); + virtual void renderTo(const CCamera& camera, IRenderTarget* fbo); virtual void resize(uint32_t w, uint32_t h); virtual void getSize(uint32_t& w, uint32_t& h) { diff --git a/renderlib/graphics/RenderGLPT.cpp b/renderlib/graphics/RenderGLPT.cpp index 50acb5b5..edf1fe48 100644 --- a/renderlib/graphics/RenderGLPT.cpp +++ b/renderlib/graphics/RenderGLPT.cpp @@ -454,7 +454,7 @@ RenderGLPT::render(const CCamera& camera) } void -RenderGLPT::renderTo(const CCamera& camera, GLFramebufferObject* fbo) +RenderGLPT::renderTo(const CCamera& camera, IRenderTarget* fbo) { doRender(camera); diff --git a/renderlib/graphics/RenderGLPT.h b/renderlib/graphics/RenderGLPT.h index a1627a2e..a0219074 100644 --- a/renderlib/graphics/RenderGLPT.h +++ b/renderlib/graphics/RenderGLPT.h @@ -32,7 +32,7 @@ class RenderGLPT : public IRenderWindow virtual void initialize(uint32_t w, uint32_t h); virtual void render(const CCamera& camera); - virtual void renderTo(const CCamera& camera, GLFramebufferObject* fbo); + virtual void renderTo(const CCamera& camera, IRenderTarget* fbo); virtual void resize(uint32_t w, uint32_t h); virtual void getSize(uint32_t& w, uint32_t& h) { diff --git a/renderlib/graphics/gl/Util.cpp b/renderlib/graphics/gl/Util.cpp index bf202746..02ac291f 100644 --- a/renderlib/graphics/gl/Util.cpp +++ b/renderlib/graphics/gl/Util.cpp @@ -798,6 +798,7 @@ void GLShaderProgram::addShader(GLShader* shader) { glAttachShader(m_program, shader->id()); + check_gl("GLShaderProgram::addShader"); m_isLinked = false; } @@ -816,6 +817,7 @@ GLShaderProgram::link() } glLinkProgram(m_program); + check_gl("GLShaderProgram::link"); value = 0; glGetProgramiv(m_program, GL_LINK_STATUS, &value); m_isLinked = (value != 0); @@ -944,3 +946,23 @@ GLShaderProgram::utilMakeSimpleProgram(std::string const& vertexShaderSource, delete fshader; } } + +void +GLShaderProgram::utilMakeSimpleProgram(GLShader* vShader, GLShader* fShader) +{ + if (!vShader->isCompiled()) { + LOG_ERROR << "GLShaderProgram: Failed to compile vertex shader\n" << vShader->log(); + } + + if (!fShader->isCompiled()) { + LOG_ERROR << "GLShaderProgram: Failed to compile fragment shader\n" << fShader->log(); + } + + addShader(vShader); + addShader(fShader); + link(); + + if (!isLinked()) { + LOG_ERROR << "GLShaderProgram: Failed to link shader program\n" << log(); + } +} diff --git a/renderlib/graphics/gl/Util.h b/renderlib/graphics/gl/Util.h index 945f6a4c..928599c9 100644 --- a/renderlib/graphics/gl/Util.h +++ b/renderlib/graphics/gl/Util.h @@ -4,6 +4,7 @@ #include "glm.h" #include "Logging.h" +#include "graphics/IRenderWindow.h" #include @@ -98,7 +99,7 @@ class GLTimer }; // RAII; must have a current gl context at creation time. -class GLFramebufferObject +class GLFramebufferObject : public IRenderTarget { public: GLFramebufferObject(int width, int height, GLint colorInternalFormat); @@ -165,6 +166,7 @@ class GLShaderProgram std::string const& fragmentShaderSource, GLShader** outVShader = nullptr, GLShader** outFShader = nullptr); + void utilMakeSimpleProgram(GLShader* vShader, GLShader* fShader); private: // std::vector m_shaders; diff --git a/renderlib/graphics/glsl/CMakeLists.txt b/renderlib/graphics/glsl/CMakeLists.txt index b0376916..d35077c2 100644 --- a/renderlib/graphics/glsl/CMakeLists.txt +++ b/renderlib/graphics/glsl/CMakeLists.txt @@ -1,6 +1,7 @@ target_include_directories(renderlib PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}" ) + target_sources(renderlib PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/GLBasicVolumeShader.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/GLBasicVolumeShader.h" diff --git a/renderlib/graphics/glsl/GLBasicVolumeShader.cpp b/renderlib/graphics/glsl/GLBasicVolumeShader.cpp index fa9ba44b..b82a4a0a 100644 --- a/renderlib/graphics/glsl/GLBasicVolumeShader.cpp +++ b/renderlib/graphics/glsl/GLBasicVolumeShader.cpp @@ -1,5 +1,6 @@ #include "GLBasicVolumeShader.h" #include "glad/glad.h" +#include "shaders.h" #include "Logging.h" #include "shaders.h" diff --git a/renderlib/graphics/glsl/GLPTAccumShader.cpp b/renderlib/graphics/glsl/GLPTAccumShader.cpp index b7104c18..0917459f 100644 --- a/renderlib/graphics/glsl/GLPTAccumShader.cpp +++ b/renderlib/graphics/glsl/GLPTAccumShader.cpp @@ -1,6 +1,7 @@ #include "glad/glad.h" #include "GLPTAccumShader.h" +#include "shaders.h" #include "shaders.h" diff --git a/renderlib/graphics/glsl/GLPTVolumeShader.cpp b/renderlib/graphics/glsl/GLPTVolumeShader.cpp index 3e10aa5b..55542aad 100644 --- a/renderlib/graphics/glsl/GLPTVolumeShader.cpp +++ b/renderlib/graphics/glsl/GLPTVolumeShader.cpp @@ -17,8 +17,8 @@ GLPTVolumeShader::GLPTVolumeShader() : GLShaderProgram() - , m_vshader() - , m_fshader() + , m_vshader(nullptr) + , m_fshader(nullptr) { m_vshader = new GLShader(GL_VERTEX_SHADER); m_vshader->compileSourceCode(getShaderSource("pathTraceVolume_vert").c_str()); diff --git a/renderlib/graphics/glsl/shaders.h b/renderlib/graphics/glsl/shaders.h index 89608459..5dbf5846 100644 --- a/renderlib/graphics/glsl/shaders.h +++ b/renderlib/graphics/glsl/shaders.h @@ -3,4 +3,4 @@ #include const std::string -getShaderSource(const std::string& shaderName); \ No newline at end of file +getShaderSource(const std::string& shaderName); diff --git a/renderlib/renderlib.cpp b/renderlib/renderlib.cpp index f5ba922a..134b5c53 100644 --- a/renderlib/renderlib.cpp +++ b/renderlib/renderlib.cpp @@ -5,6 +5,7 @@ #include "Logging.h" #include "RenderGL.h" #include "RenderGLPT.h" +#include "shaders.h" #include #include @@ -84,6 +85,10 @@ QOpenGLContext* renderlib::createOpenGLContext() { QOpenGLContext* context = new QOpenGLContext(); + context->setShareContext(QOpenGLContext::globalShareContext()); + // if (dummyContext) { + // context->setShareContext(dummyContext); + // } context->setFormat(getQSurfaceFormat()); // ...and set the format on the context too bool createdOk = context->create(); @@ -214,9 +219,6 @@ renderlib::initialize(std::string assetPath, bool headless, bool listDevices, in bool enableDebug = false; - QSurfaceFormat format = getQSurfaceFormat(); - QSurfaceFormat::setDefaultFormat(format); - HeadlessGLContext* dummyHeadlessContext = nullptr; if (headless) { diff --git a/renderlib/threading.h b/renderlib/threading.h index 22208849..ba64be34 100644 --- a/renderlib/threading.h +++ b/renderlib/threading.h @@ -53,7 +53,7 @@ struct Tasks // queue( lambda ) will enqueue the lambda into the tasks for the threads // to use. A future of the type the lambda returns is given to let you get // the result out. - template> + template> std::future queue(F&& f); // start N threads in the thread pool. diff --git a/renderlib_wgpu/CMakeLists.txt b/renderlib_wgpu/CMakeLists.txt new file mode 100644 index 00000000..1f6deb71 --- /dev/null +++ b/renderlib_wgpu/CMakeLists.txt @@ -0,0 +1,93 @@ +add_library(renderlib_wgpu "${CMAKE_CURRENT_SOURCE_DIR}/renderlib_wgpu.cpp") +target_include_directories(renderlib_wgpu PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + ${GLM_INCLUDE_DIRS} +) +target_sources(renderlib_wgpu PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/renderlib_wgpu.h" + "${CMAKE_CURRENT_SOURCE_DIR}/renderlib_wgpu.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/getsurface_wgpu.h" + "${CMAKE_CURRENT_SOURCE_DIR}/getsurface_wgpu.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/getsurface_wgpu_mac.h" + "${CMAKE_CURRENT_SOURCE_DIR}/getsurface_wgpu_mac.c" + "${CMAKE_CURRENT_SOURCE_DIR}/wgpu_util.h" + "${CMAKE_CURRENT_SOURCE_DIR}/wgpu_util.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/RenderWgpuPT.h" + "${CMAKE_CURRENT_SOURCE_DIR}/RenderWgpuPT.cpp" +) + +# wgpu-native dep +include(ExternalProject) +ExternalProject_Add( + wgpu_native + GIT_REPOSITORY "https://github.com/gfx-rs/wgpu-native.git" + GIT_TAG "v22.1.0.5" # origin/trunk" # "v0.17.2.1" + CONFIGURE_COMMAND "" + BUILD_COMMAND cargo build + COMMAND cargo build --release + INSTALL_COMMAND "" + BUILD_IN_SOURCE true + LOG_BUILD OFF + LOG_DOWNLOAD OFF # redirect output to log-file (so that we have less clutter) + LOG_CONFIGURE OFF # redirect output to log-file (so that we have less clutter) + # this is where the cargo build output lands + BUILD_BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/wgpu_native-prefix/src/wgpu_native/target/release/${CMAKE_STATIC_LIBRARY_PREFIX}wgpu_native${CMAKE_STATIC_LIBRARY_SUFFIX} ${CMAKE_CURRENT_BINARY_DIR}/wgpu_native-prefix/src/wgpu_native/target/debug/${CMAKE_STATIC_LIBRARY_PREFIX}wgpu_native${CMAKE_STATIC_LIBRARY_SUFFIX} +) + +# find_library( +# WGPU_LIBRARY NAMES libwgpu_native.a wgpu_native.lib wgpu_native +# HINTS ${WGPU_NATIVE_LIB_DIR} +# REQUIRED +# ) +if(MSVC) + add_definitions(-DWGPU_TARGET_WINDOWS) + target_compile_options(renderlib_wgpu PRIVATE /W4) + set(OS_LIBRARIES d3dcompiler ws2_32 userenv bcrypt ntdll opengl32) +elseif(APPLE) + add_definitions(-DWGPU_TARGET_MACOS) + set(OS_LIBRARIES "-framework Cocoa" "-framework CoreVideo" "-framework IOKit -framework CoreFoundation -framework QuartzCore -framework Metal") + target_compile_options(renderlib_wgpu PRIVATE -Wall -Wextra -pedantic) + # for getting at window surface stuff + set_source_files_properties("${CMAKE_CURRENT_SOURCE_DIR}/getsurface_wgpu_mac.c" PROPERTIES COMPILE_FLAGS "-x objective-c") +else(MSVC) + if(USE_WAYLAND) + add_definitions(-DWGPU_TARGET_LINUX_WAYLAND) + else(USE_WAYLAND) + add_definitions(-DWGPU_TARGET_LINUX_X11) + find_package(X11 REQUIRED) + set(OS_LIBRARIES "-lm -ldl") + endif(USE_WAYLAND) + + target_compile_options(renderlib_wgpu PRIVATE -Wall -Wextra -pedantic) +endif(MSVC) + +add_dependencies(renderlib_wgpu wgpu_native) + +ExternalProject_Get_Property(wgpu_native SOURCE_DIR) + +# ExternalProject_Get_Property(wgpu_native BINARY_DIR) +target_link_libraries(renderlib_wgpu + debug "${SOURCE_DIR}/target/debug/${CMAKE_STATIC_LIBRARY_PREFIX}wgpu_native${CMAKE_STATIC_LIBRARY_SUFFIX}" + optimized "${SOURCE_DIR}/target/release/${CMAKE_STATIC_LIBRARY_PREFIX}wgpu_native${CMAKE_STATIC_LIBRARY_SUFFIX}" +) + +# C:\Users\dmt\source\repos\agave\build\renderlib_wgpu\wgpu_native-prefix\src\wgpu_native\target\release + +# get the include dir for the c header file +target_include_directories(renderlib_wgpu PUBLIC ${SOURCE_DIR}/ffi) +target_include_directories(renderlib_wgpu PUBLIC ${SOURCE_DIR}/ffi/webgpu-headers) +target_include_directories(renderlib_wgpu PUBLIC ${SOURCE_DIR}/) + +# message(STATUS "wgpu_native: ${SOURCE_DIR}/ffi") +target_link_libraries(renderlib_wgpu + ${CMAKE_DL_LIBS} + ${OS_LIBRARIES} + spdlog::spdlog_header_only +) + +IF(WIN32) + target_link_libraries(renderlib_wgpu glm::glm) +ENDIF(WIN32) +IF(LINUX) + target_link_libraries(renderlib_wgpu ${X11_LIBRARIES}) +ENDIF(LINUX) diff --git a/renderlib_wgpu/RenderWgpuPT.cpp b/renderlib_wgpu/RenderWgpuPT.cpp new file mode 100644 index 00000000..5dfa5b3d --- /dev/null +++ b/renderlib_wgpu/RenderWgpuPT.cpp @@ -0,0 +1,30 @@ +#include "RenderWgpuPT.h" + +#include "../renderlib/Status.h" + +RenderWgpuPT::RenderWgpuPT(RenderSettings* rs) + : m_scene(nullptr) + , m_w(0) + , m_h(0) + , m_renderSettings(rs) + , m_status(new CStatus) +{ +} +RenderWgpuPT::~RenderWgpuPT() {} + +void +RenderWgpuPT::initialize(uint32_t w, uint32_t h) +{ +} +void +RenderWgpuPT::render(const CCamera& camera) +{ +} +void +RenderWgpuPT::renderTo(const CCamera& camera, IRenderTarget* fbo) +{ +} +void +RenderWgpuPT::resize(uint32_t w, uint32_t h) +{ +} diff --git a/renderlib_wgpu/RenderWgpuPT.h b/renderlib_wgpu/RenderWgpuPT.h new file mode 100644 index 00000000..58015995 --- /dev/null +++ b/renderlib_wgpu/RenderWgpuPT.h @@ -0,0 +1,47 @@ +#pragma once + +#include "../renderlib/graphics/IRenderWindow.h" + +class Scene; +class CStatus; +class RenderSettings; +class BoundingBoxDrawable; + +class RenderWgpuPT : public IRenderWindow +{ +public: + RenderWgpuPT(RenderSettings* rs); + virtual ~RenderWgpuPT(); + + virtual void initialize(uint32_t w, uint32_t h); + virtual void render(const CCamera& camera); + virtual void renderTo(const CCamera& camera, IRenderTarget* fbo); + virtual void resize(uint32_t w, uint32_t h); + virtual void getSize(uint32_t& w, uint32_t& h) + { + w = m_w; + h = m_h; + } + virtual void cleanUpResources() {} + virtual RenderSettings& renderSettings() { return *m_renderSettings; } + virtual Scene* scene() { return m_scene; } + virtual void setScene(Scene* s) { m_scene = s; } + + virtual std::shared_ptr getStatusInterface() { return m_status; } + +private: + RenderSettings* m_renderSettings; + Scene* m_scene; + + BoundingBoxDrawable* m_boundingBoxDrawable; + + // screen size auxiliary buffers for rendering + unsigned int* m_randomSeeds1; + unsigned int* m_randomSeeds2; + // incrementing integer to give to shader + int m_RandSeed; + + int m_w, m_h; + + std::shared_ptr m_status; +}; diff --git a/renderlib_wgpu/getsurface_wgpu.cpp b/renderlib_wgpu/getsurface_wgpu.cpp new file mode 100644 index 00000000..53b2f721 --- /dev/null +++ b/renderlib_wgpu/getsurface_wgpu.cpp @@ -0,0 +1,101 @@ +#include "getsurface_wgpu.h" + +#include "../renderlib/Logging.h" + +#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) +#include +#elif defined(__APPLE__) +#include "getsurface_wgpu_mac.h" +#elif defined(__linux__) +#include +#include +#include +#endif + +#include + +WGPUSurface +get_surface_from_canvas(WGPUInstance instance, void* win_id) +{ + // "" + // "Get an id representing the surface to render to. The way to + // obtain this id differs per platform and GUI toolkit."" + // " + WGPUSurfaceDescriptor surface_descriptor; + surface_descriptor.label = "AGAVE wgpu surface"; + +#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) + HINSTANCE hinstance = GetModuleHandle(NULL); + WGPUSurfaceDescriptorFromWindowsHWND wgpustruct; + wgpustruct.hinstance = hinstance; + wgpustruct.hwnd = win_id; + wgpustruct.chain.next = nullptr; + wgpustruct.chain.sType = WGPUSType_SurfaceDescriptorFromWindowsHWND; + surface_descriptor.nextInChain = (const WGPUChainedStruct*)(&wgpustruct); + +#elif defined(__APPLE__) + void* metal_layer_ptr = getMetalLayerFromWindow((void*)win_id); + + WGPUSurfaceDescriptorFromMetalLayer wgpustruct; + wgpustruct.layer = metal_layer_ptr; + wgpustruct.chain.next = nullptr; + wgpustruct.chain.sType = WGPUSType_SurfaceDescriptorFromMetalLayer; + surface_descriptor.nextInChain = (const WGPUChainedStruct*)(&wgpustruct); + +#elif __linux__ + Display* display_id = XOpenDisplay(nullptr); + const char* xdgsessiontype = getenv("XDG_SESSION_TYPE"); + std::string xdgsessiontype_str = xdgsessiontype ? xdgsessiontype : ""; + bool is_wayland = false; + bool is_x11 = false; + bool is_xcb = false; + if (xdgsessiontype_str.empty()) { + const char* waylanddisplayenv = getenv("WAYLAND_DISPLAY"); + std::string waylanddisplay_str = waylanddisplayenv ? waylanddisplayenv : ""; + if (waylanddisplay_str.empty()) { + // check DISPLAY ? + // const char* displayenv = getenv("DISPLAY"); + is_x11 = true; + } else { + is_wayland = true; + } + } else { + is_wayland = (xdgsessiontype_str.find("wayland") != std::string::npos); + is_x11 = (xdgsessiontype_str.find("x11") != std::string::npos); + } + WGPUSurfaceDescriptorFromWaylandSurface wgpustruct1; + WGPUSurfaceDescriptorFromXcbWindow wgpustruct2; + WGPUSurfaceDescriptorFromXlibWindow wgpustruct3; + if (is_wayland) { + // # todo: wayland seems to be broken right now + wgpustruct1.display = display_id; + wgpustruct1.surface = win_id; + wgpustruct1.chain.next = nullptr; + wgpustruct1.chain.sType = WGPUSType_SurfaceDescriptorFromWaylandSurface; + surface_descriptor.nextInChain = (const WGPUChainedStruct*)(&wgpustruct1); + LOG_INFO << "Wayland surface descriptor"; + } else if (is_xcb) { + // # todo: xcb untested + wgpustruct2.connection = display_id; + wgpustruct2.window = *((uint32_t*)(&win_id)); + wgpustruct2.chain.next = nullptr; + wgpustruct2.chain.sType = WGPUSType_SurfaceDescriptorFromXcbWindow; + surface_descriptor.nextInChain = (const WGPUChainedStruct*)(&wgpustruct2); + LOG_INFO << "XCB surface descriptor"; + } else { + wgpustruct3.display = display_id; + wgpustruct3.window = *((uint32_t*)(&win_id)); + // wgpustruct3.window = static_cast(win_id); //*((uint32_t*)(&win_id)); + wgpustruct3.chain.next = nullptr; + wgpustruct3.chain.sType = WGPUSType_SurfaceDescriptorFromXlibWindow; + surface_descriptor.nextInChain = (const WGPUChainedStruct*)(&wgpustruct3); + LOG_INFO << "Xlib surface descriptor"; + } + +#else + throw("Cannot get surface id: unsupported platform."); +#endif + + WGPUSurface surface = wgpuInstanceCreateSurface(instance, &surface_descriptor); + return surface; +} diff --git a/renderlib_wgpu/getsurface_wgpu.h b/renderlib_wgpu/getsurface_wgpu.h new file mode 100644 index 00000000..c0908eed --- /dev/null +++ b/renderlib_wgpu/getsurface_wgpu.h @@ -0,0 +1,18 @@ +#pragma once + +#include "webgpu-headers/webgpu.h" +#include "wgpu.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * Get a WGPUSurface from a window handle. + */ + WGPUSurface get_surface_from_canvas(WGPUInstance instance, void* win_id); + +#ifdef __cplusplus +} +#endif diff --git a/renderlib_wgpu/getsurface_wgpu_mac.c b/renderlib_wgpu/getsurface_wgpu_mac.c new file mode 100644 index 00000000..c8b5ccbf --- /dev/null +++ b/renderlib_wgpu/getsurface_wgpu_mac.c @@ -0,0 +1,35 @@ + +#ifdef __APPLE__ + +#include "getsurface_wgpu_mac.h" + +#include +#include +#include + +void* +getMetalLayerFromWindow(void* win_id) +{ + // # id metal_layer = NULL; + // # NSWindow *ns_window = glfwGetCocoaWindow(window); + // # [ns_window.contentView setWantsLayer:YES]; + // # metal_layer = [CAMetalLayer layer]; + // # [ns_window.contentView setLayer:metal_layer]; + // # surface = wgpu_create_surface_from_metal_layer(metal_layer); + // # } + + NSView* view = (NSView*)(win_id); + if (![view isKindOfClass:[NSView class]]) { + return NULL; + } + + if (![view.layer isKindOfClass:[CAMetalLayer class]]) { + // orilayer = [view layer]; + [view setLayer:[CAMetalLayer layer]]; + [view setWantsLayer:YES]; + } + + // TODO cleanup later? + return [view layer]; +} +#endif diff --git a/renderlib_wgpu/getsurface_wgpu_mac.h b/renderlib_wgpu/getsurface_wgpu_mac.h new file mode 100644 index 00000000..5b88003f --- /dev/null +++ b/renderlib_wgpu/getsurface_wgpu_mac.h @@ -0,0 +1,14 @@ +#ifdef __APPLE__ + +#ifdef __cplusplus +extern "C" +{ +#endif + + void* getMetalLayerFromWindow(void* win_id); + +#ifdef __cplusplus +} +#endif + +#endif // __APPLE__ diff --git a/renderlib_wgpu/renderlib_wgpu.cpp b/renderlib_wgpu/renderlib_wgpu.cpp new file mode 100644 index 00000000..2a6e9968 --- /dev/null +++ b/renderlib_wgpu/renderlib_wgpu.cpp @@ -0,0 +1,225 @@ +#include "renderlib_wgpu.h" + +#include "../renderlib/Logging.h" + +#include "getsurface_wgpu.h" +#include "wgpu_util.h" + +#include + +static bool renderLibInitialized = false; + +static bool renderLibHeadless = false; + +static const uint32_t AICS_DEFAULT_STENCIL_BUFFER_BITS = 8; + +static const uint32_t AICS_DEFAULT_DEPTH_BUFFER_BITS = 24; + +static WGPUInstance sInstance = nullptr; + +int +renderlib_wgpu::initialize(bool headless, bool listDevices, int selectedGpu) +{ + if (renderLibInitialized && sInstance) { + return 1; + } + + WGPUInstanceDescriptor desc = {}; + desc.nextInChain = nullptr; + sInstance = wgpuCreateInstance(&desc); + if (!sInstance) { + LOG_ERROR << "Could not initialize WebGPU, wgpuCreateInstance failed!"; + return 0; + } + + renderLibInitialized = true; + + renderLibHeadless = headless; + + LOG_INFO << "Renderlib_wgpu startup"; + + bool enableDebug = false; + + // list out all known adapters in a headless style + LOG_INFO << "Enumerate Adapters:"; + const size_t adapter_count = wgpuInstanceEnumerateAdapters(sInstance, NULL, NULL); + WGPUAdapter* adapters = new WGPUAdapter[adapter_count]; + assert(adapters); + wgpuInstanceEnumerateAdapters(sInstance, NULL, adapters); + for (int i = 0; i < adapter_count; i++) { + WGPUAdapter adapter = adapters[i]; + assert(adapter); + + WGPUAdapterInfo info = { 0 }; + wgpuAdapterGetInfo(adapter, &info); + LOG_INFO << "WGPUAdapter: " << i; + LOG_INFO << "\tvendor: " << info.vendor; + LOG_INFO << "\tarchitecture: " << info.architecture; + LOG_INFO << "\tdevice: " << info.device; + LOG_INFO << "\tdescription: " << info.description; + LOG_INFO << "\tbackendType: %#.8x" << info.backendType; + // LOG_INFO << "\tadapterType: %#.8x" << ; + // LOG_INFO << "\tvendorID: %" PRIu32 "" << ; + // LOG_INFO << "\tdeviceID: %" PRIu32 "" << ; + // info.backendType, + // info.adapterType, + // info.vendorID, + // info.deviceID); + + wgpuAdapterInfoFreeMembers(info); + wgpuAdapterRelease(adapter); + } + delete[] adapters; + + if (headless) { + } else { + } + + if (enableDebug) { + } + + // load gl functions and init stuff + + // then log out some info + // LOG_INFO << "GL_VENDOR: " << std::string((char*)glGetString(GL_VENDOR)); + // LOG_INFO << "GL_RENDERER: " << std::string((char*)glGetString(GL_RENDERER)); + + return 1; +} + +WGPUInstance +renderlib_wgpu::getInstance() +{ + return sInstance; +} + +WGPUSurface +renderlib_wgpu::getSurfaceFromCanvas(void* win_id) +{ + WGPUSurface surface = get_surface_from_canvas(getInstance(), win_id); + return surface; +} + +WGPUAdapter +renderlib_wgpu::getAdapter(WGPUSurface surface) +{ + WGPUAdapter adapter; + WGPURequestAdapterOptions options = {}; + options.nextInChain = NULL; + options.compatibleSurface = surface; + + wgpuInstanceRequestAdapter(getInstance(), &options, request_adapter_callback, (void*)&adapter); + + printAdapterFeatures(adapter); + + return adapter; +} + +WGPUDevice +renderlib_wgpu::requestDevice(WGPUAdapter adapter) +{ + WGPUDevice device; + + WGPULimits limits; + limits.maxBindGroups = 1; + + WGPURequiredLimits requiredLimits = {}; + requiredLimits.nextInChain = NULL; + requiredLimits.limits = limits; + + WGPUChainedStruct chain = {}; + chain.next = NULL; + chain.sType = (WGPUSType)WGPUSType_DeviceExtras; + WGPUDeviceExtras deviceExtras = {}; + deviceExtras.chain = chain; + deviceExtras.tracePath = NULL; + + WGPUUncapturedErrorCallbackInfo uncapturedErrorInfo = {}; + uncapturedErrorInfo.nextInChain = nullptr; + uncapturedErrorInfo.callback = handle_uncaptured_error; + uncapturedErrorInfo.userdata = NULL; + + WGPUQueueDescriptor queueDescriptor = {}; + queueDescriptor.nextInChain = NULL; + queueDescriptor.label = "AGAVE default wgpu queue"; + + WGPUDeviceDescriptor deviceDescriptor = {}; + deviceDescriptor.nextInChain = (const WGPUChainedStruct*)&deviceExtras; + deviceDescriptor.label = "AGAVE wgpu device"; + deviceDescriptor.requiredFeatureCount = 0; + deviceDescriptor.requiredLimits = nullptr; + deviceDescriptor.defaultQueue = queueDescriptor; + deviceDescriptor.deviceLostCallback = handle_device_lost; + deviceDescriptor.deviceLostUserdata = NULL; + deviceDescriptor.uncapturedErrorCallbackInfo = uncapturedErrorInfo; + + // creates/ fills in m_device! + wgpuAdapterRequestDevice(adapter, &deviceDescriptor, request_device_callback, (void*)&device); + + return device; +} + +void +renderlib_wgpu::cleanup() +{ + if (!renderLibInitialized) { + return; + } + LOG_INFO << "Renderlib_wgpu shutdown"; + + wgpuInstanceRelease(sInstance); + + if (renderLibHeadless) { + } + renderLibInitialized = false; +} + +WgpuWindowContext* +renderlib_wgpu::setupWindowContext(WGPUSurface surface, WGPUDevice device, uint32_t width, uint32_t height) +{ + WgpuWindowContext* context = new WgpuWindowContext(); + context->m_surface = surface; + context->m_surfaceFormat = WGPUTextureFormat_BGRA8Unorm; + context->m_surfaceConfig = {}; + context->m_surfaceConfig.nextInChain = NULL; + context->m_surfaceConfig.device = device; + context->m_surfaceConfig.format = context->m_surfaceFormat; + context->m_surfaceConfig.usage = WGPUTextureUsage_RenderAttachment; + context->m_surfaceConfig.viewFormatCount = 0; + context->m_surfaceConfig.viewFormats = NULL; + context->m_surfaceConfig.alphaMode = WGPUCompositeAlphaMode_Auto; + context->m_surfaceConfig.width = width; + context->m_surfaceConfig.height = height; + context->m_surfaceConfig.presentMode = WGPUPresentMode_Fifo; + + wgpuSurfaceConfigure(context->m_surface, &context->m_surfaceConfig); + + return context; +} + +WgpuWindowContext::WgpuWindowContext() +{ + m_surfaceConfig = {}; + m_surface = nullptr; + m_surfaceFormat = WGPUTextureFormat_BGRA8Unorm; +} + +WgpuWindowContext::~WgpuWindowContext() +{ + wgpuSurfaceUnconfigure(m_surface); + wgpuSurfaceRelease(m_surface); +} + +void +WgpuWindowContext::resize(uint32_t width, uint32_t height) +{ + m_surfaceConfig.width = width; + m_surfaceConfig.height = height; + wgpuSurfaceConfigure(m_surface, &m_surfaceConfig); +} + +void +WgpuWindowContext::present() +{ + wgpuSurfacePresent(m_surface); +} diff --git a/renderlib_wgpu/renderlib_wgpu.h b/renderlib_wgpu/renderlib_wgpu.h new file mode 100644 index 00000000..ac66f746 --- /dev/null +++ b/renderlib_wgpu/renderlib_wgpu.h @@ -0,0 +1,35 @@ +#pragma once + +#include "webgpu-headers/webgpu.h" +#include "wgpu.h" + +#include +#include +#include + +class WgpuWindowContext +{ + +public: + WgpuWindowContext(); + ~WgpuWindowContext(); + WGPUSurface m_surface; + WGPUSurfaceConfiguration m_surfaceConfig; + WGPUTextureFormat m_surfaceFormat; + + void resize(uint32_t width, uint32_t height); + void present(); +}; + +class renderlib_wgpu +{ +public: + static int initialize(bool headless = false, bool listDevices = false, int selectedGpu = 0); + static void cleanup(); + + static WGPUInstance getInstance(); + static WGPUSurface getSurfaceFromCanvas(void* win_id); + static WGPUAdapter getAdapter(WGPUSurface surface); + static WGPUDevice requestDevice(WGPUAdapter adapter); + static WgpuWindowContext* setupWindowContext(WGPUSurface surface, WGPUDevice device, uint32_t width, uint32_t height); +}; diff --git a/renderlib_wgpu/shaders/accum.wgsl b/renderlib_wgpu/shaders/accum.wgsl new file mode 100644 index 00000000..4ef65ade --- /dev/null +++ b/renderlib_wgpu/shaders/accum.wgsl @@ -0,0 +1,49 @@ +// Vertex shader +struct CameraUniform { + modelViewMatrix: mat4x4, + projectionMatrix: mat4x4, +}; +@group(1) @binding(0) // 1. +var camera: CameraUniform; + + +struct VertexInput { + @location(0) position: vec2, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) pObj: vec2, +}; + +@vertex +fn vs_main( + v: VertexInput, +) -> VertexOutput { + var out: VertexOutput; + out.pObj = v.position; + out.clip_position = vec4(v.position, 0.0, 1.0); + return out; +} + + +@group(0) @binding(0) +var textureRender: texture_2d; +@group(0) @binding(1) +var textureAccum: texture_2d; +@group(0) @binding(2) +var s_tex: sampler; +@group(0) @binding(3) +var numIterations: i32; + +fn CumulativeMovingAverage(A: vec4, Ax: vec4, N: i32) -> vec4 { + return A + ((Ax - A) / max(f32(N), 1.0f)); +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + var accum: vec4 = textureSampleLevel(textureAccum, s_tex, (in.pObj + 1.0) / 2.0, 0.0).rgba; + var render: vec4 = textureSampleLevel(textureRender, s_tex, (in.pObj + 1.0) / 2.0, 0.0).rgba; + + return CumulativeMovingAverage(accum, render, numIterations); +} diff --git a/renderlib_wgpu/shaders/basicVol.wgsl b/renderlib_wgpu/shaders/basicVol.wgsl new file mode 100644 index 00000000..0d81ef07 --- /dev/null +++ b/renderlib_wgpu/shaders/basicVol.wgsl @@ -0,0 +1,203 @@ + +struct VertexInput { + @location(0) position: vec3, +}; + +struct CameraUniform { + modelViewMatrix: mat4x4, + projectionMatrix: mat4x4, +}; +@group(1) @binding(0) // 1. +var camera: CameraUniform; + + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) pObj: vec3, +}; + + + +@vertex +fn vs_main( + v: VertexInput, +) -> VertexOutput { + var out: VertexOutput; + out.pObj = v.position; + out.clip_position = camera.projectionMatrix * camera.modelViewMatrix * vec4(v.position, 1.0); + return out; +} + + + +@group(0) @binding(0) +var textureAtlas: texture_3d; +@group(0) @binding(1) +var textureAtlasMask: texture_2d; +@group(0) @binding(2) +var s_texAtlas: sampler; +@group(0) @binding(3) +var s_texAtlasMask: sampler; + +struct VolumeUniforms { + inverseModelViewMatrix: mat4x4, + iResolution: vec2, + isPerspective: f32, + orthoScale: f32, + GAMMA_MIN: f32, + GAMMA_MAX: f32, + GAMMA_SCALE: f32, + BRIGHTNESS: f32, + DENSITY: f32, + maskAlpha: f32, + BREAK_STEPS: i32, + AABB_CLIP_MIN: vec3, + AABB_CLIP_MAX: vec3, + dataRangeMin: f32, // 0..1 (mapped from 0..uint16_max) + dataRangeMax: f32, // 0..1 (mapped from 0..uint16_max) +}; +@group(1) @binding(0) // 1. +var v: VolumeUniforms; + +fn powf(a: f32, b: f32) -> f32 { + return pow(a, b); +} + +fn rand(co: vec2, fragCoord: vec2) -> f32 { + var threadId = fragCoord.x / (fragCoord.y + 1.0); + var bigVal = threadId * 1299721.0 / 911.0; + var smallVal: vec2 = vec2(threadId * 7927.0 / 577.0, threadId * 104743.0 / 1039.0); + return fract(sin(dot(co, smallVal)) * bigVal); +} + +fn luma2Alpha(color: vec4, vmin: f32, vmax: f32, C: f32) -> vec4 { + var x = max(color[2], max(color[0], color[1])); + var xi = (x - vmin) / (vmax - vmin); + xi = clamp(xi, 0.0, 1.0); + var y = pow(xi, C); + y = clamp(y, 0.0, 1.0); + return vec4(color[0], color[1], color[2], y); +} + +fn sampleAs3DTexture(tex: texture_3d, s: sampler, pos: vec4) -> vec4 { + var bounds = f32(pos[0] > 0.001 && pos[0] < 0.999 && pos[1] > 0.001 && pos[1] < 0.999 && pos[2] > 0.001 && pos[2] < 0.999); + + var texval = textureSampleLevel(tex, s, pos.xyz, 0.0).rgba; + var retval = vec4(texval.rgb, 1.0); + +// f32 texval = textureLod(tex, pos.xyz, 0).r; +// texval = (texval - dataRangeMin) / (dataRangeMax - dataRangeMin); +// vec4 retval = vec4(texval, texval, texval, 1.0); + return bounds * retval; +} + +fn sampleStack(tex: texture_3d, s: sampler, pos: vec4) -> vec4 { + var col = sampleAs3DTexture(tex, s, pos); + col = luma2Alpha(col, v.GAMMA_MIN, v.GAMMA_MAX, v.GAMMA_SCALE); + return col; +} + +//->intersect AXIS-ALIGNED box routine +// +fn intersectBox(r_o: vec3, r_d: vec3, boxMin: vec3, boxMax: vec3, tnear: ptr, tfar: ptr) -> bool { + var invR = vec3(1.0, 1.0, 1.0) / r_d; + var tbot = invR * (boxMin - r_o); + var ttop = invR * (boxMax - r_o); + var tmin = min(ttop, tbot); + var tmax = max(ttop, tbot); + var largest_tmin = max(max(tmin.x, tmin.y), max(tmin.x, tmin.z)); + var smallest_tmax = min(min(tmax.x, tmax.y), min(tmax.x, tmax.z)); + *tnear = largest_tmin; + *tfar = smallest_tmax; + return(smallest_tmax > largest_tmin); +} + +fn mymod(x: f32, y: f32) -> f32 { + return x - y * floor(x / y); +} + + const maxSteps: i32 = 512; +fn integrateVolume(fragCoord: vec2, eye_o: vec4, eye_d: vec4, tnear: f32, tfar: f32, clipNear: f32, clipFar: f32, textureAtlas: texture_3d) -> vec4 { + var C = vec4(0.0); + var tend = min(tfar, clipFar); + var tbegin = tnear; + var csteps = clamp(f32(v.BREAK_STEPS), 1.0, f32(maxSteps)); + var invstep = 1.0 / csteps; + var r = 0.5 - 1.0 * rand(eye_d.xy, fragCoord); + var tstep = invstep; + var tfarsurf = r * tstep; + var overflow = mymod((tfarsurf - tend), tstep); + var t = tbegin + overflow; + t += r * tstep; + var tdist = 0.0; + var numSteps: i32 = 0; + + var pos: vec4; var col: vec4; + var s = 0.5 * f32(maxSteps) / csteps; + for (var i: i32 = 0; i < maxSteps; i++) { + pos = eye_o + eye_d * t; + pos = vec4(pos.xyz + 0.5, pos.w);//0.5 * (pos + 1.0); // map position from [boxMin, boxMax] to [0, 1] coordinates + col = sampleStack(textureAtlas, s_texAtlas, pos); + + //Finish up by adding brightness/density + col = vec4(col.xyz * v.BRIGHTNESS, col.w * v.DENSITY); + var stepScale = (1.0 - powf((1.0 - col.w), s)); + col = vec4(col.xyz * stepScale, stepScale); + col = clamp(col, vec4(0.0), vec4(1.0)); + + C = (1.0 - C.w) * col + C; + t += tstep; + numSteps = i; + if t > tend { break;} + if C.w > 1.0 {break;} + } + return C; +} + + @fragment +fn fs_main( + //@builtin(position) position: vec4, + inData: VertexOutput +) -> @location(0) vec4 { + var outputColour: vec4 = vec4(1.0, 0.0, 0.0, 1.0); + // gl_FragCoord defaults to 0,0 at lower left + var vUv: vec2 = inData.clip_position.xy / v.iResolution.xy; + + var eyeRay_o: vec3; var eyeRay_d: vec3; + if v.isPerspective != 0.0 { + // camera position in camera space is 0,0,0! + eyeRay_o = (v.inverseModelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz; + eyeRay_d = normalize(inData.pObj - eyeRay_o); + } else { + var zDist = 2.0; + eyeRay_d = (v.inverseModelViewMatrix * vec4(0.0, 0.0, -zDist, 0.0)).xyz; + var ray_o: vec4 = vec4(2.0 * vUv - 1.0, 1.0, 1.0); + ray_o = ray_o * vec4(v.orthoScale * v.iResolution.x / v.iResolution.y, v.orthoScale, 1.0, 1.0); +// ray_o.xy *= v.orthoScale; +// ray_o.x *= v.iResolution.x / v.iResolution.y; + eyeRay_o = (v.inverseModelViewMatrix * ray_o).xyz; + } + + var boxMin = v.AABB_CLIP_MIN; + var boxMax = v.AABB_CLIP_MAX; + var tnear: f32; var tfar: f32; + var hit = intersectBox(eyeRay_o, eyeRay_d, boxMin, boxMax, &tnear, &tfar); + if !hit { + outputColour = vec4(1.0, 0.0, 1.0, 0.0); + return outputColour; + } +//else { +// outputColour = vec4(1.0, 1.0, 1.0, 1.0); +// return; +//} + var clipNear = 0.0;//-(dot(eyeRay_o.xyz, eyeNorm) + dNear) / dot(eyeRay_d.xyz, eyeNorm); + var clipFar = 10000.0;//-(dot(eyeRay_o.xyz,-eyeNorm) + dFar ) / dot(eyeRay_d.xyz,-eyeNorm); + + var C = integrateVolume(inData.clip_position.xy, vec4(eyeRay_o, 1.0), vec4(eyeRay_d, 0.0), + tnear, tfar, + clipNear, clipFar, + textureAtlas); + C = clamp(C, vec4(0.0), vec4(1.0)); + outputColour = C; + return outputColour; +} diff --git a/renderlib_wgpu/shaders/copy.wgsl b/renderlib_wgpu/shaders/copy.wgsl new file mode 100644 index 00000000..486f2e33 --- /dev/null +++ b/renderlib_wgpu/shaders/copy.wgsl @@ -0,0 +1,30 @@ +struct VertexInput { + @location(0) position: vec3, + @location(1) uv: vec2, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) vUv: vec2, +}; + +@vertex +fn vs_main( + v: VertexInput, +) -> VertexOutput { + var out: VertexOutput; + out.vUv = v.uv; + out.clip_position = vec4(v.position, 1.0); + return out; +} + + +@group(0) @binding(0) +var tTexture0: texture_2d; +@group(0) @binding(1) +var s: sampler; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return textureSample(tTexture0, s, in.vUv); +} diff --git a/renderlib_wgpu/shaders/flat.wgsl b/renderlib_wgpu/shaders/flat.wgsl new file mode 100644 index 00000000..df5fde8e --- /dev/null +++ b/renderlib_wgpu/shaders/flat.wgsl @@ -0,0 +1,31 @@ +// Vertex shader +struct Uniform { + mvp: mat4x4, + color: vec4, +}; +@group(1) @binding(0) // 1. +var u: Uniform; + +struct VertexInput { + @location(0) position: vec3, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) f_color: vec4, +}; + +@vertex +fn vs_main( + v: VertexInput, +) -> VertexOutput { + var out: VertexOutput; + out.clip_position = u.mvp * vec4(v.position, 1.0); + out.f_color = u.color; + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return in.f_color; +} diff --git a/renderlib_wgpu/shaders/gui.wgsl b/renderlib_wgpu/shaders/gui.wgsl new file mode 100644 index 00000000..5c65ec64 --- /dev/null +++ b/renderlib_wgpu/shaders/gui.wgsl @@ -0,0 +1,73 @@ +struct VertexInput { + @location(0) vPos: vec3, + @location(1) vUV: vec2, + @location(2) vCol: vec4, + @location(3) vCode: u32, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) Frag_color: vec4, + @location(1) Frag_UV: vec2, +}; + +struct Uniform { + projection: mat4x4, + picking: i32, //< draw for display or for picking? Picking has no texture. +}; +@group(1) @binding(0) // 1. +var u: Uniform; + +@vertex +fn vs_main( + v: VertexInput, +) -> VertexOutput { + var out: VertexOutput; + out.Frag_UV = v.vUV; + if u.picking == 1 { + out.Frag_color = vec4(f32(v.vCode & 0xffu) / 255.0, + f32((v.vCode >> 8) & 0xffu) / 255.0, + f32((v.vCode >> 16) & 0xffu) / 255.0, + 1.0); + } else { + out.Frag_color = v.vCol; + } + out.clip_position = u.projection * vec4(v.vPos, 1.0); + return out; +} + +@group(0) @binding(0) +var Texture: texture_2d; +@group(0) @binding(1) +var s: sampler; + +const EPSILON:f32 = 0.1; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + var result = in.Frag_color; + + // When drawing selection codes, everything is opaque. + if u.picking == 1 { + result.w = 1.0; + } + + // Gesture geometry handshake: any uv value below -64 means + // no texture lookup. Check VertsCode::k_noTexture + // (add an epsilon to fix some fp errors. + // TODO check to see if highp would have helped) + if u.picking == 0 && in.Frag_UV.x > -64 + EPSILON { + result *= textureSample(Texture, s, in.Frag_UV.xy); + } + + // Gesture geometry handshake: any uv equal to -128 means + // overlay a checkerboard pattern. Check VertsCode::k_marqueePattern + if in.Frag_UV.x == -128.0 { + // Create a pixel checkerboard pattern used for marquee + // selection + var x = i32(in.clip_position.x); + var y = i32(in.clip_position.y); + if ((x + y) & 1) == 0 {result = vec4(0, 0, 0, 1);} + } + return result; +} diff --git a/renderlib_wgpu/shaders/image.wgsl b/renderlib_wgpu/shaders/image.wgsl new file mode 100644 index 00000000..66c6d1cc --- /dev/null +++ b/renderlib_wgpu/shaders/image.wgsl @@ -0,0 +1,34 @@ + +struct Uniform { + mvp: mat4x4, +}; +@group(1) @binding(0) // 1. +var u: Uniform; + +struct VertexInput { + @location(0) coord2d: vec2, + @location(1) texcoord: vec2, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) f_texcoord: vec2, +}; + +@vertex +fn vs_main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.clip_position = u.mvp * vec4(in.coord2d, 0.0, 1.0); + out.f_texcoord = in.texcoord; + return out; +} + +@group(0) @binding(0) +var tex: texture_2d; +@group(0) @binding(1) +var s: sampler; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return textureSample(tex, s, in.f_texcoord); +} diff --git a/renderlib_wgpu/shaders/ptVol_frag.wgsl b/renderlib_wgpu/shaders/ptVol_frag.wgsl new file mode 100644 index 00000000..955112a3 --- /dev/null +++ b/renderlib_wgpu/shaders/ptVol_frag.wgsl @@ -0,0 +1,1233 @@ +#version 460 core + +#define PI (3.1415926535897932384626433832795) +#define PI_OVER_2 (1.57079632679489661923) +#define PI_OVER_4 (0.785398163397448309616) +#define INV_PI (1.0/PI) +#define INV_2_PI (0.5/PI) +#define INV_4_PI (0.25/PI) + +const vec3 BLACK = vec3(0,0,0); +const vec3 WHITE = vec3(1.0,1.0,1.0); +const int ShaderType_Brdf = 0; +const int ShaderType_Phase = 1; + +in vec2 vUv; +out vec4 out_FragColor; + +struct Camera { + vec3 m_from; + vec3 m_U, m_V, m_N; + vec4 m_screen; // left, right, bottom, top + vec2 m_invScreen; // 1/w, 1/h + float m_focalDistance; + float m_apertureSize; + float m_isPerspective; +}; + +uniform Camera gCamera; + +struct Light { + float m_theta; + float m_phi; + float m_width; + float m_halfWidth; + float m_height; + float m_halfHeight; + float m_distance; + float m_skyRadius; + vec3 m_P; + vec3 m_target; + vec3 m_N; + vec3 m_U; + vec3 m_V; + float m_area; + float m_areaPdf; + vec3 m_color; + vec3 m_colorTop; + vec3 m_colorMiddle; + vec3 m_colorBottom; + int m_T; +}; +const int NUM_LIGHTS = 2; +uniform Light gLights[2]; + +uniform vec3 gClippedAaBbMin; +uniform vec3 gClippedAaBbMax; +uniform float gDensityScale; +uniform float gStepSize; +uniform float gStepSizeShadow; +uniform sampler3D volumeTexture; +uniform vec3 gInvAaBbSize; +uniform int g_nChannels; +uniform int gShadingType; +uniform vec3 gGradientDeltaX; +uniform vec3 gGradientDeltaY; +uniform vec3 gGradientDeltaZ; +uniform float gInvGradientDelta; +uniform float gGradientFactor; +uniform float uShowLights; + +// per channel +uniform sampler2D g_lutTexture[4]; +uniform sampler2D g_colormapTexture[4]; +uniform vec4 g_intensityMax; +uniform vec4 g_intensityMin; +uniform vec4 g_lutMax; +uniform vec4 g_lutMin; +uniform float g_opacity[4]; +uniform vec3 g_emissive[4]; +uniform vec3 g_diffuse[4]; +uniform vec3 g_specular[4]; +uniform float g_roughness[4]; + +// compositing / progressive render +uniform float uFrameCounter; +uniform float uSampleCounter; +uniform vec2 uResolution; +uniform sampler2D tPreviousTexture; + +// from iq https://www.shadertoy.com/view/4tXyWN +float rand( inout uvec2 seed ) +{ + seed += uvec2(1); + uvec2 q = 1103515245U * ( (seed >> 1U) ^ (seed.yx) ); + uint n = 1103515245U * ( (q.x) ^ (q.y >> 3U) ); + return float(n) * (1.0 / float(0xffffffffU)); +} + +vec3 XYZtoRGB(vec3 xyz) { + return vec3( + 3.240479f*xyz[0] - 1.537150f*xyz[1] - 0.498535f*xyz[2], + -0.969256f*xyz[0] + 1.875991f*xyz[1] + 0.041556f*xyz[2], + 0.055648f*xyz[0] - 0.204043f*xyz[1] + 1.057311f*xyz[2] + ); +} + +vec3 RGBtoXYZ(vec3 rgb) { + return vec3( + 0.412453f*rgb[0] + 0.357580f*rgb[1] + 0.180423f*rgb[2], + 0.212671f*rgb[0] + 0.715160f*rgb[1] + 0.072169f*rgb[2], + 0.019334f*rgb[0] + 0.119193f*rgb[1] + 0.950227f*rgb[2] + ); +} + +vec3 getUniformSphereSample(in vec2 U) +{ + float z = 1.f - 2.f * U.x; + float r = sqrt(max(0.f, 1.f - z*z)); + float phi = 2.f * PI * U.y; + float x = r * cos(phi); + float y = r * sin(phi); + return vec3(x, y, z); +} + +float SphericalPhi(in vec3 Wl) +{ + float p = atan(Wl.z, Wl.x); + return (p < 0.f) ? p + 2.f * PI : p; +} + +float SphericalTheta(in vec3 Wl) +{ + return acos(clamp(Wl.y, -1.f, 1.f)); +} + +bool SameHemisphere(in vec3 Ww1, in vec3 Ww2) +{ + return (Ww1.z * Ww2.z) > 0.0f; +} + +vec2 getConcentricDiskSample(in vec2 U) +{ + float r, theta; + // Map uniform random numbers to [-1,1]^2 + float sx = 2.0 * U.x - 1.0; + float sy = 2.0 * U.y - 1.0; + // Map square to (r,theta) + // Handle degeneracy at the origin + + if (sx == 0.0 && sy == 0.0) + { + return vec2(0.0f, 0.0f); + } + + if (sx >= -sy) + { + if (sx > sy) + { + // Handle first region of disk + r = sx; + if (sy > 0.0) + theta = sy/r; + else + theta = 8.0f + sy/r; + } + else + { + // Handle second region of disk + r = sy; + theta = 2.0f - sx/r; + } + } + else + { + if (sx <= sy) + { + // Handle third region of disk + r = -sx; + theta = 4.0f - sy/r; + } + else + { + // Handle fourth region of disk + r = -sy; + theta = 6.0f + sx/r; + } + } + + theta *= PI_OVER_4; + + return vec2(r*cos(theta), r*sin(theta)); +} + +vec3 getCosineWeightedHemisphereSample(in vec2 U) +{ + vec2 ret = getConcentricDiskSample(U); + return vec3(ret.x, ret.y, sqrt(max(0.f, 1.f - ret.x * ret.x - ret.y * ret.y))); +} + +struct Ray { + vec3 m_O; + vec3 m_D; + float m_MinT, m_MaxT; +}; + +Ray newRay(in vec3 o, in vec3 d) { + return Ray(o, d, 0.0, 1500000.0); +} + +Ray newRayT(in vec3 o, in vec3 d, in float t0, in float t1) { + return Ray(o, d, t0, t1); +} + +vec3 rayAt(Ray r, float t) { + return r.m_O + t*r.m_D; +} + +Ray GenerateCameraRay(in Camera cam, in vec2 Pixel, in vec2 ApertureRnd) +{ + vec2 ScreenPoint; + + // m_screen: x:left, y:right, z:bottom, w:top + ScreenPoint.x = cam.m_screen.x + (cam.m_invScreen.x * Pixel.x); + ScreenPoint.y = cam.m_screen.z + (cam.m_invScreen.y * Pixel.y); + + vec3 RayO = cam.m_from; + if (cam.m_isPerspective == 0.0) { + RayO += (ScreenPoint.x * cam.m_U) + (ScreenPoint.y * cam.m_V); + } + + vec3 RayD = normalize(cam.m_N + (ScreenPoint.x * cam.m_U) + (ScreenPoint.y * cam.m_V)); + if (cam.m_isPerspective == 0.0) { + RayD = cam.m_N; + } + + if (cam.m_apertureSize != 0.0f) + { + vec2 LensUV = cam.m_apertureSize * getConcentricDiskSample(ApertureRnd); + + vec3 LI = cam.m_U * LensUV.x + cam.m_V * LensUV.y; + RayO += LI; + RayD = normalize((RayD * cam.m_focalDistance) - LI); + } + + return newRay(RayO, RayD); +} + +bool IntersectBox(in Ray R, out float pNearT, out float pFarT) +{ + vec3 invR = vec3(1.0f, 1.0f, 1.0f) / R.m_D; + vec3 bottomT = invR * (vec3(gClippedAaBbMin.x, gClippedAaBbMin.y, gClippedAaBbMin.z) - R.m_O); + vec3 topT = invR * (vec3(gClippedAaBbMax.x, gClippedAaBbMax.y, gClippedAaBbMax.z) - R.m_O); + vec3 minT = min(topT, bottomT); + vec3 maxT = max(topT, bottomT); + float largestMinT = max(max(minT.x, minT.y), max(minT.x, minT.z)); + float smallestMaxT = min(min(maxT.x, maxT.y), min(maxT.x, maxT.z)); + + pNearT = largestMinT; + pFarT = smallestMaxT; + + return smallestMaxT > largestMinT; +} + +vec3 PtoVolumeTex(vec3 p) { + // center of volume is 0.5*extents + // this needs to return a number in 0..1 range, so just rescale to bounds. + return p * gInvAaBbSize; +} + +const float UINT16_MAX = 65535.0; +float GetNormalizedIntensityMax4ch(in vec3 P, out int ch) +{ + vec4 intensity = UINT16_MAX * texture(volumeTexture, PtoVolumeTex(P)); + + float maxIn = 0.0; + ch = 0; + + // relative to min/max for each channel + intensity = (intensity - g_intensityMin) / (g_intensityMax - g_intensityMin); + intensity.x = texture(g_lutTexture[0], vec2(intensity.x, 0.5)).x * pow(g_opacity[0], 4.0); + intensity.y = texture(g_lutTexture[1], vec2(intensity.y, 0.5)).x * pow(g_opacity[1], 4.0); + intensity.z = texture(g_lutTexture[2], vec2(intensity.z, 0.5)).x * pow(g_opacity[2], 4.0); + intensity.w = texture(g_lutTexture[3], vec2(intensity.w, 0.5)).x * pow(g_opacity[3], 4.0); + + // take the high value of the 4 channels + for (int i = 0; i < min(g_nChannels, 4); ++i) { + if (intensity[i] > maxIn) { + maxIn = intensity[i]; + ch = i; + } + } + return maxIn; // *factor; +} + +float GetNormalizedIntensityRnd4ch(in vec3 P, out int ch, inout uvec2 seed) +{ + vec4 intensity = UINT16_MAX * texture(volumeTexture, PtoVolumeTex(P)); + + float maxIn = 0.0; + ch = 0; + + // relative to min/max for each channel + intensity = (intensity - g_intensityMin) / (g_intensityMax - g_intensityMin); + + // take a random value of the 4 channels + // TODO weight this based on the post-LUT 4-channel intensities? + float r = rand(seed)*min(float(g_nChannels), 4.0); + ch = int(r); + + float retval = texture(g_lutTexture[ch], vec2(intensity[ch], 0.5)).x * pow(g_opacity[ch], 4.0); + + return retval; +} +float GetNormalizedIntensityRnd4ch_weighted(in vec3 P, out int ch, inout uvec2 seed) +{ + vec4 intensity = UINT16_MAX * texture(volumeTexture, PtoVolumeTex(P)); + + ch = 0; + + // relative to min/max for each channel + intensity = (intensity - g_intensityMin) / (g_intensityMax - g_intensityMin); + intensity.x = texture(g_lutTexture[0], vec2(intensity.x, 0.5)).x * pow(g_opacity[0], 4.0); + intensity.y = texture(g_lutTexture[1], vec2(intensity.y, 0.5)).x * pow(g_opacity[1], 4.0); + intensity.z = texture(g_lutTexture[2], vec2(intensity.z, 0.5)).x * pow(g_opacity[2], 4.0); + intensity.w = texture(g_lutTexture[3], vec2(intensity.w, 0.5)).x * pow(g_opacity[3], 4.0); + + // ensure 0 for nonexistent channels? + float sum = intensity.x + intensity.y + intensity.z + intensity.w; + // take a random value of the 4 channels + float r = rand(seed)*sum; + float cum = 0; + float retval = 0; + for (int i = 0; i < min(g_nChannels, 4); ++i) { + cum = cum + intensity[i]; + if (r < cum) { + ch = i; + retval = intensity[i]; + break; + } + } + return retval; +} + +float GetNormalizedIntensity(in vec3 P, in int ch) +{ + float intensity = UINT16_MAX * texture(volumeTexture, PtoVolumeTex(P))[ch]; + intensity = (intensity - g_intensityMin[ch]) / (g_intensityMax[ch] - g_intensityMin[ch]); + intensity = texture(g_lutTexture[ch], vec2(intensity, 0.5)).x; + return intensity; +} + +float GetNormalizedIntensity4ch(vec3 P, int ch) +{ + vec4 intensity = UINT16_MAX * texture(volumeTexture, PtoVolumeTex(P)); + // select channel + float intensityf = intensity[ch]; + intensityf = (intensityf - g_intensityMin[ch]) / (g_intensityMax[ch] - g_intensityMin[ch]); + //intensityf = texture(g_lutTexture[ch], vec2(intensityf, 0.5)).x; + + return intensityf; +} + +float GetRawIntensity(vec3 P, int ch) +{ + return texture(volumeTexture, PtoVolumeTex(P))[ch]; +} + +// note that gInvGradientDelta is maxpixeldim of volume +// gGradientDeltaX,Y,Z is 1/X,Y,Z of volume +vec3 Gradient4ch(vec3 P, int ch) +{ + vec3 Gradient; + + Gradient.x = (GetRawIntensity(P + (gGradientDeltaX), ch) - GetRawIntensity(P - (gGradientDeltaX), ch)) * gInvGradientDelta; + Gradient.y = (GetRawIntensity(P + (gGradientDeltaY), ch) - GetRawIntensity(P - (gGradientDeltaY), ch)) * gInvGradientDelta; + Gradient.z = (GetRawIntensity(P + (gGradientDeltaZ), ch) - GetRawIntensity(P - (gGradientDeltaZ), ch)) * gInvGradientDelta; + + //Gradient.x = (GetNormalizedIntensity4ch(P + (gGradientDeltaX), ch) - GetNormalizedIntensity4ch(P - (gGradientDeltaX), ch)) * gInvGradientDelta; + //Gradient.y = (GetNormalizedIntensity4ch(P + (gGradientDeltaY), ch) - GetNormalizedIntensity4ch(P - (gGradientDeltaY), ch)) * gInvGradientDelta; + //Gradient.z = (GetNormalizedIntensity4ch(P + (gGradientDeltaZ), ch) - GetNormalizedIntensity4ch(P - (gGradientDeltaZ), ch)) * gInvGradientDelta; + + return Gradient; +} + + +float GetOpacity(float NormalizedIntensity, int ch) +{ + // apply lut + float Intensity = NormalizedIntensity;// * exp(1.0-1.0/g_opacity[ch]); + return Intensity; +} + +vec3 GetEmissionN(float NormalizedIntensity, int ch) +{ + return g_emissive[ch]; +} + +vec3 GetDiffuseN(float NormalizedIntensity, vec3 Pe, int ch) +{ + vec4 intensity = UINT16_MAX * texture(volumeTexture, PtoVolumeTex(Pe)); + float i = intensity[ch]; + i = (i - g_lutMin[ch]) / (g_lutMax[ch] - g_lutMin[ch]); + + // ideally this is supposed to be + // colormap = texture(g_colormapTexture[ch], vec2(i, 0.5)).xyz; + // but apparently macos + amd graphics can't do this dynamic lookup + // in a sampler2d array. + // so we have to do this instead: + vec3 colormap = vec3(0,0,0); + if (ch == 0) { + colormap = texture(g_colormapTexture[0], vec2(i, 0.5)).xyz; + } + else if (ch == 1) { + colormap = texture(g_colormapTexture[1], vec2(i, 0.5)).xyz; + } + else if (ch == 2) { + colormap = texture(g_colormapTexture[2], vec2(i, 0.5)).xyz; + } + else if (ch == 3) { + colormap = texture(g_colormapTexture[3], vec2(i, 0.5)).xyz; + } + + return colormap * g_diffuse[ch]; +} + +vec3 GetSpecularN(float NormalizedIntensity, int ch) +{ + return g_specular[ch]; +} + +float GetRoughnessN(float NormalizedIntensity, int ch) +{ + return g_roughness[ch]; +} + +// a bsdf sample, a sample on a light source, and a randomly chosen light index +struct CLightingSample { + float m_bsdfComponent; + vec2 m_bsdfDir; + vec2 m_lightPos; + float m_lightComponent; + float m_LightNum; +}; + +CLightingSample LightingSample_LargeStep(inout uvec2 seed) { + return CLightingSample( + rand(seed), + vec2(rand(seed), rand(seed)), + vec2(rand(seed), rand(seed)), + rand(seed), + rand(seed) + ); +} + +// return a color xyz +vec3 Light_Le(in Light light, in vec2 UV) +{ + if (light.m_T == 0) + return RGBtoXYZ(light.m_color) / light.m_area; + + if (light.m_T == 1) + { + if (UV.y > 0.0f) + return RGBtoXYZ(mix(light.m_colorMiddle, light.m_colorTop, abs(UV.y))); + else + return RGBtoXYZ(mix(light.m_colorMiddle, light.m_colorBottom, abs(UV.y))); + } + + return BLACK; +} + +// return a color xyz +vec3 Light_SampleL(in Light light, in vec3 P, out Ray Rl, out float Pdf, in CLightingSample LS) +{ + vec3 L = BLACK; + Pdf = 0.0; + vec3 Ro = vec3(0,0,0), Rd = vec3(0,0,1); + if (light.m_T == 0) + { + Ro = (light.m_P + ((-0.5f + LS.m_lightPos.x) * light.m_width * light.m_U) + ((-0.5f + LS.m_lightPos.y) * light.m_height * light.m_V)); + Rd = normalize(P - Ro); + L = dot(Rd, light.m_N) > 0.0f ? Light_Le(light, vec2(0.0f)) : BLACK; + Pdf = abs(dot(Rd, light.m_N)) > 0.0f ? dot(P-Ro, P-Ro) / (abs(dot(Rd, light.m_N)) * light.m_area) : 0.0f; + } + else if (light.m_T == 1) + { + Ro = light.m_P + light.m_skyRadius * getUniformSphereSample(LS.m_lightPos); + Rd = normalize(P - Ro); + L = Light_Le(light, vec2(1.0f) - 2.0f * LS.m_lightPos); + Pdf = pow(light.m_skyRadius, 2.0f) / light.m_area; + } + + Rl = Ray(Ro, Rd, 0.0f, length(P - Ro)); + + return L; +} + +// Intersect ray with light +bool Light_Intersect(Light light, inout Ray R, out float T, out vec3 L, out float pPdf) +{ + if (light.m_T == 0) + { + // Compute projection + float DotN = dot(R.m_D, light.m_N); + + // Ray is coplanar with light surface + if (DotN >= 0.0f) + return false; + + // Compute hit distance + T = (-light.m_distance - dot(R.m_O, light.m_N)) / DotN; + + // Intersection is in ray's negative direction + if (T < R.m_MinT || T > R.m_MaxT) + return false; + + // Determine position on light + vec3 Pl = rayAt(R, T); + + // Vector from point on area light to center of area light + vec3 Wl = Pl - light.m_P; + + // Compute texture coordinates + vec2 UV = vec2(dot(Wl, light.m_U), dot(Wl, light.m_V)); + + // Check if within bounds of light surface + if (UV.x > light.m_halfWidth || UV.x < -light.m_halfWidth || UV.y > light.m_halfHeight || UV.y < -light.m_halfHeight) + return false; + + R.m_MaxT = T; + + //pUV = UV; + + if (DotN < 0.0f) + L = RGBtoXYZ(light.m_color) / light.m_area; + else + L = BLACK; + + pPdf = dot(R.m_O-Pl, R.m_O-Pl) / (DotN * light.m_area); + + return true; + } + + else if (light.m_T == 1) + { + T = light.m_skyRadius; + + // Intersection is in ray's negative direction + if (T < R.m_MinT || T > R.m_MaxT) + return false; + + R.m_MaxT = T; + + vec2 UV = vec2(SphericalPhi(R.m_D) * INV_2_PI, SphericalTheta(R.m_D) * INV_PI); + + L = Light_Le(light, vec2(1.0f,1.0f) - 2.0f * UV); + + pPdf = pow(light.m_skyRadius, 2.0f) / light.m_area; + //pUV = UV; + + return true; + } + + return false; +} + +float Light_Pdf(in Light light, in vec3 P, in vec3 Wi) +{ + vec3 L; + vec2 UV; + float Pdf = 1.0f; + + Ray Rl = Ray(P, Wi, 0.0f, 100000.0f); + + if (light.m_T == 0) + { + float T = 0.0f; + + if (!Light_Intersect(light, Rl, T, L, Pdf)) + return 0.0f; + + return pow(T, 2.0f) / (abs(dot(light.m_N, -Wi)) * light.m_area); + } + + else if (light.m_T == 1) + { + return pow(light.m_skyRadius, 2.0f) / light.m_area; + } + + return 0.0f; +} + +struct CVolumeShader { + int m_Type; // 0 = bsdf, 1 = phase + + vec3 m_Kd; // isotropic phase // xyz color + vec3 m_R; // specular reflectance + float m_Ior; + float m_Exponent; + vec3 m_Nn; + vec3 m_Nu; + vec3 m_Nv; +}; + +// return a xyz color +vec3 ShaderPhase_F(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + return shader.m_Kd * INV_PI; +} + +float ShaderPhase_Pdf(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + return INV_4_PI; +} + +vec3 ShaderPhase_SampleF(in CVolumeShader shader, in vec3 Wo, out vec3 Wi, out float Pdf, in vec2 U) +{ + Wi = getUniformSphereSample(U); + Pdf = ShaderPhase_Pdf(shader, Wo, Wi); + + return ShaderPhase_F(shader, Wo, Wi); +} + +// return a xyz color +vec3 Lambertian_F(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + return shader.m_Kd * INV_PI; +} + +float Lambertian_Pdf(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + //return abs(Wi.z)*INV_PI; + return SameHemisphere(Wo, Wi) ? abs(Wi.z) * INV_PI : 0.0f; +} + +// return a xyz color +vec3 Lambertian_SampleF(in CVolumeShader shader, in vec3 Wo, out vec3 Wi, out float Pdf, in vec2 U) +{ + Wi = getCosineWeightedHemisphereSample(U); + + if (Wo.z < 0.0f) + Wi.z *= -1.0f; + + Pdf = Lambertian_Pdf(shader, Wo, Wi); + + return Lambertian_F(shader, Wo, Wi); +} + +vec3 SphericalDirection(in float SinTheta, in float CosTheta, in float Phi) +{ + return vec3(SinTheta * cos(Phi), SinTheta * sin(Phi), CosTheta); +} + +void Blinn_SampleF(in CVolumeShader shader, in vec3 Wo, out vec3 Wi, out float Pdf, in vec2 U) +{ + // Compute sampled half-angle vector wh for Blinn distribution + float costheta = pow(U.x, 1.f / (shader.m_Exponent+1.0)); + float sintheta = sqrt(max(0.f, 1.f - costheta*costheta)); + float phi = U.y * 2.f * PI; + + vec3 wh = SphericalDirection(sintheta, costheta, phi); + + if (!SameHemisphere(Wo, wh)) + wh = -wh; + + // Compute incident direction by reflecting about $\wh$ + Wi = -Wo + 2.f * dot(Wo, wh) * wh; + + // Compute PDF for wi from Blinn distribution + float blinn_pdf = ((shader.m_Exponent + 1.f) * pow(costheta, shader.m_Exponent)) / (2.f * PI * 4.f * dot(Wo, wh)); + + if (dot(Wo, wh) <= 0.f) + blinn_pdf = 0.f; + + Pdf = blinn_pdf; +} + +float Blinn_D(in CVolumeShader shader, in vec3 wh) +{ + float costhetah = abs(wh.z);//AbsCosTheta(wh); + return (shader.m_Exponent+2.0) * INV_2_PI * pow(costhetah, shader.m_Exponent); +} +float Microfacet_G(in CVolumeShader shader, in vec3 wo, in vec3 wi, in vec3 wh) +{ + float NdotWh = abs(wh.z);//AbsCosTheta(wh); + float NdotWo = abs(wo.z);//AbsCosTheta(wo); + float NdotWi = abs(wi.z);//AbsCosTheta(wi); + float WOdotWh = abs(dot(wo, wh)); + + return min(1.f, min((2.f * NdotWh * NdotWo / WOdotWh), (2.f * NdotWh * NdotWi / WOdotWh))); +} + +vec3 Microfacet_F(in CVolumeShader shader, in vec3 wo, in vec3 wi) +{ + float cosThetaO = abs(wo.z);//AbsCosTheta(wo); + float cosThetaI = abs(wi.z);//AbsCosTheta(wi); + + if (cosThetaI == 0.f || cosThetaO == 0.f) + return BLACK; + + vec3 wh = wi + wo; + + if (wh.x == 0. && wh.y == 0. && wh.z == 0.) + return BLACK; + + wh = normalize(wh); + float cosThetaH = dot(wi, wh); + + vec3 F = WHITE;//m_Fresnel.Evaluate(cosThetaH); + + return shader.m_R * Blinn_D(shader, wh) * Microfacet_G(shader, wo, wi, wh) * F / (4.f * cosThetaI * cosThetaO); +} + +vec3 ShaderBsdf_WorldToLocal(in CVolumeShader shader, in vec3 W) +{ + return vec3(dot(W, shader.m_Nu), dot(W, shader.m_Nv), dot(W, shader.m_Nn)); +} + +vec3 ShaderBsdf_LocalToWorld(in CVolumeShader shader, in vec3 W) +{ + return vec3( shader.m_Nu.x * W.x + shader.m_Nv.x * W.y + shader.m_Nn.x * W.z, + shader.m_Nu.y * W.x + shader.m_Nv.y * W.y + shader.m_Nn.y * W.z, + shader.m_Nu.z * W.x + shader.m_Nv.z * W.y + shader.m_Nn.z * W.z); +} + +float Blinn_Pdf(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + vec3 wh = normalize(Wo + Wi); + + float costheta = abs(wh.z);//AbsCosTheta(wh); + // Compute PDF for wi from Blinn distribution + float blinn_pdf = ((shader.m_Exponent + 1.f) * pow(costheta, shader.m_Exponent)) / (2.f * PI * 4.f * dot(Wo, wh)); + + if (dot(Wo, wh) <= 0.0f) + blinn_pdf = 0.0f; + + return blinn_pdf; +} + +vec3 Microfacet_SampleF(in CVolumeShader shader, in vec3 wo, out vec3 wi, out float Pdf, in vec2 U) +{ + Blinn_SampleF(shader, wo, wi, Pdf, U); + + if (!SameHemisphere(wo, wi)) + return BLACK; + + return Microfacet_F(shader, wo, wi); +} + +float Microfacet_Pdf(in CVolumeShader shader, in vec3 wo, in vec3 wi) +{ + if (!SameHemisphere(wo, wi)) + return 0.0f; + + return Blinn_Pdf(shader, wo, wi); +} + +// return a xyz color +vec3 ShaderBsdf_F(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + vec3 Wol = ShaderBsdf_WorldToLocal(shader, Wo); + vec3 Wil = ShaderBsdf_WorldToLocal(shader, Wi); + + vec3 R = vec3(0,0,0); + + R += Lambertian_F(shader, Wol, Wil); + R += Microfacet_F(shader, Wol, Wil); + + return R; +} + +float ShaderBsdf_Pdf(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + vec3 Wol = ShaderBsdf_WorldToLocal(shader, Wo); + vec3 Wil = ShaderBsdf_WorldToLocal(shader, Wi); + + float Pdf = 0.0f; + + Pdf += Lambertian_Pdf(shader, Wol, Wil); + Pdf += Microfacet_Pdf(shader, Wol, Wil); + + return Pdf; +} + +vec3 ShaderBsdf_SampleF(in CVolumeShader shader, in CLightingSample S, in vec3 Wo, out vec3 Wi, out float Pdf, in vec2 U) +{ + vec3 Wol = ShaderBsdf_WorldToLocal(shader, Wo); + vec3 Wil = vec3(0,0,0); + + vec3 R = vec3(0,0,0); + + if (S.m_bsdfComponent <= 0.5f) + { + Lambertian_SampleF(shader, Wol, Wil, Pdf, S.m_bsdfDir); + } + else + { + Microfacet_SampleF(shader, Wol, Wil, Pdf, S.m_bsdfDir); + } + + Pdf += Lambertian_Pdf(shader, Wol, Wil); + Pdf += Microfacet_Pdf(shader, Wol, Wil); + + R += Lambertian_F(shader, Wol, Wil); + R += Microfacet_F(shader, Wol, Wil); + + Wi = ShaderBsdf_LocalToWorld(shader, Wil); + + //return vec3(1,1,1); + return R; +} + +// return a xyz color +vec3 Shader_F(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + if (shader.m_Type == 0) { + return ShaderBsdf_F(shader, Wo, Wi); + } + else { + return ShaderPhase_F(shader, Wo, Wi); + } +} + +float Shader_Pdf(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + if (shader.m_Type == 0) { + return ShaderBsdf_Pdf(shader, Wo, Wi); + } + else { + return ShaderPhase_Pdf(shader, Wo, Wi); + } +} + +vec3 Shader_SampleF(in CVolumeShader shader, in CLightingSample S, in vec3 Wo, out vec3 Wi, out float Pdf, in vec2 U) +{ + //return vec3(1,0,0); + if (shader.m_Type == 0) { + return ShaderBsdf_SampleF(shader, S, Wo, Wi, Pdf, U); + } + else { + return ShaderPhase_SampleF(shader, Wo, Wi, Pdf, U); + } +} + +bool IsBlack(in vec3 v) { + return (v.x==0.0 && v.y == 0.0 && v.z == 0.0); +} + +float PowerHeuristic(float nf, float fPdf, float ng, float gPdf) +{ + float f = nf * fPdf; + float g = ng * gPdf; + return (f * f) / (f * f + g * g); +} + +// "shadow ray" using gStepSizeShadow, test whether it can exit the volume or not +bool FreePathRM(inout Ray R, inout uvec2 seed) +{ + float MinT; + float MaxT; + vec3 Ps; + + if (!IntersectBox(R, MinT, MaxT)) + return false; + + MinT = max(MinT, R.m_MinT); + MaxT = min(MaxT, R.m_MaxT); + + float S = -log(rand(seed)) / gDensityScale; + float Sum = 0.0f; + float SigmaT = 0.0f; + + MinT += rand(seed) * gStepSizeShadow; + int ch = 0; + float intensity = 0.0; + while (Sum < S) + { + Ps = rayAt(R, MinT); // R.m_O + MinT * R.m_D; + + if (MinT > MaxT) + return false; + + //intensity = GetNormalizedIntensityRnd4ch_weighted(Ps, ch, seed); + intensity = GetNormalizedIntensityMax4ch(Ps, ch); + SigmaT = gDensityScale * GetOpacity(intensity, ch); + + Sum += SigmaT * gStepSizeShadow; + MinT += gStepSizeShadow; + } + + return true; +} + + +int NearestLight(Ray R, out vec3 LightColor, out vec3 Pl, out float oPdf) +{ + int Hit = -1; + + float T = 0.0f; + + Ray RayCopy = R; + + float Pdf = 0.0f; + + for (int i = 0; i < 2; i++) + { + if (Light_Intersect(gLights[i], RayCopy, T, LightColor, Pdf)) + { + Pl = rayAt(R, T); + Hit = i; + } + } + + oPdf = Pdf; + + return Hit; +} + +// return a XYZ color +vec3 EstimateDirectLight(int shaderType, float Density, int ch, in Light light, in CLightingSample LS, in vec3 Wo, in vec3 Pe, in vec3 N, inout uvec2 seed) +{ + vec3 Ld = BLACK, Li = BLACK, F = BLACK; + + vec3 diffuse = GetDiffuseN(Density, Pe, ch); + vec3 specular = GetSpecularN(Density, ch); + float roughness = GetRoughnessN(Density, ch); + + vec3 nu = normalize(cross(N, Wo)); + vec3 nv = normalize(cross(N, nu)); + CVolumeShader Shader = CVolumeShader(shaderType, RGBtoXYZ(diffuse), RGBtoXYZ(specular), 2.5f, roughness, N, nu, nv); + + float LightPdf = 1.0f, ShaderPdf = 1.0f; + + + Ray Rl = Ray(vec3(0,0,0), vec3(0,0,1.0), 0.0, 1500000.0f); + Li = Light_SampleL(light, Pe, Rl, LightPdf, LS); + + vec3 Wi = -Rl.m_D, P = vec3(0,0,0); + + F = Shader_F(Shader,Wo, Wi); + + ShaderPdf = Shader_Pdf(Shader, Wo, Wi); + + if (!IsBlack(Li) && (ShaderPdf > 0.0f) && (LightPdf > 0.0f) && !FreePathRM(Rl, seed)) + { + float WeightMIS = PowerHeuristic(1.0f, LightPdf, 1.0f, ShaderPdf); + + if (shaderType == ShaderType_Brdf){ + Ld += F * Li * abs(dot(Wi, N)) * WeightMIS / LightPdf; + } + + else if (shaderType == ShaderType_Phase){ + Ld += F * Li * WeightMIS / LightPdf; + } + } + + F = Shader_SampleF(Shader, LS, Wo, Wi, ShaderPdf, LS.m_bsdfDir); + + if (!IsBlack(F) && (ShaderPdf > 0.0f)) + { + vec3 Pl = vec3(0,0,0); + int n = NearestLight(Ray(Pe, Wi, 0.0f, 1000000.0f), Li, Pl, LightPdf); + if (n > -1) + { + Light pLight = gLights[n]; + LightPdf = Light_Pdf(pLight, Pe, Wi); + + if ((LightPdf > 0.0f) && !IsBlack(Li)) { + Ray rr = Ray(Pl, normalize(Pe - Pl), 0.0f, length(Pe - Pl)); + if (!FreePathRM(rr, seed)) + { + float WeightMIS = PowerHeuristic(1.0f, ShaderPdf, 1.0f, LightPdf); + + if (shaderType == ShaderType_Brdf) { + Ld += F * Li * abs(dot(Wi, N)) * WeightMIS / ShaderPdf; + + } + + else if (shaderType == ShaderType_Phase) { + Ld += F * Li * WeightMIS / ShaderPdf; + } + } + + } + } + } + + //return vec3(1,1,1); + return Ld; +} + +// return a linear xyz color +vec3 UniformSampleOneLight(int shaderType, float Density, int ch, in vec3 Wo, in vec3 Pe, in vec3 N, inout uvec2 seed) +{ + //if (NUM_LIGHTS == 0) + // return BLACK; + + // select a random light, a random 2d sample on light, and a random 2d sample on brdf + CLightingSample LS = LightingSample_LargeStep(seed); + + int WhichLight = int(floor(LS.m_LightNum * float(NUM_LIGHTS))); + + Light light = gLights[WhichLight]; + + return float(NUM_LIGHTS) * EstimateDirectLight(shaderType, Density, ch, light, LS, Wo, Pe, N, seed); + +} + +bool SampleDistanceRM(inout Ray R, inout uvec2 seed, out vec3 Ps, out float intensity, out int ch) +{ + float MinT; + float MaxT; + + if (!IntersectBox(R, MinT, MaxT)) + return false; + + MinT = max(MinT, R.m_MinT); + MaxT = min(MaxT, R.m_MaxT); + + // ray march along the ray's projected path and keep an average sigmaT value. + // The distance is weighted by the intensity at each ray step sample. High intensity increases the apparent distance. + // When the distance has become greater than the average sigmaT value given by -log(RandomFloat[0, 1]) / averageSigmaT + // then that would be considered the interaction position. + + // sigmaT = sigmaA + sigmaS = absorption coeff + scattering coeff = extinction coeff + + // Beer-Lambert law: transmittance T(t) = exp(-sigmaT*t) + // importance sampling the exponential function to produce a free path distance S + // the PDF is p(t) = sigmaT * exp(-sigmaT * t) + // S is the free-path distance = -ln(1-zeta)/sigmaT where zeta is a random variable + float S = -log(rand(seed)) / gDensityScale; // note that ln(x:0..1) is negative + + // density scale 0... S --> 0..inf. Low density means randomly sized ray paths + // density scale inf... S --> 0. High density means short ray paths! + float Sum = 0.0f; + float SigmaT = 0.0f; // accumulated extinction along ray march + + MinT += rand(seed) * gStepSize; + //int ch = 0; + //float intensity = 0.0; + // ray march until we have traveled S (or hit the maxT of the ray) + while (Sum < S) + { + Ps = rayAt(R, MinT); // R.m_O + MinT * R.m_D; + + if (MinT > MaxT) + return false; + + //intensity = GetNormalizedIntensityRnd4ch_weighted(Ps, ch, seed); + intensity = GetNormalizedIntensityMax4ch(Ps, ch); + SigmaT = gDensityScale * GetOpacity(intensity, ch); + //SigmaT = gDensityScale * GetBlendedOpacity(volumedata, GetIntensity4ch(Ps, volumedata)); + + Sum += SigmaT * gStepSize; + MinT += gStepSize; + } + + // Ps is the point + return true; +} + +uvec2 Sobol(uint n) { + uvec2 p = uvec2(0u); + uvec2 d = uvec2(0x80000000u); + + for(; n != 0u; n >>= 1u) { + if((n & 1u) != 0u) + p ^= d; + + d.x >>= 1u; // 1st dimension Sobol matrix, is same as base 2 Van der Corput + d.y ^= d.y >> 1u; // 2nd dimension Sobol matrix + } + + return p; +} + +// adapted from: https://www.shadertoy.com/view/3lcczS +uint ReverseBits(uint x) { + x = ((x & 0xaaaaaaaau) >> 1) | ((x & 0x55555555u) << 1); + x = ((x & 0xccccccccu) >> 2) | ((x & 0x33333333u) << 2); + x = ((x & 0xf0f0f0f0u) >> 4) | ((x & 0x0f0f0f0fu) << 4); + x = ((x & 0xff00ff00u) >> 8) | ((x & 0x00ff00ffu) << 8); + return (x >> 16) | (x << 16); + //return bitfieldReverse(x); +} + +// EDIT: updated with a new hash that fixes an issue with the old one. +// details in the post linked at the top. +uint OwenHash(uint x, uint seed) { // works best with random seeds + x ^= x * 0x3d20adeau; + x += seed; + x *= (seed >> 16) | 1u; + x ^= x * 0x05526c56u; + x ^= x * 0x53a22864u; + return x; +} + +uint OwenScramble(uint p, uint seed) { + p = ReverseBits(p); + p = OwenHash(p, seed); + return ReverseBits(p); +} + +vec2 OwenScrambledSobol(uint iter) { + uvec2 ip = Sobol(iter); + ip.x = OwenScramble(ip.x, 0xe7843fbfu); + ip.y = OwenScramble(ip.y, 0x8d8fb1e0u); + return vec2(ip) / float(0xffffffffu); +} + +vec4 CalculateRadiance(inout uvec2 seed) { + float r = rand(seed); + //return vec4(r,0,0,1); + + vec3 Lv = BLACK, Li = BLACK; + + //Ray Re = Ray(vec3(0,0,0), vec3(0,0,1), 0.0, 1500000.0); + //vec2 pixSample = vec2(rand(seed), rand(seed)); + vec2 pixSample = OwenScrambledSobol(uint(uSampleCounter)); + + vec2 UV = vUv*uResolution + pixSample; + + Ray Re = GenerateCameraRay(gCamera, UV, vec2(rand(seed), rand(seed))); + + //return vec4(vUv, 0.0, 1.0); + //return vec4(0.5*(Re.m_D + 1.0), 1.0); + //return vec4(Re.m_D, 1.0); + + //Re.m_MinT = 0.0f; + //Re.m_MaxT = 1500000.0f; + + vec3 Pe = vec3(0,0,0), Pl = vec3(0,0,0); + float lpdf = 0.0; + float alpha = 0.0; + + int ch; + float D; + // find point Pe along ray Re, and get its normalized intensity D and channel ch + if (SampleDistanceRM(Re, seed, Pe, D, ch)) + { + alpha = 1.0; + //return vec4(1.0, 1.0, 1.0, 1.0); + + // is there a light between Re.m_O and Pe? (ray's maxT is distance to Pe) + // (test to see if area light was hit before volume.) + int i = NearestLight(Ray(Re.m_O, Re.m_D, 0.0f, length(Pe - Re.m_O)), Li, Pl, lpdf); + if (i > -1) + { + // set sample pixel value in frame estimate (prior to accumulation) + return vec4(Li, 1.0); + } + + //int ch = 0; + //float D = GetNormalizedIntensityMax4ch(Pe, ch); + + // emission from volume + Lv += RGBtoXYZ(GetEmissionN(D, ch)); + + vec3 gradient = Gradient4ch(Pe, ch); + // send ray out from Pe toward light + switch (gShadingType) + { + case 0: + { + Lv += UniformSampleOneLight(ShaderType_Brdf, D, ch, normalize(-Re.m_D), Pe, normalize(gradient), seed); + break; + } + + case 1: + { + Lv += 0.5f * UniformSampleOneLight(ShaderType_Phase, D, ch, normalize(-Re.m_D), Pe, normalize(gradient), seed); + break; + } + + case 2: + { + //const float GradMag = GradientMagnitude(Pe, volumedata.gradientVolumeTexture[ch]) * (1.0/volumedata.intensityMax[ch]); + float GradMag = length(gradient); + float PdfBrdf = (1.0f - exp(-gGradientFactor * GradMag)); + + vec3 cls; // xyz color + if (rand(seed) < PdfBrdf) { + cls = UniformSampleOneLight(ShaderType_Brdf, D, ch, normalize(-Re.m_D), Pe, normalize(gradient), seed); + } + else { + cls = 0.5f * UniformSampleOneLight(ShaderType_Phase, D, ch, normalize(-Re.m_D), Pe, normalize(gradient), seed); + } + + Lv += cls; + + break; + } + } + } + else + { + // background color: + // set Lv to a selected color based on environment light source? +// if (uShowLights > 0.0) { +// int n = NearestLight(Ray(Re.m_O, Re.m_D, 0.0f, 1000000.0f), Li, Pl, lpdf); +// if (n > -1) +// Lv = Li; +// } + + //Lv = vec3(r,0,0); + + } + + // set sample pixel value in frame estimate (prior to accumulation) + + return vec4(Lv, alpha); +} + +vec4 CumulativeMovingAverage(vec4 A, vec4 Ax, float N) +{ + return A + ((Ax - A) / max((N), 1.0f)); +} + +void main() +{ + // seed for rand(seed) function + uvec2 seed = uvec2(uFrameCounter, uFrameCounter + 1.0) * uvec2(gl_FragCoord); + + // perform path tracing and get resulting pixel color + vec4 pixelColor = CalculateRadiance( seed ); + + vec4 previousColor = texture(tPreviousTexture, vUv); + if (uSampleCounter < 1.0) { + previousColor = vec4(0,0,0,0); + } + + out_FragColor = CumulativeMovingAverage(previousColor, pixelColor, uSampleCounter); +} diff --git a/renderlib_wgpu/shaders/ptVol_vert.wgsl b/renderlib_wgpu/shaders/ptVol_vert.wgsl new file mode 100644 index 00000000..7d2001db --- /dev/null +++ b/renderlib_wgpu/shaders/ptVol_vert.wgsl @@ -0,0 +1,1265 @@ +struct VertexInput { + @location(0) position: vec3, + @location(1) uv: vec2, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) vUv: vec2, +}; + +@vertex +fn vs_main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.clip_position = vec4(in.position, 1.0); + out.vUv = in.uv; + return out; +} + + +const PI = (3.1415926535897932384626433832795); +const PI_OVER_2 = (1.57079632679489661923); +const PI_OVER_4 = (0.785398163397448309616); +const INV_PI = (1.0 / PI); +const INV_2_PI = (0.5 / PI); +const INV_4_PI = (0.25 / PI); + +const BLACK = vec3(0, 0, 0); +const WHITE = vec3(1.0, 1.0, 1.0); +const ShaderType_Brdf:i32 = 0; +const ShaderType_Phase:i32 = 1; + +struct Camera { + m_from: vec3; + m_U: vec3; m_V: vec3; m_N: vec3; + m_screen: vec4; // left, right, bottom, top + m_invScreen: vec2; // 1/w, 1/h + m_focalDistance: f32; + m_apertureSize: f32; + m_isPerspective: f32; +}; + +@group(1) @binding(0) // 1. +var gCamera: Camera; + +struct Light { + m_theta: f32; + m_phi: f32; + m_width: f32; + m_halfWidth: f32; + m_height: f32; + m_halfHeight: f32; + m_distance: f32; + m_skyRadius: f32; + m_P: vec3; + m_target: vec3; + m_N: vec3; + m_U: vec3; + m_V: vec3; + m_area: f32; + m_areaPdf: f32; + m_color: vec3; + m_colorTop: vec3; + m_colorMiddle: vec3; + m_colorBottom: vec3; + m_T: i32; +}; +const NUM_LIGHTS:i32 = 2; +@group(1) @binding(1) // 1. +var gLights: Light[2]; + +struct VolumeShading { + gClippedAaBbMin: vec3; + gClippedAaBbMax: vec3; + gDensityScale: f32; + gStepSize: f32; + gStepSizeShadow: f32; + gInvAaBbSize: vec3; + g_nChannels: i32; + gShadingType: i32; + gGradientDeltaX: vec3; + gGradientDeltaY: vec3; + gGradientDeltaZ: vec3; + gInvGradientDelta: f32; + gGradientFactor: f32; + uShowLights: f32; +} +@group(1) @binding(2) // 1. +var volumeShading: VolumeShading; + +uniform sampler3D volumeTexture; + +// per channel +uniform sampler2D g_lutTexture[4]; +uniform sampler2D g_colormapTexture[4]; + +struct VolumeMaterial4ch { + g_intensityMax: vec4; + g_intensityMin: vec4; + g_lutMax: vec4; + g_lutMin: vec4; + g_opacity: array; + g_emissive: array; + g_diffuse: array; + g_specular: array; + g_roughness: array; +} +@group(1) @binding(3) // 1. +var volumeMaterial: VolumeMaterial4ch; + +// compositing / progressive render +struct RenderSettings { + uFrameCounter: f32; + uSampleCounter: f32; + uResolution: vec2; +} +@group(1) @binding(4) // 1. +var renderSettings: RenderSettings; +// tPreviousTexture: texture_2d; +uniform sampler2D tPreviousTexture; + +// from iq https://www.shadertoy.com/view/4tXyWN +float rand( inout uvec2 seed ) +{ + seed += uvec2(1); + uvec2 q = 1103515245U * ( (seed >> 1U) ^ (seed.yx) ); + uint n = 1103515245U * ( (q.x) ^ (q.y >> 3U) ); + return float(n) * (1.0 / float(0xffffffffU)); +} + +vec3 XYZtoRGB(vec3 xyz) { + return vec3( + 3.240479f*xyz[0] - 1.537150f*xyz[1] - 0.498535f*xyz[2], + -0.969256f*xyz[0] + 1.875991f*xyz[1] + 0.041556f*xyz[2], + 0.055648f*xyz[0] - 0.204043f*xyz[1] + 1.057311f*xyz[2] + ); +} + +vec3 RGBtoXYZ(vec3 rgb) { + return vec3( + 0.412453f*rgb[0] + 0.357580f*rgb[1] + 0.180423f*rgb[2], + 0.212671f*rgb[0] + 0.715160f*rgb[1] + 0.072169f*rgb[2], + 0.019334f*rgb[0] + 0.119193f*rgb[1] + 0.950227f*rgb[2] + ); +} + +vec3 getUniformSphereSample(in vec2 U) +{ + float z = 1.f - 2.f * U.x; + float r = sqrt(max(0.f, 1.f - z*z)); + float phi = 2.f * PI * U.y; + float x = r * cos(phi); + float y = r * sin(phi); + return vec3(x, y, z); +} + +float SphericalPhi(in vec3 Wl) +{ + float p = atan(Wl.z, Wl.x); + return (p < 0.f) ? p + 2.f * PI : p; +} + +float SphericalTheta(in vec3 Wl) +{ + return acos(clamp(Wl.y, -1.f, 1.f)); +} + +bool SameHemisphere(in vec3 Ww1, in vec3 Ww2) +{ + return (Ww1.z * Ww2.z) > 0.0f; +} + +vec2 getConcentricDiskSample(in vec2 U) +{ + float r, theta; + // Map uniform random numbers to [-1,1]^2 + float sx = 2.0 * U.x - 1.0; + float sy = 2.0 * U.y - 1.0; + // Map square to (r,theta) + // Handle degeneracy at the origin + + if (sx == 0.0 && sy == 0.0) + { + return vec2(0.0f, 0.0f); + } + + if (sx >= -sy) + { + if (sx > sy) + { + // Handle first region of disk + r = sx; + if (sy > 0.0) + theta = sy/r; + else + theta = 8.0f + sy/r; + } + else + { + // Handle second region of disk + r = sy; + theta = 2.0f - sx/r; + } + } + else + { + if (sx <= sy) + { + // Handle third region of disk + r = -sx; + theta = 4.0f - sy/r; + } + else + { + // Handle fourth region of disk + r = -sy; + theta = 6.0f + sx/r; + } + } + + theta *= PI_OVER_4; + + return vec2(r*cos(theta), r*sin(theta)); +} + +vec3 getCosineWeightedHemisphereSample(in vec2 U) +{ + vec2 ret = getConcentricDiskSample(U); + return vec3(ret.x, ret.y, sqrt(max(0.f, 1.f - ret.x * ret.x - ret.y * ret.y))); +} + +struct Ray { + vec3 m_O; + vec3 m_D; + float m_MinT, m_MaxT; +}; + +Ray newRay(in vec3 o, in vec3 d) { + return Ray(o, d, 0.0, 1500000.0); +} + +Ray newRayT(in vec3 o, in vec3 d, in float t0, in float t1) { + return Ray(o, d, t0, t1); +} + +vec3 rayAt(Ray r, float t) { + return r.m_O + t*r.m_D; +} + +Ray GenerateCameraRay(in Camera cam, in vec2 Pixel, in vec2 ApertureRnd) +{ + vec2 ScreenPoint; + + // m_screen: x:left, y:right, z:bottom, w:top + ScreenPoint.x = cam.m_screen.x + (cam.m_invScreen.x * Pixel.x); + ScreenPoint.y = cam.m_screen.z + (cam.m_invScreen.y * Pixel.y); + + vec3 RayO = cam.m_from; + if (cam.m_isPerspective == 0.0) { + RayO += (ScreenPoint.x * cam.m_U) + (ScreenPoint.y * cam.m_V); + } + + vec3 RayD = normalize(cam.m_N + (ScreenPoint.x * cam.m_U) + (ScreenPoint.y * cam.m_V)); + if (cam.m_isPerspective == 0.0) { + RayD = cam.m_N; + } + + if (cam.m_apertureSize != 0.0f) + { + vec2 LensUV = cam.m_apertureSize * getConcentricDiskSample(ApertureRnd); + + vec3 LI = cam.m_U * LensUV.x + cam.m_V * LensUV.y; + RayO += LI; + RayD = normalize((RayD * cam.m_focalDistance) - LI); + } + + return newRay(RayO, RayD); +} + +bool IntersectBox(in Ray R, out float pNearT, out float pFarT) +{ + vec3 invR = vec3(1.0f, 1.0f, 1.0f) / R.m_D; + vec3 bottomT = invR * (vec3(gClippedAaBbMin.x, gClippedAaBbMin.y, gClippedAaBbMin.z) - R.m_O); + vec3 topT = invR * (vec3(gClippedAaBbMax.x, gClippedAaBbMax.y, gClippedAaBbMax.z) - R.m_O); + vec3 minT = min(topT, bottomT); + vec3 maxT = max(topT, bottomT); + float largestMinT = max(max(minT.x, minT.y), max(minT.x, minT.z)); + float smallestMaxT = min(min(maxT.x, maxT.y), min(maxT.x, maxT.z)); + + pNearT = largestMinT; + pFarT = smallestMaxT; + + return smallestMaxT > largestMinT; +} + +vec3 PtoVolumeTex(vec3 p) { + // center of volume is 0.5*extents + // this needs to return a number in 0..1 range, so just rescale to bounds. + return p * gInvAaBbSize; +} + +const float UINT16_MAX = 65535.0; +float GetNormalizedIntensityMax4ch(in vec3 P, out int ch) +{ + vec4 intensity = UINT16_MAX * texture(volumeTexture, PtoVolumeTex(P)); + + float maxIn = 0.0; + ch = 0; + + // relative to min/max for each channel + intensity = (intensity - g_intensityMin) / (g_intensityMax - g_intensityMin); + intensity.x = texture(g_lutTexture[0], vec2(intensity.x, 0.5)).x * pow(g_opacity[0], 4.0); + intensity.y = texture(g_lutTexture[1], vec2(intensity.y, 0.5)).x * pow(g_opacity[1], 4.0); + intensity.z = texture(g_lutTexture[2], vec2(intensity.z, 0.5)).x * pow(g_opacity[2], 4.0); + intensity.w = texture(g_lutTexture[3], vec2(intensity.w, 0.5)).x * pow(g_opacity[3], 4.0); + + // take the high value of the 4 channels + for (int i = 0; i < min(g_nChannels, 4); ++i) { + if (intensity[i] > maxIn) { + maxIn = intensity[i]; + ch = i; + } + } + return maxIn; // *factor; +} + +float GetNormalizedIntensityRnd4ch(in vec3 P, out int ch, inout uvec2 seed) +{ + vec4 intensity = UINT16_MAX * texture(volumeTexture, PtoVolumeTex(P)); + + float maxIn = 0.0; + ch = 0; + + // relative to min/max for each channel + intensity = (intensity - g_intensityMin) / (g_intensityMax - g_intensityMin); + + // take a random value of the 4 channels + // TODO weight this based on the post-LUT 4-channel intensities? + float r = rand(seed)*min(float(g_nChannels), 4.0); + ch = int(r); + + float retval = texture(g_lutTexture[ch], vec2(intensity[ch], 0.5)).x * pow(g_opacity[ch], 4.0); + + return retval; +} +float GetNormalizedIntensityRnd4ch_weighted(in vec3 P, out int ch, inout uvec2 seed) +{ + vec4 intensity = UINT16_MAX * texture(volumeTexture, PtoVolumeTex(P)); + + ch = 0; + + // relative to min/max for each channel + intensity = (intensity - g_intensityMin) / (g_intensityMax - g_intensityMin); + intensity.x = texture(g_lutTexture[0], vec2(intensity.x, 0.5)).x * pow(g_opacity[0], 4.0); + intensity.y = texture(g_lutTexture[1], vec2(intensity.y, 0.5)).x * pow(g_opacity[1], 4.0); + intensity.z = texture(g_lutTexture[2], vec2(intensity.z, 0.5)).x * pow(g_opacity[2], 4.0); + intensity.w = texture(g_lutTexture[3], vec2(intensity.w, 0.5)).x * pow(g_opacity[3], 4.0); + + // ensure 0 for nonexistent channels? + float sum = intensity.x + intensity.y + intensity.z + intensity.w; + // take a random value of the 4 channels + float r = rand(seed)*sum; + float cum = 0; + float retval = 0; + for (int i = 0; i < min(g_nChannels, 4); ++i) { + cum = cum + intensity[i]; + if (r < cum) { + ch = i; + retval = intensity[i]; + break; + } + } + return retval; +} + +float GetNormalizedIntensity(in vec3 P, in int ch) +{ + float intensity = UINT16_MAX * texture(volumeTexture, PtoVolumeTex(P))[ch]; + intensity = (intensity - g_intensityMin[ch]) / (g_intensityMax[ch] - g_intensityMin[ch]); + intensity = texture(g_lutTexture[ch], vec2(intensity, 0.5)).x; + return intensity; +} + +float GetNormalizedIntensity4ch(vec3 P, int ch) +{ + vec4 intensity = UINT16_MAX * texture(volumeTexture, PtoVolumeTex(P)); + // select channel + float intensityf = intensity[ch]; + intensityf = (intensityf - g_intensityMin[ch]) / (g_intensityMax[ch] - g_intensityMin[ch]); + //intensityf = texture(g_lutTexture[ch], vec2(intensityf, 0.5)).x; + + return intensityf; +} + +float GetRawIntensity(vec3 P, int ch) +{ + return texture(volumeTexture, PtoVolumeTex(P))[ch]; +} + +// note that gInvGradientDelta is maxpixeldim of volume +// gGradientDeltaX,Y,Z is 1/X,Y,Z of volume +vec3 Gradient4ch(vec3 P, int ch) +{ + vec3 Gradient; + + Gradient.x = (GetRawIntensity(P + (gGradientDeltaX), ch) - GetRawIntensity(P - (gGradientDeltaX), ch)) * gInvGradientDelta; + Gradient.y = (GetRawIntensity(P + (gGradientDeltaY), ch) - GetRawIntensity(P - (gGradientDeltaY), ch)) * gInvGradientDelta; + Gradient.z = (GetRawIntensity(P + (gGradientDeltaZ), ch) - GetRawIntensity(P - (gGradientDeltaZ), ch)) * gInvGradientDelta; + + //Gradient.x = (GetNormalizedIntensity4ch(P + (gGradientDeltaX), ch) - GetNormalizedIntensity4ch(P - (gGradientDeltaX), ch)) * gInvGradientDelta; + //Gradient.y = (GetNormalizedIntensity4ch(P + (gGradientDeltaY), ch) - GetNormalizedIntensity4ch(P - (gGradientDeltaY), ch)) * gInvGradientDelta; + //Gradient.z = (GetNormalizedIntensity4ch(P + (gGradientDeltaZ), ch) - GetNormalizedIntensity4ch(P - (gGradientDeltaZ), ch)) * gInvGradientDelta; + + return Gradient; +} + + +float GetOpacity(float NormalizedIntensity, int ch) +{ + // apply lut + float Intensity = NormalizedIntensity;// * exp(1.0-1.0/g_opacity[ch]); + return Intensity; +} + +vec3 GetEmissionN(float NormalizedIntensity, int ch) +{ + return g_emissive[ch]; +} + +vec3 GetDiffuseN(float NormalizedIntensity, vec3 Pe, int ch) +{ + vec4 intensity = UINT16_MAX * texture(volumeTexture, PtoVolumeTex(Pe)); + float i = intensity[ch]; + i = (i - g_lutMin[ch]) / (g_lutMax[ch] - g_lutMin[ch]); + + // ideally this is supposed to be + // colormap = texture(g_colormapTexture[ch], vec2(i, 0.5)).xyz; + // but apparently macos + amd graphics can't do this dynamic lookup + // in a sampler2d array. + // so we have to do this instead: + vec3 colormap = vec3(0,0,0); + if (ch == 0) { + colormap = texture(g_colormapTexture[0], vec2(i, 0.5)).xyz; + } + else if (ch == 1) { + colormap = texture(g_colormapTexture[1], vec2(i, 0.5)).xyz; + } + else if (ch == 2) { + colormap = texture(g_colormapTexture[2], vec2(i, 0.5)).xyz; + } + else if (ch == 3) { + colormap = texture(g_colormapTexture[3], vec2(i, 0.5)).xyz; + } + + return colormap * g_diffuse[ch]; +} + +vec3 GetSpecularN(float NormalizedIntensity, int ch) +{ + return g_specular[ch]; +} + +float GetRoughnessN(float NormalizedIntensity, int ch) +{ + return g_roughness[ch]; +} + +// a bsdf sample, a sample on a light source, and a randomly chosen light index +struct CLightingSample { + float m_bsdfComponent; + vec2 m_bsdfDir; + vec2 m_lightPos; + float m_lightComponent; + float m_LightNum; +}; + +CLightingSample LightingSample_LargeStep(inout uvec2 seed) { + return CLightingSample( + rand(seed), + vec2(rand(seed), rand(seed)), + vec2(rand(seed), rand(seed)), + rand(seed), + rand(seed) + ); +} + +// return a color xyz +vec3 Light_Le(in Light light, in vec2 UV) +{ + if (light.m_T == 0) + return RGBtoXYZ(light.m_color) / light.m_area; + + if (light.m_T == 1) + { + if (UV.y > 0.0f) + return RGBtoXYZ(mix(light.m_colorMiddle, light.m_colorTop, abs(UV.y))); + else + return RGBtoXYZ(mix(light.m_colorMiddle, light.m_colorBottom, abs(UV.y))); + } + + return BLACK; +} + +// return a color xyz +vec3 Light_SampleL(in Light light, in vec3 P, out Ray Rl, out float Pdf, in CLightingSample LS) +{ + vec3 L = BLACK; + Pdf = 0.0; + vec3 Ro = vec3(0,0,0), Rd = vec3(0,0,1); + if (light.m_T == 0) + { + Ro = (light.m_P + ((-0.5f + LS.m_lightPos.x) * light.m_width * light.m_U) + ((-0.5f + LS.m_lightPos.y) * light.m_height * light.m_V)); + Rd = normalize(P - Ro); + L = dot(Rd, light.m_N) > 0.0f ? Light_Le(light, vec2(0.0f)) : BLACK; + Pdf = abs(dot(Rd, light.m_N)) > 0.0f ? dot(P-Ro, P-Ro) / (abs(dot(Rd, light.m_N)) * light.m_area) : 0.0f; + } + else if (light.m_T == 1) + { + Ro = light.m_P + light.m_skyRadius * getUniformSphereSample(LS.m_lightPos); + Rd = normalize(P - Ro); + L = Light_Le(light, vec2(1.0f) - 2.0f * LS.m_lightPos); + Pdf = pow(light.m_skyRadius, 2.0f) / light.m_area; + } + + Rl = Ray(Ro, Rd, 0.0f, length(P - Ro)); + + return L; +} + +// Intersect ray with light +bool Light_Intersect(Light light, inout Ray R, out float T, out vec3 L, out float pPdf) +{ + if (light.m_T == 0) + { + // Compute projection + float DotN = dot(R.m_D, light.m_N); + + // Ray is coplanar with light surface + if (DotN >= 0.0f) + return false; + + // Compute hit distance + T = (-light.m_distance - dot(R.m_O, light.m_N)) / DotN; + + // Intersection is in ray's negative direction + if (T < R.m_MinT || T > R.m_MaxT) + return false; + + // Determine position on light + vec3 Pl = rayAt(R, T); + + // Vector from point on area light to center of area light + vec3 Wl = Pl - light.m_P; + + // Compute texture coordinates + vec2 UV = vec2(dot(Wl, light.m_U), dot(Wl, light.m_V)); + + // Check if within bounds of light surface + if (UV.x > light.m_halfWidth || UV.x < -light.m_halfWidth || UV.y > light.m_halfHeight || UV.y < -light.m_halfHeight) + return false; + + R.m_MaxT = T; + + //pUV = UV; + + if (DotN < 0.0f) + L = RGBtoXYZ(light.m_color) / light.m_area; + else + L = BLACK; + + pPdf = dot(R.m_O-Pl, R.m_O-Pl) / (DotN * light.m_area); + + return true; + } + + else if (light.m_T == 1) + { + T = light.m_skyRadius; + + // Intersection is in ray's negative direction + if (T < R.m_MinT || T > R.m_MaxT) + return false; + + R.m_MaxT = T; + + vec2 UV = vec2(SphericalPhi(R.m_D) * INV_2_PI, SphericalTheta(R.m_D) * INV_PI); + + L = Light_Le(light, vec2(1.0f,1.0f) - 2.0f * UV); + + pPdf = pow(light.m_skyRadius, 2.0f) / light.m_area; + //pUV = UV; + + return true; + } + + return false; +} + +float Light_Pdf(in Light light, in vec3 P, in vec3 Wi) +{ + vec3 L; + vec2 UV; + float Pdf = 1.0f; + + Ray Rl = Ray(P, Wi, 0.0f, 100000.0f); + + if (light.m_T == 0) + { + float T = 0.0f; + + if (!Light_Intersect(light, Rl, T, L, Pdf)) + return 0.0f; + + return pow(T, 2.0f) / (abs(dot(light.m_N, -Wi)) * light.m_area); + } + + else if (light.m_T == 1) + { + return pow(light.m_skyRadius, 2.0f) / light.m_area; + } + + return 0.0f; +} + +struct CVolumeShader { + int m_Type; // 0 = bsdf, 1 = phase + + vec3 m_Kd; // isotropic phase // xyz color + vec3 m_R; // specular reflectance + float m_Ior; + float m_Exponent; + vec3 m_Nn; + vec3 m_Nu; + vec3 m_Nv; +}; + +// return a xyz color +vec3 ShaderPhase_F(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + return shader.m_Kd * INV_PI; +} + +float ShaderPhase_Pdf(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + return INV_4_PI; +} + +vec3 ShaderPhase_SampleF(in CVolumeShader shader, in vec3 Wo, out vec3 Wi, out float Pdf, in vec2 U) +{ + Wi = getUniformSphereSample(U); + Pdf = ShaderPhase_Pdf(shader, Wo, Wi); + + return ShaderPhase_F(shader, Wo, Wi); +} + +// return a xyz color +vec3 Lambertian_F(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + return shader.m_Kd * INV_PI; +} + +float Lambertian_Pdf(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + //return abs(Wi.z)*INV_PI; + return SameHemisphere(Wo, Wi) ? abs(Wi.z) * INV_PI : 0.0f; +} + +// return a xyz color +vec3 Lambertian_SampleF(in CVolumeShader shader, in vec3 Wo, out vec3 Wi, out float Pdf, in vec2 U) +{ + Wi = getCosineWeightedHemisphereSample(U); + + if (Wo.z < 0.0f) + Wi.z *= -1.0f; + + Pdf = Lambertian_Pdf(shader, Wo, Wi); + + return Lambertian_F(shader, Wo, Wi); +} + +vec3 SphericalDirection(in float SinTheta, in float CosTheta, in float Phi) +{ + return vec3(SinTheta * cos(Phi), SinTheta * sin(Phi), CosTheta); +} + +void Blinn_SampleF(in CVolumeShader shader, in vec3 Wo, out vec3 Wi, out float Pdf, in vec2 U) +{ + // Compute sampled half-angle vector wh for Blinn distribution + float costheta = pow(U.x, 1.f / (shader.m_Exponent+1.0)); + float sintheta = sqrt(max(0.f, 1.f - costheta*costheta)); + float phi = U.y * 2.f * PI; + + vec3 wh = SphericalDirection(sintheta, costheta, phi); + + if (!SameHemisphere(Wo, wh)) + wh = -wh; + + // Compute incident direction by reflecting about $\wh$ + Wi = -Wo + 2.f * dot(Wo, wh) * wh; + + // Compute PDF for wi from Blinn distribution + float blinn_pdf = ((shader.m_Exponent + 1.f) * pow(costheta, shader.m_Exponent)) / (2.f * PI * 4.f * dot(Wo, wh)); + + if (dot(Wo, wh) <= 0.f) + blinn_pdf = 0.f; + + Pdf = blinn_pdf; +} + +float Blinn_D(in CVolumeShader shader, in vec3 wh) +{ + float costhetah = abs(wh.z);//AbsCosTheta(wh); + return (shader.m_Exponent+2.0) * INV_2_PI * pow(costhetah, shader.m_Exponent); +} +float Microfacet_G(in CVolumeShader shader, in vec3 wo, in vec3 wi, in vec3 wh) +{ + float NdotWh = abs(wh.z);//AbsCosTheta(wh); + float NdotWo = abs(wo.z);//AbsCosTheta(wo); + float NdotWi = abs(wi.z);//AbsCosTheta(wi); + float WOdotWh = abs(dot(wo, wh)); + + return min(1.f, min((2.f * NdotWh * NdotWo / WOdotWh), (2.f * NdotWh * NdotWi / WOdotWh))); +} + +vec3 Microfacet_F(in CVolumeShader shader, in vec3 wo, in vec3 wi) +{ + float cosThetaO = abs(wo.z);//AbsCosTheta(wo); + float cosThetaI = abs(wi.z);//AbsCosTheta(wi); + + if (cosThetaI == 0.f || cosThetaO == 0.f) + return BLACK; + + vec3 wh = wi + wo; + + if (wh.x == 0. && wh.y == 0. && wh.z == 0.) + return BLACK; + + wh = normalize(wh); + float cosThetaH = dot(wi, wh); + + vec3 F = WHITE;//m_Fresnel.Evaluate(cosThetaH); + + return shader.m_R * Blinn_D(shader, wh) * Microfacet_G(shader, wo, wi, wh) * F / (4.f * cosThetaI * cosThetaO); +} + +vec3 ShaderBsdf_WorldToLocal(in CVolumeShader shader, in vec3 W) +{ + return vec3(dot(W, shader.m_Nu), dot(W, shader.m_Nv), dot(W, shader.m_Nn)); +} + +vec3 ShaderBsdf_LocalToWorld(in CVolumeShader shader, in vec3 W) +{ + return vec3( shader.m_Nu.x * W.x + shader.m_Nv.x * W.y + shader.m_Nn.x * W.z, + shader.m_Nu.y * W.x + shader.m_Nv.y * W.y + shader.m_Nn.y * W.z, + shader.m_Nu.z * W.x + shader.m_Nv.z * W.y + shader.m_Nn.z * W.z); +} + +float Blinn_Pdf(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + vec3 wh = normalize(Wo + Wi); + + float costheta = abs(wh.z);//AbsCosTheta(wh); + // Compute PDF for wi from Blinn distribution + float blinn_pdf = ((shader.m_Exponent + 1.f) * pow(costheta, shader.m_Exponent)) / (2.f * PI * 4.f * dot(Wo, wh)); + + if (dot(Wo, wh) <= 0.0f) + blinn_pdf = 0.0f; + + return blinn_pdf; +} + +vec3 Microfacet_SampleF(in CVolumeShader shader, in vec3 wo, out vec3 wi, out float Pdf, in vec2 U) +{ + Blinn_SampleF(shader, wo, wi, Pdf, U); + + if (!SameHemisphere(wo, wi)) + return BLACK; + + return Microfacet_F(shader, wo, wi); +} + +float Microfacet_Pdf(in CVolumeShader shader, in vec3 wo, in vec3 wi) +{ + if (!SameHemisphere(wo, wi)) + return 0.0f; + + return Blinn_Pdf(shader, wo, wi); +} + +// return a xyz color +vec3 ShaderBsdf_F(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + vec3 Wol = ShaderBsdf_WorldToLocal(shader, Wo); + vec3 Wil = ShaderBsdf_WorldToLocal(shader, Wi); + + vec3 R = vec3(0,0,0); + + R += Lambertian_F(shader, Wol, Wil); + R += Microfacet_F(shader, Wol, Wil); + + return R; +} + +float ShaderBsdf_Pdf(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + vec3 Wol = ShaderBsdf_WorldToLocal(shader, Wo); + vec3 Wil = ShaderBsdf_WorldToLocal(shader, Wi); + + float Pdf = 0.0f; + + Pdf += Lambertian_Pdf(shader, Wol, Wil); + Pdf += Microfacet_Pdf(shader, Wol, Wil); + + return Pdf; +} + +vec3 ShaderBsdf_SampleF(in CVolumeShader shader, in CLightingSample S, in vec3 Wo, out vec3 Wi, out float Pdf, in vec2 U) +{ + vec3 Wol = ShaderBsdf_WorldToLocal(shader, Wo); + vec3 Wil = vec3(0,0,0); + + vec3 R = vec3(0,0,0); + + if (S.m_bsdfComponent <= 0.5f) + { + Lambertian_SampleF(shader, Wol, Wil, Pdf, S.m_bsdfDir); + } + else + { + Microfacet_SampleF(shader, Wol, Wil, Pdf, S.m_bsdfDir); + } + + Pdf += Lambertian_Pdf(shader, Wol, Wil); + Pdf += Microfacet_Pdf(shader, Wol, Wil); + + R += Lambertian_F(shader, Wol, Wil); + R += Microfacet_F(shader, Wol, Wil); + + Wi = ShaderBsdf_LocalToWorld(shader, Wil); + + //return vec3(1,1,1); + return R; +} + +// return a xyz color +vec3 Shader_F(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + if (shader.m_Type == 0) { + return ShaderBsdf_F(shader, Wo, Wi); + } + else { + return ShaderPhase_F(shader, Wo, Wi); + } +} + +float Shader_Pdf(in CVolumeShader shader, in vec3 Wo, in vec3 Wi) +{ + if (shader.m_Type == 0) { + return ShaderBsdf_Pdf(shader, Wo, Wi); + } + else { + return ShaderPhase_Pdf(shader, Wo, Wi); + } +} + +vec3 Shader_SampleF(in CVolumeShader shader, in CLightingSample S, in vec3 Wo, out vec3 Wi, out float Pdf, in vec2 U) +{ + //return vec3(1,0,0); + if (shader.m_Type == 0) { + return ShaderBsdf_SampleF(shader, S, Wo, Wi, Pdf, U); + } + else { + return ShaderPhase_SampleF(shader, Wo, Wi, Pdf, U); + } +} + +bool IsBlack(in vec3 v) { + return (v.x==0.0 && v.y == 0.0 && v.z == 0.0); +} + +float PowerHeuristic(float nf, float fPdf, float ng, float gPdf) +{ + float f = nf * fPdf; + float g = ng * gPdf; + return (f * f) / (f * f + g * g); +} + +// "shadow ray" using gStepSizeShadow, test whether it can exit the volume or not +bool FreePathRM(inout Ray R, inout uvec2 seed) +{ + float MinT; + float MaxT; + vec3 Ps; + + if (!IntersectBox(R, MinT, MaxT)) + return false; + + MinT = max(MinT, R.m_MinT); + MaxT = min(MaxT, R.m_MaxT); + + float S = -log(rand(seed)) / gDensityScale; + float Sum = 0.0f; + float SigmaT = 0.0f; + + MinT += rand(seed) * gStepSizeShadow; + int ch = 0; + float intensity = 0.0; + while (Sum < S) + { + Ps = rayAt(R, MinT); // R.m_O + MinT * R.m_D; + + if (MinT > MaxT) + return false; + + //intensity = GetNormalizedIntensityRnd4ch_weighted(Ps, ch, seed); + intensity = GetNormalizedIntensityMax4ch(Ps, ch); + SigmaT = gDensityScale * GetOpacity(intensity, ch); + + Sum += SigmaT * gStepSizeShadow; + MinT += gStepSizeShadow; + } + + return true; +} + + +int NearestLight(Ray R, out vec3 LightColor, out vec3 Pl, out float oPdf) +{ + int Hit = -1; + + float T = 0.0f; + + Ray RayCopy = R; + + float Pdf = 0.0f; + + for (int i = 0; i < 2; i++) + { + if (Light_Intersect(gLights[i], RayCopy, T, LightColor, Pdf)) + { + Pl = rayAt(R, T); + Hit = i; + } + } + + oPdf = Pdf; + + return Hit; +} + +// return a XYZ color +vec3 EstimateDirectLight(int shaderType, float Density, int ch, in Light light, in CLightingSample LS, in vec3 Wo, in vec3 Pe, in vec3 N, inout uvec2 seed) +{ + vec3 Ld = BLACK, Li = BLACK, F = BLACK; + + vec3 diffuse = GetDiffuseN(Density, Pe, ch); + vec3 specular = GetSpecularN(Density, ch); + float roughness = GetRoughnessN(Density, ch); + + vec3 nu = normalize(cross(N, Wo)); + vec3 nv = normalize(cross(N, nu)); + CVolumeShader Shader = CVolumeShader(shaderType, RGBtoXYZ(diffuse), RGBtoXYZ(specular), 2.5f, roughness, N, nu, nv); + + float LightPdf = 1.0f, ShaderPdf = 1.0f; + + + Ray Rl = Ray(vec3(0,0,0), vec3(0,0,1.0), 0.0, 1500000.0f); + Li = Light_SampleL(light, Pe, Rl, LightPdf, LS); + + vec3 Wi = -Rl.m_D, P = vec3(0,0,0); + + F = Shader_F(Shader,Wo, Wi); + + ShaderPdf = Shader_Pdf(Shader, Wo, Wi); + + if (!IsBlack(Li) && (ShaderPdf > 0.0f) && (LightPdf > 0.0f) && !FreePathRM(Rl, seed)) + { + float WeightMIS = PowerHeuristic(1.0f, LightPdf, 1.0f, ShaderPdf); + + if (shaderType == ShaderType_Brdf){ + Ld += F * Li * abs(dot(Wi, N)) * WeightMIS / LightPdf; + } + + else if (shaderType == ShaderType_Phase){ + Ld += F * Li * WeightMIS / LightPdf; + } + } + + F = Shader_SampleF(Shader, LS, Wo, Wi, ShaderPdf, LS.m_bsdfDir); + + if (!IsBlack(F) && (ShaderPdf > 0.0f)) + { + vec3 Pl = vec3(0,0,0); + int n = NearestLight(Ray(Pe, Wi, 0.0f, 1000000.0f), Li, Pl, LightPdf); + if (n > -1) + { + Light pLight = gLights[n]; + LightPdf = Light_Pdf(pLight, Pe, Wi); + + if ((LightPdf > 0.0f) && !IsBlack(Li)) { + Ray rr = Ray(Pl, normalize(Pe - Pl), 0.0f, length(Pe - Pl)); + if (!FreePathRM(rr, seed)) + { + float WeightMIS = PowerHeuristic(1.0f, ShaderPdf, 1.0f, LightPdf); + + if (shaderType == ShaderType_Brdf) { + Ld += F * Li * abs(dot(Wi, N)) * WeightMIS / ShaderPdf; + + } + + else if (shaderType == ShaderType_Phase) { + Ld += F * Li * WeightMIS / ShaderPdf; + } + } + + } + } + } + + //return vec3(1,1,1); + return Ld; +} + +// return a linear xyz color +vec3 UniformSampleOneLight(int shaderType, float Density, int ch, in vec3 Wo, in vec3 Pe, in vec3 N, inout uvec2 seed) +{ + //if (NUM_LIGHTS == 0) + // return BLACK; + + // select a random light, a random 2d sample on light, and a random 2d sample on brdf + CLightingSample LS = LightingSample_LargeStep(seed); + + int WhichLight = int(floor(LS.m_LightNum * float(NUM_LIGHTS))); + + Light light = gLights[WhichLight]; + + return float(NUM_LIGHTS) * EstimateDirectLight(shaderType, Density, ch, light, LS, Wo, Pe, N, seed); + +} + +bool SampleDistanceRM(inout Ray R, inout uvec2 seed, out vec3 Ps, out float intensity, out int ch) +{ + float MinT; + float MaxT; + + if (!IntersectBox(R, MinT, MaxT)) + return false; + + MinT = max(MinT, R.m_MinT); + MaxT = min(MaxT, R.m_MaxT); + + // ray march along the ray's projected path and keep an average sigmaT value. + // The distance is weighted by the intensity at each ray step sample. High intensity increases the apparent distance. + // When the distance has become greater than the average sigmaT value given by -log(RandomFloat[0, 1]) / averageSigmaT + // then that would be considered the interaction position. + + // sigmaT = sigmaA + sigmaS = absorption coeff + scattering coeff = extinction coeff + + // Beer-Lambert law: transmittance T(t) = exp(-sigmaT*t) + // importance sampling the exponential function to produce a free path distance S + // the PDF is p(t) = sigmaT * exp(-sigmaT * t) + // S is the free-path distance = -ln(1-zeta)/sigmaT where zeta is a random variable + float S = -log(rand(seed)) / gDensityScale; // note that ln(x:0..1) is negative + + // density scale 0... S --> 0..inf. Low density means randomly sized ray paths + // density scale inf... S --> 0. High density means short ray paths! + float Sum = 0.0f; + float SigmaT = 0.0f; // accumulated extinction along ray march + + MinT += rand(seed) * gStepSize; + //int ch = 0; + //float intensity = 0.0; + // ray march until we have traveled S (or hit the maxT of the ray) + while (Sum < S) + { + Ps = rayAt(R, MinT); // R.m_O + MinT * R.m_D; + + if (MinT > MaxT) + return false; + + //intensity = GetNormalizedIntensityRnd4ch_weighted(Ps, ch, seed); + intensity = GetNormalizedIntensityMax4ch(Ps, ch); + SigmaT = gDensityScale * GetOpacity(intensity, ch); + //SigmaT = gDensityScale * GetBlendedOpacity(volumedata, GetIntensity4ch(Ps, volumedata)); + + Sum += SigmaT * gStepSize; + MinT += gStepSize; + } + + // Ps is the point + return true; +} + +uvec2 Sobol(uint n) { + uvec2 p = uvec2(0u); + uvec2 d = uvec2(0x80000000u); + + for(; n != 0u; n >>= 1u) { + if((n & 1u) != 0u) + p ^= d; + + d.x >>= 1u; // 1st dimension Sobol matrix, is same as base 2 Van der Corput + d.y ^= d.y >> 1u; // 2nd dimension Sobol matrix + } + + return p; +} + +// adapted from: https://www.shadertoy.com/view/3lcczS +uint ReverseBits(uint x) { + x = ((x & 0xaaaaaaaau) >> 1) | ((x & 0x55555555u) << 1); + x = ((x & 0xccccccccu) >> 2) | ((x & 0x33333333u) << 2); + x = ((x & 0xf0f0f0f0u) >> 4) | ((x & 0x0f0f0f0fu) << 4); + x = ((x & 0xff00ff00u) >> 8) | ((x & 0x00ff00ffu) << 8); + return (x >> 16) | (x << 16); + //return bitfieldReverse(x); +} + +// EDIT: updated with a new hash that fixes an issue with the old one. +// details in the post linked at the top. +uint OwenHash(uint x, uint seed) { // works best with random seeds + x ^= x * 0x3d20adeau; + x += seed; + x *= (seed >> 16) | 1u; + x ^= x * 0x05526c56u; + x ^= x * 0x53a22864u; + return x; +} + +uint OwenScramble(uint p, uint seed) { + p = ReverseBits(p); + p = OwenHash(p, seed); + return ReverseBits(p); +} + +vec2 OwenScrambledSobol(uint iter) { + uvec2 ip = Sobol(iter); + ip.x = OwenScramble(ip.x, 0xe7843fbfu); + ip.y = OwenScramble(ip.y, 0x8d8fb1e0u); + return vec2(ip) / float(0xffffffffu); +} + +vec4 CalculateRadiance(inout uvec2 seed) { + float r = rand(seed); + //return vec4(r,0,0,1); + + vec3 Lv = BLACK, Li = BLACK; + + //Ray Re = Ray(vec3(0,0,0), vec3(0,0,1), 0.0, 1500000.0); + //vec2 pixSample = vec2(rand(seed), rand(seed)); + vec2 pixSample = OwenScrambledSobol(uint(uSampleCounter)); + + vec2 UV = vUv*uResolution + pixSample; + + Ray Re = GenerateCameraRay(gCamera, UV, vec2(rand(seed), rand(seed))); + + //return vec4(vUv, 0.0, 1.0); + //return vec4(0.5*(Re.m_D + 1.0), 1.0); + //return vec4(Re.m_D, 1.0); + + //Re.m_MinT = 0.0f; + //Re.m_MaxT = 1500000.0f; + + vec3 Pe = vec3(0,0,0), Pl = vec3(0,0,0); + float lpdf = 0.0; + float alpha = 0.0; + + int ch; + float D; + // find point Pe along ray Re, and get its normalized intensity D and channel ch + if (SampleDistanceRM(Re, seed, Pe, D, ch)) + { + alpha = 1.0; + //return vec4(1.0, 1.0, 1.0, 1.0); + + // is there a light between Re.m_O and Pe? (ray's maxT is distance to Pe) + // (test to see if area light was hit before volume.) + int i = NearestLight(Ray(Re.m_O, Re.m_D, 0.0f, length(Pe - Re.m_O)), Li, Pl, lpdf); + if (i > -1) + { + // set sample pixel value in frame estimate (prior to accumulation) + return vec4(Li, 1.0); + } + + //int ch = 0; + //float D = GetNormalizedIntensityMax4ch(Pe, ch); + + // emission from volume + Lv += RGBtoXYZ(GetEmissionN(D, ch)); + + vec3 gradient = Gradient4ch(Pe, ch); + // send ray out from Pe toward light + switch (gShadingType) + { + case 0: + { + Lv += UniformSampleOneLight(ShaderType_Brdf, D, ch, normalize(-Re.m_D), Pe, normalize(gradient), seed); + break; + } + + case 1: + { + Lv += 0.5f * UniformSampleOneLight(ShaderType_Phase, D, ch, normalize(-Re.m_D), Pe, normalize(gradient), seed); + break; + } + + case 2: + { + //const float GradMag = GradientMagnitude(Pe, volumedata.gradientVolumeTexture[ch]) * (1.0/volumedata.intensityMax[ch]); + float GradMag = length(gradient); + float PdfBrdf = (1.0f - exp(-gGradientFactor * GradMag)); + + vec3 cls; // xyz color + if (rand(seed) < PdfBrdf) { + cls = UniformSampleOneLight(ShaderType_Brdf, D, ch, normalize(-Re.m_D), Pe, normalize(gradient), seed); + } + else { + cls = 0.5f * UniformSampleOneLight(ShaderType_Phase, D, ch, normalize(-Re.m_D), Pe, normalize(gradient), seed); + } + + Lv += cls; + + break; + } + } + } + else + { + // background color: + // set Lv to a selected color based on environment light source? +// if (uShowLights > 0.0) { +// int n = NearestLight(Ray(Re.m_O, Re.m_D, 0.0f, 1000000.0f), Li, Pl, lpdf); +// if (n > -1) +// Lv = Li; +// } + + //Lv = vec3(r,0,0); + + } + + // set sample pixel value in frame estimate (prior to accumulation) + + return vec4(Lv, alpha); +} + +vec4 CumulativeMovingAverage(vec4 A, vec4 Ax, float N) +{ + return A + ((Ax - A) / max((N), 1.0f)); +} + +void main() +{ + // seed for rand(seed) function + uvec2 seed = uvec2(uFrameCounter, uFrameCounter + 1.0) * uvec2(gl_FragCoord); + + // perform path tracing and get resulting pixel color + vec4 pixelColor = CalculateRadiance( seed ); + + vec4 previousColor = texture(tPreviousTexture, vUv); + if (uSampleCounter < 1.0) { + previousColor = vec4(0,0,0,0); + } + + out_FragColor = CumulativeMovingAverage(previousColor, pixelColor, uSampleCounter); +} + diff --git a/renderlib_wgpu/shaders/toneMap.wgsl b/renderlib_wgpu/shaders/toneMap.wgsl new file mode 100644 index 00000000..f49abfd4 --- /dev/null +++ b/renderlib_wgpu/shaders/toneMap.wgsl @@ -0,0 +1,112 @@ +struct VertexInput { + @location(0) position: vec3, + @location(1) uv: vec2, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) vUv: vec2, +}; + +@vertex +fn vs_main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.clip_position = vec4(in.position, 1.0); + out.vUv = in.uv; + return out; +} + + +@group(1) @binding(0) var gInvExposure:f32; + +@group(0) @binding(0) +var tTexture0: texture_2d; +@group(0) @binding(1) +var s: sampler; + + +fn XYZtoRGB(xyz: vec3) -> vec3 { + return vec3( + 3.240479f * xyz[0] - 1.537150f * xyz[1] - 0.498535f * xyz[2], + -0.969256f * xyz[0] + 1.875991f * xyz[1] + 0.041556f * xyz[2], + 0.055648f * xyz[0] - 0.204043f * xyz[1] + 1.057311f * xyz[2] + ); +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + // assumes texture stores colors in XYZ color space + var pixelColor: vec4 = textureSample(tTexture0, s, in.vUv); + pixelColor = vec4(XYZtoRGB(pixelColor.xyz), pixelColor.w); + pixelColor = vec4(vec3(1.0) - exp(-pixelColor.xyz * gInvExposure), pixelColor.w); + pixelColor = clamp(pixelColor, vec4(0.0), vec4(1.0)); + return pixelColor; +} + +/* + ///////////////////// + ///////////////////// + ///////////////////// + ///////////////////// + //// DENOISING FILTER + + vec4 clr00 = pixelColor; + vec4 rgbsample = clr00; + // convert XYZ to RGB here. + rgbsample.rgb = XYZtoRGB(clr00.rgb); + // tone map! + rgbsample.rgb = 1.0 - exp(-rgbsample.rgb * gInvExposure); + rgbsample = clamp(rgbsample, 0.0, 1.0); + clr00 = rgbsample; + + float fCount = 0.0; + float SumWeights = 0.0; + vec3 clr = vec3(0.0, 0.0, 0.0); + + for (int i = -gDenoiseWindowRadius; i <= gDenoiseWindowRadius; i++) { + for (int j = -gDenoiseWindowRadius; j <= gDenoiseWindowRadius; j++) { + // sad face... + if (Y + i < 0) + continue; + if (X + j < 0) + continue; + if (Y + i >= gFilmHeight) + continue; + if (X + j >= gFilmWidth) + continue; + + vec4 clrIJ = texture(tTexture0, vUv + vec2(i,j)); + rgbsample.rgb = XYZToRGB(clrIJ.rgb); + // tone map! + rgbsample.rgb = 1.0 - exp(-rgbsample.rgb * gInvExposure); + rgbsample = clamp(rgbsample, 0.0, 1.0); + + clrIJ = rgbsample; + + float distanceIJ = vecLen(clr00, clrIJ); + + // gDenoiseNoise = 1/h^2 + // + float weightIJ = expf(-(distanceIJ * gDenoiseNoise + (float)(i * i + j * j) * gDenoiseInvWindowArea)); + + clr.x += clrIJ.x * weightIJ; + clr.y += clrIJ.y * weightIJ; + clr.z += clrIJ.z * weightIJ; + + SumWeights += weightIJ; + + fCount += (weightIJ > gDenoiseWeightThreshold) ? gDenoiseInvWindowArea : 0; + } + } + + SumWeights = 1.0f / SumWeights; + + clr.rgb *= SumWeights; + + float LerpQ = (fCount > gDenoiseLerpThreshold) ? gDenoiseLerpC : 1.0f - gDenoiseLerpC; + + clr.rgb = mix(clr.rgb, clr00.rgb, LerpQ); + clr.rgb = clamp(clr.rgb, 0.0, 1.0); + + out_FragColor = vec4(clr.rgb, clr00.a); +*/ diff --git a/renderlib_wgpu/wgpu_util.cpp b/renderlib_wgpu/wgpu_util.cpp new file mode 100644 index 00000000..572be2cc --- /dev/null +++ b/renderlib_wgpu/wgpu_util.cpp @@ -0,0 +1,157 @@ +#include "wgpu_util.h" + +#include "../renderlib/Logging.h" + +void +request_adapter_callback(WGPURequestAdapterStatus status, WGPUAdapter received, const char* message, void* userdata) +{ + if (status == WGPURequestAdapterStatus_Success) { + LOG_INFO << "Got WebGPU adapter"; + } else { + LOG_ERROR << "Could not get WebGPU adapter"; + } + if (message) { + LOG_INFO << message; + } + *(WGPUAdapter*)userdata = received; +} + +void +request_device_callback(WGPURequestDeviceStatus status, WGPUDevice received, const char* message, void* userdata) +{ + if (status == WGPURequestDeviceStatus_Success) { + LOG_INFO << "Got WebGPU device"; + } else { + LOG_WARNING << "Could not get WebGPU device"; + } + if (message) { + LOG_INFO << message; + } + *(WGPUDevice*)userdata = received; +} + +void +handle_uncaptured_error(WGPUErrorType type, char const* message, void* userdata) +{ + std::string s; + switch (type) { + case WGPUErrorType_NoError: + s = "NoError"; + break; + case WGPUErrorType_Validation: + s = "Validation"; + break; + case WGPUErrorType_OutOfMemory: + s = "OutOfMemory"; + break; + case WGPUErrorType_Internal: + s = "Internal"; + break; + case WGPUErrorType_Unknown: + s = "Unknown"; + break; + case WGPUErrorType_DeviceLost: + s = "DeviceLost"; + break; + default: + s = "Unknown"; + break; + } + // UNUSED(userdata); + + LOG_INFO << "UNCAPTURED ERROR " << s << " (" << type << "): " << message; +} + +void +printAdapterFeatures(WGPUAdapter adapter) +{ + std::vector features; + + // Call the function a first time with a null return address, just to get + // the entry count. + size_t count = wgpuAdapterEnumerateFeatures(adapter, nullptr); + + // Allocate memory (could be a new, or a malloc() if this were a C program) + features.resize(count); + + // Call the function a second time, with a non-null return address + wgpuAdapterEnumerateFeatures(adapter, features.data()); + + LOG_INFO << "Adapter features:"; + for (uint32_t f : features) { + std::string s; + switch (f) { + case WGPUFeatureName_Undefined: + s = "Undefined"; + break; + case WGPUFeatureName_DepthClipControl: + s = "DepthClipControl"; + break; + case WGPUFeatureName_Depth32FloatStencil8: + s = "Depth32FloatStencil8"; + break; + case WGPUFeatureName_TimestampQuery: + s = "TimestampQuery"; + break; + case WGPUFeatureName_TextureCompressionBC: + s = "TextureCompressionBC"; + break; + case WGPUFeatureName_TextureCompressionETC2: + s = "TextureCompressionETC2"; + break; + case WGPUFeatureName_TextureCompressionASTC: + s = "TextureCompressionASTC"; + break; + case WGPUFeatureName_IndirectFirstInstance: + s = "IndirectFirstInstance"; + break; + case WGPUFeatureName_ShaderF16: + s = "ShaderF16"; + break; + case WGPUFeatureName_RG11B10UfloatRenderable: + s = "RG11B10UfloatRenderable"; + break; + case WGPUFeatureName_BGRA8UnormStorage: + s = "BGRA8UnormStorage"; + break; + case WGPUFeatureName_Float32Filterable: + s = "Float32Filterable"; + break; + case WGPUNativeFeature_PushConstants: + s = "PushConstants"; + break; + case WGPUNativeFeature_TextureAdapterSpecificFormatFeatures: + s = "TextureAdapterSpecificFormatFeatures"; + break; + case WGPUNativeFeature_MultiDrawIndirect: + s = "MultiDrawIndirect"; + break; + case WGPUNativeFeature_MultiDrawIndirectCount: + s = "MultiDrawIndirectCount"; + break; + case WGPUNativeFeature_VertexWritableStorage: + s = "VertexWritableStorage"; + break; + case WGPUNativeFeature_TextureBindingArray: + s = "TextureBindingArray"; + break; + case WGPUNativeFeature_SampledTextureAndStorageBufferArrayNonUniformIndexing: + s = "SampledTextureAndStorageBufferArrayNonUniformIndexing"; + break; + case WGPUNativeFeature_PipelineStatisticsQuery: + s = "PipelineStatisticsQuery"; + break; + default: + s = "Unknown"; + break; + } + LOG_INFO << " + " << s << " (" << f << ")"; + } +} + +void +handle_device_lost(WGPUDeviceLostReason reason, char const* message, void* userdata) +{ + LOG_INFO << "DEVICE LOST (" << reason << "): " << message; + // UNUSED(userdata); +} diff --git a/renderlib_wgpu/wgpu_util.h b/renderlib_wgpu/wgpu_util.h new file mode 100644 index 00000000..19c20352 --- /dev/null +++ b/renderlib_wgpu/wgpu_util.h @@ -0,0 +1,19 @@ +#pragma once + +#include "webgpu-headers/webgpu.h" +#include "wgpu.h" + +void +request_adapter_callback(WGPURequestAdapterStatus status, WGPUAdapter received, const char* message, void* userdata); + +void +request_device_callback(WGPURequestDeviceStatus status, WGPUDevice received, const char* message, void* userdata); + +void +handle_uncaptured_error(WGPUErrorType type, char const* message, void* userdata); + +void +printAdapterFeatures(WGPUAdapter adapter); + +void +handle_device_lost(WGPUDeviceLostReason reason, char const* message, void* userdata); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6a5ec9d8..f957b4b2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,6 +29,7 @@ target_sources(agave_test PRIVATE target_link_libraries(agave_test PRIVATE renderlib + renderlib_wgpu Qt6::Core Qt6::Gui Catch2WithMain )