├── .github └── workflows │ └── build.yml ├── .gitignore ├── COPYING ├── README.md ├── build.zig ├── build.zig.zon └── src ├── TODO.md ├── bin ├── hydra │ ├── README.md │ ├── blender.py │ ├── camera.cpp │ ├── camera.hpp │ ├── hydra.zig │ ├── instancer.cpp │ ├── instancer.hpp │ ├── material.cpp │ ├── material.hpp │ ├── mesh.cpp │ ├── mesh.hpp │ ├── moonshine.h │ ├── renderBuffer.cpp │ ├── renderBuffer.hpp │ ├── renderDelegate.cpp │ ├── renderDelegate.hpp │ ├── renderParam.hpp │ ├── renderPass.cpp │ ├── renderPass.hpp │ ├── rendererPlugin.cpp │ └── rendererPlugin.hpp ├── offline.zig └── online │ ├── ObjectPicker.zig │ ├── SyncCopier.zig │ ├── input.hlsl │ └── online.zig └── lib ├── README.md ├── Window.zig ├── core ├── DestructionQueue.zig ├── Encoder.zig ├── Image.zig ├── VulkanContext.zig ├── core.zig ├── descriptor.zig ├── mem.zig ├── pipeline.zig ├── shader_source.zig └── vk_helpers.zig ├── displaysystem ├── Display.zig ├── Swapchain.zig └── displaysystem.zig ├── engine.zig ├── fileformats ├── dds.zig ├── exr.zig ├── fileformats.zig └── wuffs.zig ├── gui ├── Platform.zig ├── gui.zig └── imgui.zig ├── hrtsystem ├── Accel.zig ├── BackgroundManager.zig ├── CameraManager.zig ├── ConstantSpectra.zig ├── MaterialManager.zig ├── MeshManager.zig ├── ModelManager.zig ├── Scene.zig ├── Sensor.zig ├── World.zig ├── hrtsystem.zig ├── pipeline.zig └── shaders │ ├── background │ ├── equirectangular_to_equal_area.hlsl │ └── fold.hlsl │ ├── bsdf.hlsl │ ├── camera.hlsl │ ├── integrator.hlsl │ ├── intersection.hlsl │ ├── light.hlsl │ ├── local_light │ ├── fold1.hlsl │ ├── fold3.hlsl │ ├── geometry_power.hlsl │ ├── instance_power.hlsl │ └── triangle_power.hlsl │ ├── main.hlsl │ ├── material.hlsl │ ├── medium.hlsl │ ├── phase_function.hlsl │ ├── ray.hlsl │ ├── reflection_frame.hlsl │ ├── scene.hlsl │ ├── spectrum.hlsl │ ├── volume.hlsl │ ├── volume_tracker.hlsl │ └── world.hlsl ├── shaders └── utils │ ├── helpers.hlsl │ ├── mappings.hlsl │ ├── math.hlsl │ ├── random.hlsl │ └── reservoir.hlsl ├── test_runner.zig ├── tests.zig └── vector.zig /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: [ trunk ] 6 | 7 | env: 8 | ZIG_VERSION: 0.14.0 9 | 10 | jobs: 11 | build-linux: 12 | runs-on: ubuntu-22.04 13 | 14 | steps: 15 | - name: Checkout repo 16 | uses: actions/checkout@v4 17 | 18 | - name: Install system dependencies 19 | run: | 20 | sudo apt-get update 21 | sudo apt-get install wayland-protocols libwayland-dev libxkbcommon-dev # Wayland 22 | sudo apt-get install libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev # X11 23 | 24 | - name: Install DirectXShaderCompiler 25 | run: | 26 | curl https://github.com/microsoft/DirectXShaderCompiler/releases/download/v1.8.2407/linux_dxc_2024_07_31.x86_64.tar.gz -L | tar -zx 27 | echo "$PWD/bin" >> $GITHUB_PATH 28 | 29 | - name: Setup Zig 30 | uses: mlugg/setup-zig@v1 31 | with: 32 | version: ${{env.ZIG_VERSION}} 33 | 34 | - name: Type check 35 | run: zig build check 36 | 37 | build-windows: 38 | runs-on: windows-2022 39 | 40 | steps: 41 | - name: Checkout repo 42 | uses: actions/checkout@v4 43 | 44 | - name: Install DirectXShaderCompiler 45 | run: | 46 | Invoke-WebRequest -uri "https://github.com/microsoft/DirectXShaderCompiler/releases/download/v1.8.2407/dxc_2024_07_31.zip" -Method "GET" -OutFile dxc.zip 47 | Expand-Archive dxc.zip 48 | Add-Content $env:GITHUB_PATH ";.\dxc\bin\x86" 49 | 50 | - name: Setup Zig 51 | uses: mlugg/setup-zig@v1 52 | with: 53 | version: ${{env.ZIG_VERSION}} 54 | 55 | - name: Type check 56 | run: zig build check -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # zig build artifacts 2 | .zig-cache/ 3 | zig-out/ 4 | 5 | # Any testing asset files 6 | **.exr 7 | **.glb 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Moonshine 4 | 5 | **A spectral path tracer built with Zig + Vulkan** 6 |
7 | 8 | [![A bathroom scene rendered with moonshine](https://repository-images.githubusercontent.com/378788480/b9ad3836-4558-43f6-82ed-6668d99399b4)](https://blendswap.com/blend/12584) 9 | *Salle de bain by nacimus, rendered with Moonshine* 10 | 11 | ### Features 12 | * Binaries 13 | * offline -- a headless offline renderer 14 | * online -- a real-time windowed renderer 15 | * hydra -- a hydra render delegate 16 | * Light Transport 17 | * Full spectral path tracing 18 | * Direct light sampling with multiple importance sampling for all lights and materials 19 | * Lights 20 | * 360° environment maps 21 | * Sampled via [hierarchical warping](https://pharr.org/matt/blog/2019/06/05/visualizing-env-light-warpings) 22 | * Emissive meshes 23 | * Sampled according to power via 1D [hierarchical warping](https://pharr.org/matt/blog/2019/06/05/visualizing-env-light-warpings) 24 | * Materials 25 | * Standard PBR with metallic + roughness 26 | * Mirror 27 | * Glass with dispersion 28 | * Homogeneous volumes 29 | 30 | ### Dependencies 31 | #### Build 32 | * zig (see version [in CI](.github/workflows/build.yml)) 33 | * DirectXShaderCompiler 34 | * For the online (real-time) renderer: 35 | * For Linux (Ubuntu, similar on others): 36 | * For Wayland: `wayland-protocols` `libwayland-dev` `libxkbcommon-dev` 37 | * For X11: `libxcursor-dev` `libxrandr-dev` `libxinerama-dev` `libxi-dev` 38 | #### Run 39 | * A GPU supporting Vulkan ray tracing 40 | 41 | ### License 42 | 43 | This project is licensed under the AGPL. 44 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .moonshine, 3 | .version = "0.0.0", 4 | 5 | .dependencies = .{ 6 | .vulkan_zig = .{ 7 | .url = "https://github.com/Snektron/vulkan-zig/archive/fdbe6d60d1a937ee64f4e37c2acd0c76a7588f24.tar.gz", 8 | .hash = "vulkan-0.0.0-r7Ytx2dUAwDhoUvuIqliqPUbyDZZi0bl_CQIwspUpHs_", 9 | }, 10 | .vulkan_headers = .{ 11 | .url = "https://github.com/KhronosGroup/Vulkan-Headers/archive/9c77de5c3dd216f28e407eec65ed9c0a296c1f74.tar.gz", 12 | .hash = "N-V-__8AAHwr7wGgESDREQVGeJWFvErxgAnCu78GY5gT5-Cc", 13 | }, 14 | .dcimgui = .{ 15 | .url = "https://github.com/dearimgui/dear_bindings/releases/download/master_nogeneratedefaultargfunctions_2025-05-18T04-48-54/AllBindingFiles.zip", 16 | .hash = "N-V-__8AAP5AVwBNhUwp0cB7zotClmmctA8_Tx5Xaqw7x2LN", 17 | }, 18 | .imgui = .{ 19 | .url = "https://github.com/ocornut/imgui/archive/cdb5cbe6f8786ced227624a71bc8a677618e5327.tar.gz", 20 | .hash = "N-V-__8AAFMPawC25cFqglS-ARiXPjNMkV56NJmokWQJQApN", 21 | }, 22 | .glfw = .{ 23 | .url = "https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz", 24 | .hash = "N-V-__8AAMrJSwAUGb9-vTzkNR-5LXS81MR__ZRVfF3tWgG6", 25 | }, 26 | .tinyexr = .{ 27 | .url = "https://github.com/syoyo/tinyexr/archive/735ff73ce5959cf005eb99ce517c9bcecab89dfb.tar.gz", 28 | .hash = "N-V-__8AAGC4nQHVa2CJUUb6saSEtcSR7c3wMfzHy9oF6m1g", 29 | }, 30 | .zgltf = .{ 31 | .url = "https://github.com/kooparse/zgltf/archive/b6579c3887c7ab0eef5f1eda09abbbc1f04d76ce.tar.gz", 32 | .hash = "zgltf-0.1.0-sX6QAqIKAgAfb7yQlVrdsLwCNc6Fx8wFOLIQRaEGJnVC", 33 | }, 34 | .wuffs = .{ 35 | .url = "https://github.com/google/wuffs-mirror-release-c/archive/a29749ebe0be57d2b19d8406475bd2326d0f1a85.tar.gz", 36 | .hash = "N-V-__8AALcpVQBr9VERrZc0MwDxDcN2zInDaZPF91ZMaUjq", 37 | }, 38 | }, 39 | .fingerprint = 0xccaef4ce7dcdd508, 40 | .minimum_zig_version = "0.14.0", 41 | .paths = .{ 42 | "", 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /src/TODO.md: -------------------------------------------------------------------------------- 1 | ### // TODO 2 | * Cameras 3 | * Fisheye 4 | * Lens System 5 | * Materials 6 | * Conductor with complex IOR 7 | * Transmissive with roughness 8 | * Material composition 9 | * Lights 10 | * Experiment with sampling triangle via solid angle after selecting it via area 11 | * Experiment with unifying sampling mesh lights and environment map 12 | * BVH 13 | * Sampling of filtered environment maps 14 | * Testing 15 | * Proper statistical tests GPU sampling routines 16 | * Proper statistical tests to make sure images have expected mean/variance 17 | * Resource management 18 | * Make sure we have all necessary `errdefers` 19 | * GPU resource arrays should be resizable 20 | * Need some sort of way to do async/parallel resource creation (transfers, processing) 21 | * Use physical (with correct scales) units 22 | * Integrators 23 | * ReSTIR 24 | * Spectral EXR output 25 | * Tonemapping 26 | * HDR display 27 | * A satisfying implementation is blocked by [HDR display metadata querying in Vulkan](https://github.com/KhronosGroup/Vulkan-Docs/issues/1787) 28 | * Make all objects scale-invariant 29 | * You should get the exact same image no matter what the root transform is, even if it is non-orthogonal 30 | * Cameras and backgrounds are currently missing this -------------------------------------------------------------------------------- /src/bin/hydra/README.md: -------------------------------------------------------------------------------- 1 | ### Here be dragons 2 | 3 | I don't really know how Hydra works or what the proper idioms are, so this whole subproject is in the "rapid prototyping" phase rather than the "principled, polished work" phase. -------------------------------------------------------------------------------- /src/bin/hydra/blender.py: -------------------------------------------------------------------------------- 1 | # Blender add-on for moonshine 2 | # Moonshine delegate must be in PXR_PLUGINPATH_NAME 3 | 4 | import bpy 5 | 6 | class MoonshineRenderEngine(bpy.types.HydraRenderEngine): 7 | bl_idname = 'HYDRA_MOONSHINE' 8 | bl_label = "Moonshine" 9 | 10 | bl_use_preview = True 11 | bl_use_gpu_context = False 12 | bl_use_materialx = False 13 | 14 | bl_delegate_id = 'HdMoonshinePlugin' 15 | 16 | def view_draw(self, context, depsgraph): 17 | super().view_draw(context, depsgraph) 18 | self.tag_redraw() # hack for accumulating samples; TODO: figure out how cycles does this 19 | 20 | register, unregister = bpy.utils.register_classes_factory(( 21 | MoonshineRenderEngine, 22 | )) 23 | 24 | if __name__ == "__main__": 25 | register() -------------------------------------------------------------------------------- /src/bin/hydra/camera.cpp: -------------------------------------------------------------------------------- 1 | #include "moonshine.h" 2 | 3 | #include "camera.hpp" 4 | #include "renderDelegate.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | PXR_NAMESPACE_OPEN_SCOPE 11 | 12 | HdMoonshineCamera::HdMoonshineCamera(SdfPath const& id) : HdCamera(id) {} 13 | 14 | void HdMoonshineCamera::Sync(HdSceneDelegate* sceneDelegate, HdRenderParam* renderParam, HdDirtyBits* dirtyBits) { 15 | HdCamera::Sync(sceneDelegate, renderParam, dirtyBits); 16 | HdMoonshine* msne = static_cast(renderParam)->_moonshine; 17 | 18 | GfMatrix4f transform = GfMatrix4f(GetTransform()); 19 | GfVec3f origin = transform.Transform(GfVec3f(0.0, 0.0, 0.0)); 20 | GfVec3f forward = transform.TransformDir(GfVec3f(0.0, 0.0, -1.0)); 21 | GfVec3f up = transform.TransformDir(GfVec3f(0.0, -1.0, 0.0)); // insert negation here as we want to flip resultant image 22 | 23 | forward.Normalize(); 24 | up.Normalize(); 25 | 26 | Lens lens = Lens { 27 | .origin = F32x3 { .x = origin[0], .y = origin[1], .z = origin[2] }, 28 | .forward = F32x3 { .x = forward[0], .y = forward[1], .z = forward[2] }, 29 | .up = F32x3 { .x = up[0], .y = up[1], .z = up[2] }, 30 | .vfov = 2.0f * std::atan(GetVerticalAperture() / (2.0f * GetFocalLength())), 31 | .aperture = 0, 32 | .focus_distance = 1, 33 | }; 34 | 35 | if (_handle == -1) { 36 | _handle = HdMoonshineCreateLens(msne, lens); 37 | } else { 38 | HdMoonshineSetLens(msne, _handle, lens); 39 | } 40 | } 41 | 42 | PXR_NAMESPACE_CLOSE_SCOPE 43 | -------------------------------------------------------------------------------- /src/bin/hydra/camera.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "moonshine.h" 4 | 5 | #include "pxr/pxr.h" 6 | #include "pxr/imaging/hd/camera.h" 7 | 8 | PXR_NAMESPACE_OPEN_SCOPE 9 | 10 | class HdMoonshineCamera final : public HdCamera { 11 | public: 12 | HdMoonshineCamera(SdfPath const& id); 13 | ~HdMoonshineCamera() override = default; 14 | 15 | void Sync(HdSceneDelegate* sceneDelegate, HdRenderParam* renderParam, HdDirtyBits* dirtyBits) override; 16 | 17 | LensHandle _handle = -1; 18 | protected: 19 | HdMoonshineCamera(const HdMoonshineCamera&) = delete; 20 | HdMoonshineCamera &operator =(const HdMoonshineCamera&) = delete; 21 | }; 22 | 23 | PXR_NAMESPACE_CLOSE_SCOPE 24 | -------------------------------------------------------------------------------- /src/bin/hydra/instancer.cpp: -------------------------------------------------------------------------------- 1 | #include "instancer.hpp" 2 | 3 | #include "pxr/imaging/hd/sceneDelegate.h" 4 | #include "pxr/imaging/hd/tokens.h" 5 | 6 | #include "pxr/base/gf/vec3f.h" 7 | #include "pxr/base/gf/vec4f.h" 8 | #include "pxr/base/gf/matrix4d.h" 9 | #include "pxr/base/gf/rotation.h" 10 | #include "pxr/base/gf/quaternion.h" 11 | 12 | PXR_NAMESPACE_OPEN_SCOPE 13 | 14 | HdMoonshineInstancer::HdMoonshineInstancer(HdSceneDelegate* delegate, SdfPath const& id) : HdInstancer(delegate, id) {} 15 | 16 | HdMoonshineInstancer::~HdMoonshineInstancer() {} 17 | 18 | void HdMoonshineInstancer::Sync(HdSceneDelegate* delegate, HdRenderParam* renderParam, HdDirtyBits* dirtyBits) { 19 | _UpdateInstancer(delegate, dirtyBits); 20 | 21 | if (HdChangeTracker::IsAnyPrimvarDirty(*dirtyBits, GetId())) { 22 | SdfPath const& id = GetId(); 23 | HdPrimvarDescriptorVector primvars = delegate->GetPrimvarDescriptors(id, HdInterpolationInstance); 24 | 25 | for (HdPrimvarDescriptor const& pv: primvars) { 26 | if (HdChangeTracker::IsPrimvarDirty(*dirtyBits, id, pv.name)) { 27 | VtValue value = delegate->Get(id, pv.name); 28 | if (!value.IsEmpty()) { 29 | primvarMap_[pv.name] = value; 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | VtMatrix4dArray HdMoonshineInstancer::ComputeInstanceTransforms(SdfPath const &prototypeId) { 37 | GfMatrix4d instancerTransform = GetDelegate()->GetInstancerTransform(GetId()); 38 | VtIntArray instanceIndices = GetDelegate()->GetInstanceIndices(GetId(), prototypeId); 39 | 40 | VtMatrix4dArray instanceTransforms(instanceIndices.size()); 41 | 42 | VtValue translationsValue = primvarMap_[HdInstancerTokens->instanceTranslations]; 43 | VtValue rotationsValue = primvarMap_[HdInstancerTokens->instanceRotations]; 44 | VtValue scalesValue = primvarMap_[HdInstancerTokens->instanceScales]; 45 | VtValue transformsValue = primvarMap_[HdInstancerTokens->instanceTransforms]; 46 | 47 | VtVec3dArray translations; 48 | if (translationsValue.CanCast()) { 49 | translations = translationsValue.Cast().UncheckedGet(); 50 | } 51 | 52 | VtQuatdArray rotations; 53 | if (rotationsValue.CanCast()) { 54 | rotations = rotationsValue.Cast().UncheckedGet(); 55 | } 56 | 57 | VtVec3dArray scales; 58 | if (scalesValue.CanCast()) { 59 | scales = scalesValue.Cast().UncheckedGet(); 60 | } 61 | 62 | VtMatrix4dArray transforms; 63 | if (transformsValue.CanCast()) { 64 | transforms = transformsValue.Cast().UncheckedGet(); 65 | } 66 | 67 | for (size_t i = 0; i < instanceIndices.size(); i++) { 68 | int instanceIndex = instanceIndices[i]; 69 | 70 | GfMatrix4d out = instancerTransform; 71 | 72 | GfMatrix4d temp; 73 | if (i < translations.size()) { 74 | temp.SetTranslate(translations[instanceIndex]); 75 | out = temp * out; 76 | } 77 | if (i < rotations.size()) { 78 | temp.SetRotate(rotations[instanceIndex]); 79 | out = temp * out; 80 | } 81 | if (i < scales.size()) { 82 | temp.SetScale(scales[instanceIndex]); 83 | out = temp * out; 84 | } 85 | if (i < transforms.size()) { 86 | temp = transforms[instanceIndex]; 87 | out = temp * out; 88 | } 89 | 90 | instanceTransforms[i] = out; 91 | } 92 | 93 | if (GetParentId().IsEmpty()) { 94 | return instanceTransforms; 95 | } 96 | 97 | HdInstancer *parentInstancer = GetDelegate()->GetRenderIndex().GetInstancer(GetParentId()); 98 | VtMatrix4dArray parentTransforms = static_cast(parentInstancer)->ComputeInstanceTransforms(GetId()); 99 | 100 | VtMatrix4dArray final(parentTransforms.size() * instanceTransforms.size()); 101 | for (size_t i = 0; i < parentTransforms.size(); ++i) { 102 | for (size_t j = 0; j < instanceTransforms.size(); ++j) { 103 | final[i * instanceTransforms.size() + j] = instanceTransforms[j] * parentTransforms[i]; 104 | } 105 | } 106 | 107 | return final; 108 | } 109 | 110 | PXR_NAMESPACE_CLOSE_SCOPE 111 | -------------------------------------------------------------------------------- /src/bin/hydra/instancer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "moonshine.h" 4 | 5 | #include "pxr/pxr.h" 6 | #include "pxr/imaging/hd/instancer.h" 7 | 8 | PXR_NAMESPACE_OPEN_SCOPE 9 | 10 | class HdMoonshineInstancer : public HdInstancer { 11 | public: 12 | HdMoonshineInstancer(HdSceneDelegate* delegate, SdfPath const& id); 13 | ~HdMoonshineInstancer(); 14 | 15 | void Sync(HdSceneDelegate *sceneDelegate, HdRenderParam *renderParam, HdDirtyBits *dirtyBits) override; 16 | VtMatrix4dArray ComputeInstanceTransforms(const SdfPath& prototypeId); 17 | private: 18 | TfHashMap primvarMap_; 19 | }; 20 | 21 | PXR_NAMESPACE_CLOSE_SCOPE 22 | -------------------------------------------------------------------------------- /src/bin/hydra/material.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "renderParam.hpp" 7 | 8 | PXR_NAMESPACE_OPEN_SCOPE 9 | 10 | class HdMoonshineMaterial final : public HdMaterial 11 | { 12 | public: 13 | HdMoonshineMaterial(const SdfPath& id, const HdMoonshineRenderParam& renderParam); 14 | ~HdMoonshineMaterial() override; 15 | 16 | public: 17 | HdDirtyBits GetInitialDirtyBitsMask() const override; 18 | 19 | void Sync(HdSceneDelegate* sceneDelegate, HdRenderParam* renderParam, HdDirtyBits* dirtyBits) override; 20 | 21 | MaterialHandle _handle; 22 | }; 23 | 24 | PXR_NAMESPACE_CLOSE_SCOPE -------------------------------------------------------------------------------- /src/bin/hydra/mesh.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "renderParam.hpp" 10 | 11 | #include "moonshine.h" 12 | 13 | PXR_NAMESPACE_OPEN_SCOPE 14 | 15 | class HdMoonshineMesh final : public HdMesh { 16 | public: 17 | HdMoonshineMesh(SdfPath const& id, const HdMoonshineRenderParam& renderParam); 18 | ~HdMoonshineMesh() override = default; 19 | 20 | HdDirtyBits GetInitialDirtyBitsMask() const override; 21 | 22 | void Sync(HdSceneDelegate* sceneDelegate, HdRenderParam* renderParam, HdDirtyBits* dirtyBits, TfToken const &reprToken) override; 23 | 24 | void Finalize(HdRenderParam *renderParam) override; 25 | protected: 26 | void _InitRepr(TfToken const &reprToken, HdDirtyBits *dirtyBits) override; 27 | 28 | HdDirtyBits _PropagateDirtyBits(HdDirtyBits bits) const override; 29 | 30 | HdMoonshineMesh(const HdMoonshineMesh&) = delete; 31 | HdMoonshineMesh &operator =(const HdMoonshineMesh&) = delete; 32 | private: 33 | std::optional FindPrimvarInterpolation(HdSceneDelegate* sceneDelegate, TfToken name) const; 34 | 35 | template 36 | VtArray ComputePrimvar(HdSceneDelegate* sceneDelegate, VtVec3iArray const& indices, TfToken primvarName) const; 37 | 38 | GfMatrix4f _transform{1.0f}; 39 | MeshHandle _mesh; 40 | MaterialHandle _material; 41 | 42 | // these two have same len 43 | std::vector _instances = {}; 44 | std::vector _instancesTransforms = {}; 45 | }; 46 | 47 | PXR_NAMESPACE_CLOSE_SCOPE 48 | -------------------------------------------------------------------------------- /src/bin/hydra/moonshine.h: -------------------------------------------------------------------------------- 1 | // sort of viewing this whole file as a temporary hack 2 | // until emit-h is resurrected 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | typedef uint32_t MeshHandle; 11 | typedef uint32_t ImageHandle; 12 | typedef uint32_t MaterialHandle; 13 | typedef uint32_t SensorHandle; 14 | typedef uint32_t LensHandle; 15 | typedef uint32_t InstanceHandle; 16 | 17 | typedef struct F32x2 { 18 | float x, y; 19 | } F32x2; 20 | 21 | typedef struct F32x3 { 22 | float x, y, z; 23 | } F32x3; 24 | 25 | typedef struct F32x4 { 26 | float x, y, z, w; 27 | } F32x4; 28 | 29 | typedef struct U32x3 { 30 | uint32_t x, y, z; 31 | } U32x3; 32 | 33 | typedef struct Mat4x3 { 34 | F32x4 x, y, z; 35 | } Mat4x3; 36 | 37 | typedef struct Extent2D { 38 | uint32_t width; 39 | uint32_t height; 40 | } Extent2D; 41 | 42 | typedef struct Lens { 43 | F32x3 origin; 44 | F32x3 forward; 45 | F32x3 up; 46 | float vfov; 47 | float aperture; 48 | float focus_distance; 49 | } Lens; 50 | 51 | typedef struct Material { 52 | ImageHandle normal; 53 | ImageHandle emissive; 54 | ImageHandle color; 55 | ImageHandle metalness; 56 | ImageHandle roughness; 57 | float ior; 58 | } Material; 59 | 60 | typedef enum TextureFormat { 61 | f16x4, 62 | u8x1, 63 | u8x2, 64 | u8x4, 65 | u8x4_srgb, 66 | } TextureFormat; 67 | 68 | typedef struct HdMoonshine HdMoonshine; 69 | extern "C" HdMoonshine* HdMoonshineCreate(void); 70 | extern "C" void HdMoonshineDestroy(HdMoonshine*); 71 | extern "C" bool HdMoonshineRender(HdMoonshine*, SensorHandle, LensHandle); 72 | extern "C" bool HdMoonshineRebuildPipeline(HdMoonshine*); 73 | extern "C" MeshHandle HdMoonshineCreateMesh(HdMoonshine*, const F32x3*, const F32x3*, const F32x2*, size_t); 74 | extern "C" ImageHandle HdMoonshineCreateSolidTexture1(HdMoonshine*, float, const char*); 75 | extern "C" ImageHandle HdMoonshineCreateSolidTexture2(HdMoonshine*, F32x2, const char*); 76 | extern "C" ImageHandle HdMoonshineCreateSolidTexture3(HdMoonshine*, F32x3, const char*); 77 | extern "C" ImageHandle HdMoonshineCreateRawTexture(HdMoonshine*, uint8_t*, Extent2D, TextureFormat, const char*); 78 | extern "C" MaterialHandle HdMoonshineCreateMaterial(HdMoonshine*, Material); 79 | extern "C" void HdMoonshineSetMaterialNormal(HdMoonshine*, MaterialHandle, ImageHandle); 80 | extern "C" void HdMoonshineSetMaterialEmissive(HdMoonshine*, MaterialHandle, ImageHandle); 81 | extern "C" void HdMoonshineSetMaterialColor(HdMoonshine*, MaterialHandle, ImageHandle); 82 | extern "C" void HdMoonshineSetMaterialMetalness(HdMoonshine*, MaterialHandle, ImageHandle); 83 | extern "C" void HdMoonshineSetMaterialRoughness(HdMoonshine*, MaterialHandle, ImageHandle); 84 | extern "C" void HdMoonshineSetMaterialIOR(HdMoonshine*, MaterialHandle, float); 85 | extern "C" InstanceHandle HdMoonshineCreateInstance(HdMoonshine*, Mat4x3, MeshHandle, MaterialHandle, bool); 86 | extern "C" void HdMoonshineDestroyInstance(HdMoonshine*, InstanceHandle); 87 | extern "C" void HdMoonshineSetInstanceTransform(HdMoonshine*, InstanceHandle, Mat4x3); 88 | extern "C" void HdMoonshineSetInstanceVisibility(HdMoonshine*, InstanceHandle, bool); 89 | extern "C" SensorHandle HdMoonshineCreateSensor(HdMoonshine*, Extent2D); 90 | extern "C" float* HdMoonshineGetSensorData(const HdMoonshine*, SensorHandle); 91 | extern "C" LensHandle HdMoonshineCreateLens(HdMoonshine*, Lens); 92 | extern "C" void HdMoonshineSetLens(HdMoonshine*, LensHandle, Lens); 93 | -------------------------------------------------------------------------------- /src/bin/hydra/renderBuffer.cpp: -------------------------------------------------------------------------------- 1 | #include "renderBuffer.hpp" 2 | #include "moonshine.h" 3 | #include "renderDelegate.hpp" 4 | 5 | #include "pxr/imaging/hd/renderBuffer.h" 6 | #include "pxr/imaging/hd/sceneDelegate.h" 7 | #include "pxr/imaging/hd/renderIndex.h" 8 | #include "pxr/base/gf/vec3i.h" 9 | 10 | #include 11 | 12 | PXR_NAMESPACE_OPEN_SCOPE 13 | 14 | HdMoonshineRenderBuffer::HdMoonshineRenderBuffer(SdfPath const& id, HdMoonshineRenderDelegate* renderDelegate) : HdRenderBuffer(id), _renderDelegate(renderDelegate) {} 15 | 16 | HdMoonshineRenderBuffer::~HdMoonshineRenderBuffer() = default; 17 | 18 | void HdMoonshineRenderBuffer::_Deallocate() {} // TODO 19 | 20 | bool HdMoonshineRenderBuffer::Allocate(GfVec3i const& dimensions, HdFormat format, bool multiSampled) 21 | { 22 | _width = dimensions[0]; 23 | _height = dimensions[1]; 24 | 25 | _sensor = HdMoonshineCreateSensor(_renderDelegate->_moonshine, Extent2D { .width = _width, .height = _height }); 26 | _data = reinterpret_cast(HdMoonshineGetSensorData(_renderDelegate->_moonshine, _sensor)); 27 | 28 | return true; 29 | } 30 | 31 | void HdMoonshineRenderBuffer::Resolve() {} 32 | 33 | PXR_NAMESPACE_CLOSE_SCOPE 34 | -------------------------------------------------------------------------------- /src/bin/hydra/renderBuffer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "moonshine.h" 4 | 5 | #include "pxr/pxr.h" 6 | #include "pxr/imaging/hd/renderBuffer.h" 7 | #include "renderDelegate.hpp" 8 | 9 | PXR_NAMESPACE_OPEN_SCOPE 10 | 11 | class HdMoonshineRenderBuffer : public HdRenderBuffer 12 | { 13 | public: 14 | HdMoonshineRenderBuffer(SdfPath const& id, HdMoonshineRenderDelegate* renderDelegate); 15 | ~HdMoonshineRenderBuffer() override; 16 | 17 | bool Allocate(GfVec3i const& dimensions, HdFormat format, bool multiSampled) override; 18 | 19 | unsigned int GetWidth() const override { return _width; } 20 | unsigned int GetHeight() const override { return _height; } 21 | unsigned int GetDepth() const override { return 1; } 22 | HdFormat GetFormat() const override { return HdFormatFloat32Vec4; } 23 | bool IsMultiSampled() const override { return false; } 24 | 25 | void* Map() override { 26 | return _data; 27 | } 28 | 29 | void Unmap() override {} 30 | 31 | bool IsMapped() const override { 32 | return false; 33 | } 34 | 35 | bool IsConverged() const override { 36 | return false; 37 | } 38 | 39 | void Resolve() override; 40 | 41 | SensorHandle _sensor; 42 | private: 43 | void _Deallocate() override; 44 | 45 | HdMoonshineRenderDelegate* _renderDelegate; 46 | unsigned int _width; 47 | unsigned int _height; 48 | uint8_t* _data = nullptr; 49 | }; 50 | 51 | PXR_NAMESPACE_CLOSE_SCOPE 52 | -------------------------------------------------------------------------------- /src/bin/hydra/renderDelegate.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "renderDelegate.hpp" 6 | #include "renderPass.hpp" 7 | #include "renderBuffer.hpp" 8 | #include "mesh.hpp" 9 | #include "camera.hpp" 10 | #include "instancer.hpp" 11 | #include "material.hpp" 12 | 13 | PXR_NAMESPACE_OPEN_SCOPE 14 | 15 | TF_DEFINE_PRIVATE_TOKENS(_tokens, 16 | (rebuildPipeline) 17 | ); 18 | 19 | const TfTokenVector HdMoonshineRenderDelegate::SUPPORTED_RPRIM_TYPES = { 20 | HdPrimTypeTokens->mesh, 21 | }; 22 | 23 | const TfTokenVector HdMoonshineRenderDelegate::SUPPORTED_SPRIM_TYPES = { 24 | HdPrimTypeTokens->camera, 25 | HdPrimTypeTokens->extComputation, 26 | HdPrimTypeTokens->material, 27 | }; 28 | 29 | const TfTokenVector HdMoonshineRenderDelegate::SUPPORTED_BPRIM_TYPES = { 30 | HdPrimTypeTokens->renderBuffer, 31 | }; 32 | 33 | HdMoonshineRenderDelegate::HdMoonshineRenderDelegate() : HdRenderDelegate() { 34 | _Initialize(); 35 | } 36 | 37 | HdMoonshineRenderDelegate::HdMoonshineRenderDelegate(HdRenderSettingsMap const& settingsMap) : HdRenderDelegate(settingsMap) { 38 | _Initialize(); 39 | } 40 | 41 | HdCommandDescriptors HdMoonshineRenderDelegate::GetCommandDescriptors() const { 42 | HdCommandDescriptor commandDesc(_tokens->rebuildPipeline, "Rebuild pipeline", {}); 43 | return { commandDesc }; 44 | } 45 | 46 | bool HdMoonshineRenderDelegate::InvokeCommand(const TfToken &command, const HdCommandArgs &args) { 47 | if (command == _tokens->rebuildPipeline) { 48 | HdMoonshineRebuildPipeline(_moonshine); 49 | return true; 50 | } else { 51 | TF_CODING_ERROR("Unknown command %s!", command.GetText()); 52 | return false; 53 | } 54 | } 55 | 56 | void HdMoonshineRenderDelegate::_Initialize() { 57 | _moonshine = HdMoonshineCreate(); 58 | _resourceRegistry = std::make_shared(); 59 | _renderParam = std::make_unique(_moonshine); 60 | } 61 | 62 | HdMoonshineRenderDelegate::~HdMoonshineRenderDelegate() { 63 | _resourceRegistry.reset(); 64 | HdMoonshineDestroy(_moonshine); 65 | } 66 | 67 | TfTokenVector const& HdMoonshineRenderDelegate::GetSupportedRprimTypes() const { 68 | return SUPPORTED_RPRIM_TYPES; 69 | } 70 | 71 | TfTokenVector const& HdMoonshineRenderDelegate::GetSupportedSprimTypes() const { 72 | return SUPPORTED_SPRIM_TYPES; 73 | } 74 | 75 | TfTokenVector const& HdMoonshineRenderDelegate::GetSupportedBprimTypes() const { 76 | return SUPPORTED_BPRIM_TYPES; 77 | } 78 | 79 | HdResourceRegistrySharedPtr HdMoonshineRenderDelegate::GetResourceRegistry() const { 80 | return _resourceRegistry; 81 | } 82 | 83 | void HdMoonshineRenderDelegate::CommitResources(HdChangeTracker *tracker) {} 84 | 85 | HdRenderPassSharedPtr HdMoonshineRenderDelegate::CreateRenderPass(HdRenderIndex *index, HdRprimCollection const& collection) { 86 | return HdRenderPassSharedPtr(new HdMoonshineRenderPass(index, collection)); 87 | } 88 | 89 | HdRprim* HdMoonshineRenderDelegate::CreateRprim(TfToken const& typeId, SdfPath const& rprimId) { 90 | if (typeId == HdPrimTypeTokens->mesh) { 91 | return new HdMoonshineMesh(rprimId, *_renderParam); 92 | } else { 93 | TF_CODING_ERROR("Unknown Rprim type %s", typeId.GetText()); 94 | return nullptr; 95 | } 96 | } 97 | 98 | void HdMoonshineRenderDelegate::DestroyRprim(HdRprim *rPrim) { 99 | delete rPrim; 100 | } 101 | 102 | HdSprim* HdMoonshineRenderDelegate::CreateSprim(TfToken const& typeId, SdfPath const& sprimId) { 103 | if (typeId == HdPrimTypeTokens->camera) { 104 | return new HdMoonshineCamera(sprimId); 105 | } else if (typeId == HdPrimTypeTokens->extComputation) { 106 | return new HdExtComputation(sprimId); 107 | } else if (typeId == HdPrimTypeTokens->material) { 108 | return new HdMoonshineMaterial(sprimId, *_renderParam); 109 | } else { 110 | TF_CODING_ERROR("Unknown Sprim type %s", typeId.GetText()); 111 | return nullptr; 112 | } 113 | } 114 | 115 | HdSprim* HdMoonshineRenderDelegate::CreateFallbackSprim(TfToken const& typeId) { 116 | if (typeId == HdPrimTypeTokens->camera) { 117 | return new HdMoonshineCamera(SdfPath::EmptyPath()); 118 | } else if (typeId == HdPrimTypeTokens->extComputation) { 119 | return new HdExtComputation(SdfPath::EmptyPath()); 120 | } else if (typeId == HdPrimTypeTokens->material) { 121 | return new HdMoonshineMaterial(SdfPath::EmptyPath(), *_renderParam); 122 | } else { 123 | TF_CODING_ERROR("Unknown fallback Sprim type %s", typeId.GetText()); 124 | return nullptr; 125 | } 126 | } 127 | 128 | void HdMoonshineRenderDelegate::DestroySprim(HdSprim *sPrim) { 129 | delete sPrim; 130 | } 131 | 132 | HdBprim* HdMoonshineRenderDelegate::CreateBprim(TfToken const& typeId, SdfPath const& bprimId) { 133 | if (typeId == HdPrimTypeTokens->renderBuffer) { 134 | return new HdMoonshineRenderBuffer(bprimId, this); 135 | } else { 136 | TF_CODING_ERROR("Unknown Bprim type %s", typeId.GetText()); 137 | return nullptr; 138 | } 139 | } 140 | 141 | HdBprim* HdMoonshineRenderDelegate::CreateFallbackBprim(TfToken const& typeId) { 142 | if (typeId == HdPrimTypeTokens->renderBuffer) { 143 | return new HdMoonshineRenderBuffer(SdfPath::EmptyPath(), this); 144 | } else { 145 | TF_CODING_ERROR("Unknown fallback Bprim type %s", typeId.GetText()); 146 | return nullptr; 147 | } 148 | } 149 | 150 | void HdMoonshineRenderDelegate::DestroyBprim(HdBprim *bPrim) { 151 | delete bPrim; 152 | } 153 | 154 | HdInstancer* HdMoonshineRenderDelegate::CreateInstancer(HdSceneDelegate *delegate, SdfPath const& id) { 155 | return new HdMoonshineInstancer(delegate, id); 156 | } 157 | 158 | void HdMoonshineRenderDelegate::DestroyInstancer(HdInstancer *instancer) { 159 | delete instancer; 160 | } 161 | 162 | HdRenderParam* HdMoonshineRenderDelegate::GetRenderParam() const { 163 | return _renderParam.get(); 164 | } 165 | 166 | HdAovDescriptor HdMoonshineRenderDelegate::GetDefaultAovDescriptor(TfToken const& name) const { 167 | if (name == HdAovTokens->color) { 168 | return HdAovDescriptor(HdFormatFloat32Vec3, false, VtValue(GfVec4f(0.0f))); 169 | } else { 170 | return HdAovDescriptor(); 171 | } 172 | } 173 | 174 | PXR_NAMESPACE_CLOSE_SCOPE 175 | -------------------------------------------------------------------------------- /src/bin/hydra/renderDelegate.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "renderParam.hpp" 10 | 11 | #include "moonshine.h" 12 | 13 | PXR_NAMESPACE_OPEN_SCOPE 14 | 15 | class HdMoonshineRenderDelegate final : public HdRenderDelegate 16 | { 17 | public: 18 | HdMoonshineRenderDelegate(); 19 | HdMoonshineRenderDelegate(HdRenderSettingsMap const& settingsMap); 20 | ~HdMoonshineRenderDelegate(); 21 | 22 | HdCommandDescriptors GetCommandDescriptors() const override; 23 | 24 | bool InvokeCommand(const TfToken &command, const HdCommandArgs &args) override; 25 | 26 | const TfTokenVector &GetSupportedRprimTypes() const override; 27 | const TfTokenVector &GetSupportedSprimTypes() const override; 28 | const TfTokenVector &GetSupportedBprimTypes() const override; 29 | 30 | HdResourceRegistrySharedPtr GetResourceRegistry() const override; 31 | 32 | HdRenderPassSharedPtr CreateRenderPass(HdRenderIndex *index, HdRprimCollection const& collection) override; 33 | 34 | HdInstancer *CreateInstancer(HdSceneDelegate *delegate, SdfPath const& id) override; 35 | void DestroyInstancer(HdInstancer *instancer) override; 36 | 37 | HdRprim *CreateRprim(TfToken const& typeId, SdfPath const& rprimId) override; 38 | void DestroyRprim(HdRprim *rPrim) override; 39 | 40 | HdSprim *CreateSprim(TfToken const& typeId, SdfPath const& sprimId) override; 41 | HdSprim *CreateFallbackSprim(TfToken const& typeId) override; 42 | void DestroySprim(HdSprim *sprim) override; 43 | 44 | HdBprim *CreateBprim(TfToken const& typeId, SdfPath const& bprimId) override; 45 | HdBprim *CreateFallbackBprim(TfToken const& typeId) override; 46 | void DestroyBprim(HdBprim *bprim) override; 47 | 48 | void CommitResources(HdChangeTracker *tracker) override; 49 | 50 | HdRenderParam *GetRenderParam() const override; 51 | 52 | HdAovDescriptor GetDefaultAovDescriptor(TfToken const& name) const override; 53 | HdMoonshine* _moonshine; 54 | private: 55 | static const TfTokenVector SUPPORTED_RPRIM_TYPES; 56 | static const TfTokenVector SUPPORTED_SPRIM_TYPES; 57 | static const TfTokenVector SUPPORTED_BPRIM_TYPES; 58 | 59 | void _Initialize(); 60 | 61 | HdResourceRegistrySharedPtr _resourceRegistry; 62 | std::unique_ptr _renderParam; 63 | 64 | HdMoonshineRenderDelegate(const HdMoonshineRenderDelegate &) = delete; 65 | HdMoonshineRenderDelegate &operator =(const HdMoonshineRenderDelegate &) = delete; 66 | }; 67 | 68 | PXR_NAMESPACE_CLOSE_SCOPE -------------------------------------------------------------------------------- /src/bin/hydra/renderParam.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "moonshine.h" 6 | 7 | PXR_NAMESPACE_OPEN_SCOPE 8 | 9 | class HdMoonshineRenderParam final : public HdRenderParam 10 | { 11 | public: 12 | HdMoonshineRenderParam(HdMoonshine* moonshine) : _moonshine(moonshine) { 13 | _black3 = HdMoonshineCreateSolidTexture3(_moonshine, F32x3 { .x = 0.0f, .y = 0.0f, .z = 0.0f }, "black3"); 14 | _black1 = HdMoonshineCreateSolidTexture1(_moonshine, 0.0, "black1"); 15 | _upNormal = HdMoonshineCreateSolidTexture2(_moonshine, F32x2 { .x = 0.5f, .y = 0.5f }, "up normal"); 16 | _grey3 = HdMoonshineCreateSolidTexture3(_moonshine, F32x3 { .x = 0.5f, .y = 0.5f, .z = 0.5f }, "grey3"); 17 | _white1 = HdMoonshineCreateSolidTexture1(_moonshine, 1.0, "white1"); 18 | _defaultMaterial = HdMoonshineCreateMaterial(_moonshine, Material { 19 | .normal = _upNormal, 20 | .emissive = _black3, 21 | .color = _grey3, 22 | .metalness = _black1, 23 | .roughness = _white1, 24 | .ior = 1.5, 25 | }); 26 | } 27 | 28 | HdMoonshine* _moonshine; 29 | 30 | // some defaults 31 | ImageHandle _black3; 32 | ImageHandle _black1; 33 | ImageHandle _upNormal; 34 | ImageHandle _grey3; 35 | ImageHandle _white1; 36 | MaterialHandle _defaultMaterial; 37 | }; 38 | 39 | PXR_NAMESPACE_CLOSE_SCOPE -------------------------------------------------------------------------------- /src/bin/hydra/renderPass.cpp: -------------------------------------------------------------------------------- 1 | #include "moonshine.h" 2 | 3 | #include "camera.hpp" 4 | #include "renderPass.hpp" 5 | #include "renderBuffer.hpp" 6 | #include "renderDelegate.hpp" 7 | 8 | #include 9 | #include 10 | 11 | PXR_NAMESPACE_OPEN_SCOPE 12 | 13 | HdMoonshineRenderPass::HdMoonshineRenderPass(HdRenderIndex *index, HdRprimCollection const &collection) : HdRenderPass(index, collection) {} 14 | 15 | HdMoonshineRenderPass::~HdMoonshineRenderPass() {} 16 | 17 | void HdMoonshineRenderPass::_Execute(HdRenderPassStateSharedPtr const& renderPassState, TfTokenVector const& renderTags) { 18 | for (const auto aov : renderPassState->GetAovBindings()) { 19 | if (aov.aovName == HdAovTokens->color) { 20 | HdRenderIndex* renderIndex = GetRenderIndex(); 21 | HdMoonshineRenderDelegate* renderDelegate = static_cast(renderIndex->GetRenderDelegate()); 22 | const HdMoonshineCamera* camera = static_cast(renderPassState->GetCamera()); 23 | 24 | HdMoonshineRenderBuffer* renderBuffer = static_cast(aov.renderBuffer); 25 | HdMoonshineRender(renderDelegate->_moonshine, renderBuffer->_sensor, camera->_handle); 26 | } 27 | } 28 | } 29 | 30 | PXR_NAMESPACE_CLOSE_SCOPE -------------------------------------------------------------------------------- /src/bin/hydra/renderPass.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pxr/pxr.h" 4 | #include "pxr/imaging/hd/renderPass.h" 5 | 6 | PXR_NAMESPACE_OPEN_SCOPE 7 | 8 | class HdMoonshineRenderPass final : public HdRenderPass 9 | { 10 | public: 11 | HdMoonshineRenderPass(HdRenderIndex *index, HdRprimCollection const &collection); 12 | ~HdMoonshineRenderPass() override; 13 | protected: 14 | void _Execute(HdRenderPassStateSharedPtr const& renderPassState, TfTokenVector const& renderTags) override; 15 | }; 16 | 17 | PXR_NAMESPACE_CLOSE_SCOPE 18 | -------------------------------------------------------------------------------- /src/bin/hydra/rendererPlugin.cpp: -------------------------------------------------------------------------------- 1 | #include "rendererPlugin.hpp" 2 | #include "renderDelegate.hpp" 3 | 4 | #include "pxr/imaging/hd/rendererPluginRegistry.h" 5 | 6 | PXR_NAMESPACE_OPEN_SCOPE 7 | 8 | TF_REGISTRY_FUNCTION(TfType) { 9 | HdRendererPluginRegistry::Define(); 10 | } 11 | 12 | HdRenderDelegate* HdMoonshinePlugin::CreateRenderDelegate() { 13 | return new HdMoonshineRenderDelegate(); 14 | } 15 | 16 | HdRenderDelegate* HdMoonshinePlugin::CreateRenderDelegate(HdRenderSettingsMap const& settingsMap) 17 | { 18 | return new HdMoonshineRenderDelegate(settingsMap); 19 | } 20 | 21 | void HdMoonshinePlugin::DeleteRenderDelegate(HdRenderDelegate *renderDelegate) { 22 | delete renderDelegate; 23 | } 24 | 25 | bool HdMoonshinePlugin::IsSupported(bool gpuEnabled) const { 26 | return gpuEnabled; 27 | } 28 | 29 | PXR_NAMESPACE_CLOSE_SCOPE -------------------------------------------------------------------------------- /src/bin/hydra/rendererPlugin.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pxr/imaging/hd/rendererPlugin.h" 4 | 5 | PXR_NAMESPACE_OPEN_SCOPE 6 | 7 | class HdMoonshinePlugin final : public HdRendererPlugin 8 | { 9 | public: 10 | HdMoonshinePlugin() = default; 11 | ~HdMoonshinePlugin() = default; 12 | HdRenderDelegate* CreateRenderDelegate() override; 13 | HdRenderDelegate* CreateRenderDelegate(HdRenderSettingsMap const& settingsMap) override; 14 | void DeleteRenderDelegate(HdRenderDelegate *renderDelegate) override; 15 | bool IsSupported(bool gpuEnabled) const override; 16 | private: 17 | HdMoonshinePlugin(const HdMoonshinePlugin&) = delete; 18 | HdMoonshinePlugin &operator =(const HdMoonshinePlugin&) = delete; 19 | }; 20 | 21 | PXR_NAMESPACE_CLOSE_SCOPE 22 | -------------------------------------------------------------------------------- /src/bin/offline.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const vk = @import("vulkan"); 3 | 4 | const engine = @import("engine"); 5 | 6 | const core = engine.core; 7 | const VulkanContext = core.VulkanContext; 8 | const Encoder = core.Encoder; 9 | const Pipeline = engine.hrtsystem.pipeline.StandardPipeline; 10 | const Scene = engine.hrtsystem.Scene; 11 | 12 | const vk_helpers = core.vk_helpers; 13 | const exr = engine.fileformats.exr; 14 | 15 | const vector = engine.vector; 16 | const F32x3 = vector.Vec3(f32); 17 | const Mat4x3 = vector.Mat4x3(f32); 18 | 19 | const Config = struct { 20 | in_filepath: []const u8, // must be gltf/glb 21 | out_filepath: []const u8, // must be exr 22 | skybox_filepath: []const u8, // must be exr 23 | spp: u32, 24 | extent: vk.Extent2D, 25 | 26 | fn fromCli(allocator: std.mem.Allocator) !Config { 27 | const args = try std.process.argsAlloc(allocator); 28 | defer std.process.argsFree(allocator, args); 29 | if (args.len < 4) return error.BadArgs; 30 | 31 | const in_filepath = args[1]; 32 | if (!std.mem.eql(u8, std.fs.path.extension(in_filepath), ".glb") and !std.mem.eql(u8, std.fs.path.extension(in_filepath), ".gltf")) return error.OnlySupportsGltfInput; 33 | 34 | const skybox_filepath = args[2]; 35 | if (!std.mem.eql(u8, std.fs.path.extension(skybox_filepath), ".exr")) return error.OnlySupportsExrSkybox; 36 | 37 | const out_filepath = args[3]; 38 | if (!std.mem.eql(u8, std.fs.path.extension(out_filepath), ".exr")) return error.OnlySupportsExrOutput; 39 | 40 | const spp = if (args.len > 4) try std.fmt.parseInt(u32, args[4], 10) else 16; 41 | 42 | return Config { 43 | .in_filepath = try allocator.dupe(u8, in_filepath), 44 | .out_filepath = try allocator.dupe(u8, out_filepath), 45 | .skybox_filepath = try allocator.dupe(u8, skybox_filepath), 46 | .spp = spp, 47 | .extent = vk.Extent2D { .width = 1280, .height = 720 }, // TODO: cli 48 | }; 49 | } 50 | 51 | fn destroy(self: Config, allocator: std.mem.Allocator) void { 52 | allocator.free(self.in_filepath); 53 | allocator.free(self.out_filepath); 54 | allocator.free(self.skybox_filepath); 55 | } 56 | }; 57 | 58 | const IntervalLogger = struct { 59 | last_time: std.time.Instant, 60 | 61 | fn start() !IntervalLogger { 62 | return IntervalLogger { 63 | .last_time = try std.time.Instant.now(), 64 | }; 65 | } 66 | 67 | fn log(self: *IntervalLogger, state: []const u8) !void { 68 | const new_time = try std.time.Instant.now(); 69 | const elapsed = new_time.since(self.last_time); 70 | const ms = elapsed / std.time.ns_per_ms; 71 | const s = ms / std.time.ms_per_s; 72 | try std.io.getStdOut().writer().print("{}.{:0>3} seconds to {s}\n", .{ s, ms, state }); 73 | self.last_time = new_time; 74 | } 75 | }; 76 | 77 | pub fn main() !void { 78 | var logger = try IntervalLogger.start(); 79 | 80 | var gpa = std.heap.GeneralPurposeAllocator(.{}) {}; 81 | defer _ = gpa.deinit(); 82 | const allocator = gpa.allocator(); 83 | 84 | const config = try Config.fromCli(allocator); 85 | defer config.destroy(allocator); 86 | 87 | const context = try VulkanContext.create(allocator, "offline", &.{}, &engine.hrtsystem.required_device_extensions, &engine.hrtsystem.required_device_features, null); 88 | defer context.destroy(allocator); 89 | 90 | var encoder = try Encoder.create(&context, "main"); 91 | defer encoder.destroy(&context); 92 | 93 | try logger.log("set up initial state"); 94 | 95 | try encoder.begin(); 96 | var scene = try Scene.fromGltfExr(&context, allocator, &encoder, config.in_filepath, config.skybox_filepath, config.extent); 97 | defer scene.destroy(&context, allocator); 98 | try encoder.submitAndIdleUntilDone(&context); 99 | 100 | try logger.log("load world"); 101 | 102 | var pipeline = try Pipeline.create(&context, allocator, .{}, .{ scene.background.sampler }, .{ scene.world.materials.textures.descriptor_layout.handle, scene.world.constant_specta.descriptor_layout.handle }); 103 | defer pipeline.destroy(&context); 104 | 105 | try logger.log("create pipeline"); 106 | 107 | const output_buffer = try core.mem.DownloadBuffer([4]f32).create(&context, scene.camera.sensors.items[0].extent.width * scene.camera.sensors.items[0].extent.height, "output"); 108 | defer output_buffer.destroy(&context); 109 | 110 | // actual ray tracing 111 | { 112 | try encoder.begin(); 113 | 114 | // prepare our stuff 115 | scene.camera.sensors.items[0].recordPrepareForCapture(encoder.buffer, .{ .compute_shader_bit = true }, .{}); 116 | 117 | // bind our stuff 118 | pipeline.recordBindPipeline(encoder.buffer); 119 | pipeline.recordBindAdditionalDescriptorSets(encoder.buffer, .{ scene.world.materials.textures.descriptor_set, scene.world.constant_specta.descriptor_set }); 120 | pipeline.recordPushDescriptors(encoder.buffer, scene.pushDescriptors(0, 0, 0)); 121 | 122 | for (0..config.spp) |sample_count| { 123 | // push our stuff 124 | pipeline.recordPushConstants(encoder.buffer, scene.pushConstants(0, 0, 0, scene.camera.sensors.items[0].sample_count)); 125 | 126 | // trace our stuff 127 | pipeline.recordDispatchThreads2D(encoder.buffer, scene.camera.sensors.items[0].extent); 128 | 129 | // if not last invocation, need barrier cuz we write to images 130 | if (sample_count != config.spp) { 131 | encoder.buffer.pipelineBarrier2(&vk.DependencyInfo { 132 | .image_memory_barrier_count = 1, 133 | .p_image_memory_barriers = &[_]vk.ImageMemoryBarrier2 { 134 | .{ 135 | .src_stage_mask = .{ .compute_shader_bit = true }, 136 | .src_access_mask = if (sample_count == 0) .{ .shader_storage_write_bit = true } else .{ .shader_storage_write_bit = true, .shader_storage_read_bit = true }, 137 | .dst_stage_mask = .{ .compute_shader_bit = true }, 138 | .dst_access_mask = .{ .shader_storage_write_bit = true, .shader_storage_read_bit = true }, 139 | .old_layout = .general, 140 | .new_layout = .general, 141 | .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED, 142 | .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED, 143 | .image = scene.camera.sensors.items[0].image.handle, 144 | .subresource_range = .{ 145 | .aspect_mask = .{ .color_bit = true }, 146 | .base_mip_level = 0, 147 | .level_count = 1, 148 | .base_array_layer = 0, 149 | .layer_count = vk.REMAINING_ARRAY_LAYERS, 150 | }, 151 | } 152 | }, 153 | }); 154 | } 155 | scene.camera.sensors.items[0].sample_count += 1; 156 | } 157 | 158 | // copy our stuff 159 | scene.camera.sensors.items[0].recordPrepareForCopy(encoder.buffer, .{ .compute_shader_bit = true }, .{ .copy_bit = true }); 160 | 161 | // copy rendered image to host-visible staging buffer 162 | encoder.copyImageToBuffer(scene.camera.sensors.items[0].image.handle, .transfer_src_optimal, scene.camera.sensors.items[0].extent, output_buffer.handle); 163 | 164 | try encoder.submitAndIdleUntilDone(&context); 165 | } 166 | 167 | try logger.log("render"); 168 | 169 | // now done with GPU stuff/all rendering; can write from output buffer to exr 170 | try exr.helpers.Rgba2D.save(exr.helpers.Rgba2D { .ptr = output_buffer.mapped, .extent = scene.camera.sensors.items[0].extent }, allocator, config.out_filepath); 171 | 172 | try logger.log("write exr"); 173 | } 174 | -------------------------------------------------------------------------------- /src/bin/online/ObjectPicker.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const vk = @import("vulkan"); 3 | const shaders = @import("shaders"); 4 | 5 | const engine = @import("engine"); 6 | const core = engine.core; 7 | const VulkanContext = core.VulkanContext; 8 | const Encoder = core.Encoder; 9 | 10 | const hrtsystem = engine.hrtsystem; 11 | const Sensor = hrtsystem.Sensor; 12 | const Camera = hrtsystem.CameraManager; 13 | 14 | const F32x2 = engine.vector.Vec2(f32); 15 | 16 | const Self = @This(); 17 | 18 | pub const Intersection = extern struct { 19 | instance_index: i32, // -1 if clicked background 20 | geometry_index: u32, 21 | primitive_index: u32, 22 | barycentrics: F32x2, 23 | 24 | pub fn toClickedObject(self: Intersection) ?ClickedObject { 25 | if (self.instance_index == -1) { 26 | return null; 27 | } else { 28 | return ClickedObject { 29 | .instance_index = @intCast(self.instance_index), 30 | .geometry_index = self.geometry_index, 31 | .primitive_index = self.primitive_index, 32 | .barycentrics = self.barycentrics, 33 | }; 34 | } 35 | } 36 | }; 37 | 38 | pub const ClickedObject = struct { 39 | instance_index: u32, 40 | geometry_index: u32, 41 | primitive_index: u32, 42 | barycentrics: F32x2, 43 | }; 44 | 45 | pub const Pipeline = core.pipeline.Pipeline(.{ 46 | .local_size = vk.Extent3D { .width = 1, .height = 1, .depth = 1 }, 47 | .shader_source = shaders.input, 48 | .PushConstants = extern struct { 49 | camera: Camera.Camera, 50 | aspect_ratio: f32, 51 | click_position: F32x2, 52 | }, 53 | .PushSetBindings = struct { 54 | tlas: vk.AccelerationStructureKHR, 55 | output_image: core.pipeline.StorageImage, 56 | click_data: core.mem.BufferSlice(Intersection), 57 | }, 58 | }); 59 | 60 | buffer: core.mem.Buffer(Intersection, .{ .host_visible_bit = true, .host_coherent_bit = true }, .{ .storage_buffer_bit = true }), 61 | pipeline: Pipeline, 62 | 63 | encoder: Encoder, 64 | ready_fence: vk.Fence, 65 | 66 | pub fn create(vc: *const VulkanContext, allocator: std.mem.Allocator) !Self { 67 | const buffer = try core.mem.Buffer(Intersection, .{ .host_visible_bit = true, .host_coherent_bit = true }, .{ .storage_buffer_bit = true }).create(vc, 1, "object picker"); 68 | errdefer buffer.destroy(vc); 69 | 70 | var pipeline = try Pipeline.create(vc, allocator, .{}, .{}, .{}); 71 | errdefer pipeline.destroy(vc); 72 | 73 | var encoder = try Encoder.create(vc, "object picker"); 74 | errdefer encoder.destroy(vc); 75 | 76 | const ready_fence = try vc.device.createFence(&.{ 77 | .flags = .{}, 78 | }, null); 79 | errdefer vc.device.destroyFence(ready_fence, null); 80 | 81 | return Self { 82 | .buffer = buffer, 83 | .pipeline = pipeline, 84 | 85 | .encoder = encoder, 86 | .ready_fence = ready_fence, 87 | }; 88 | } 89 | 90 | pub fn getClickedObject(self: *Self, vc: *const VulkanContext, accel: vk.AccelerationStructureKHR, normalized_coords: F32x2, camera: Camera.Camera, sensor: Sensor) !?ClickedObject { 91 | // begin 92 | try self.encoder.begin(); 93 | 94 | // bind pipeline + sets 95 | self.pipeline.recordBindPipeline(self.encoder.buffer); 96 | self.pipeline.recordPushDescriptors(self.encoder.buffer, Pipeline.PushSetBindings { 97 | .tlas = accel, 98 | .output_image = .{ .view = sensor.image.view }, 99 | .click_data = self.buffer.deviceSlice(), 100 | }); 101 | 102 | self.pipeline.recordPushConstants(self.encoder.buffer, .{ .camera = camera, .aspect_ratio = sensor.aspectRatio(), .click_position = normalized_coords }); 103 | 104 | // trace rays 105 | self.pipeline.recordDispatchThreads1D(self.encoder.buffer, 1); 106 | 107 | // end 108 | try self.encoder.submit(vc.queue, .{ .fence = self.ready_fence }); 109 | 110 | _ = try vc.device.waitForFences(1, (&self.ready_fence)[0..1], vk.TRUE, std.math.maxInt(u64)); 111 | try vc.device.resetFences(1, (&self.ready_fence)[0..1]); 112 | try vc.device.resetCommandPool(self.encoder.pool, .{}); 113 | 114 | return self.buffer.hostSlice()[0].toClickedObject(); 115 | } 116 | 117 | pub fn destroy(self: *Self, vc: *const VulkanContext) void { 118 | self.buffer.destroy(vc); 119 | self.pipeline.destroy(vc); 120 | self.encoder.destroy(vc); 121 | vc.device.destroyFence(self.ready_fence, null); 122 | } 123 | -------------------------------------------------------------------------------- /src/bin/online/SyncCopier.zig: -------------------------------------------------------------------------------- 1 | // TODO: this should really be killed 2 | // 3 | // really stupid and inefficient way to get stuff from device-local buffers/images back to CPU 4 | // should not be used for "real" transfers that benefit from efficiency 5 | // 6 | // more-so designed for ease-of-use for debugging and inspecting stuff that doesn't usually need to be inspected 7 | 8 | const core = @import("engine").core; 9 | const VulkanContext = core.VulkanContext; 10 | const Encoder = core.Encoder; 11 | 12 | const std = @import("std"); 13 | const vk = @import("vulkan"); 14 | 15 | buffer: core.mem.DownloadBuffer(u8), 16 | 17 | encoder: Encoder, 18 | ready_fence: vk.Fence, 19 | 20 | const Self = @This(); 21 | 22 | pub fn create(vc: *const VulkanContext, max_bytes: u32) !Self { 23 | const buffer = try core.mem.DownloadBuffer(u8).create(vc, max_bytes, "sync copier"); 24 | 25 | var encoder = try Encoder.create(vc, "sync copier"); 26 | errdefer encoder.destroy(vc); 27 | 28 | const ready_fence = try vc.device.createFence(&.{}, null); 29 | 30 | return Self { 31 | .buffer = buffer, 32 | 33 | .encoder = encoder, 34 | .ready_fence = ready_fence, 35 | }; 36 | } 37 | 38 | pub fn copyBufferItem(self: *Self, vc: *const VulkanContext, comptime BufferInner: type, buffer: vk.Buffer, idx: vk.DeviceSize) !BufferInner { 39 | std.debug.assert(@sizeOf(BufferInner) <= self.buffer.len); 40 | 41 | try self.encoder.begin(); 42 | self.encoder.copyBuffer(buffer, self.buffer.handle, &[_]vk.BufferCopy { 43 | .{ 44 | .src_offset = @sizeOf(BufferInner) * idx, 45 | .dst_offset = 0, 46 | .size = @sizeOf(BufferInner), 47 | } 48 | }); 49 | try self.encoder.submit(vc.queue, .{ .fence = self.ready_fence }); 50 | 51 | _ = try vc.device.waitForFences(1, (&self.ready_fence)[0..1], vk.TRUE, std.math.maxInt(u64)); 52 | try vc.device.resetFences(1, (&self.ready_fence)[0..1]); 53 | try vc.device.resetCommandPool(self.encoder.pool, .{}); 54 | 55 | return @as(*BufferInner, @ptrCast(@alignCast(self.buffer.mapped))).*; 56 | } 57 | 58 | pub fn copyImagePixel(self: *Self, vc: *const VulkanContext, comptime PixelType: type, src_image: vk.Image, src_layout: vk.ImageLayout, offset: vk.Offset3D) !PixelType { 59 | std.debug.assert(@sizeOf(PixelType) <= self.buffer.len); 60 | 61 | try self.encoder.begin(); 62 | self.encoder.buffer.copyImageToBuffer(src_image, src_layout, self.buffer.handle, 1, (&vk.BufferImageCopy { 63 | .buffer_offset = 0, 64 | .buffer_row_length = 0, 65 | .buffer_image_height = 0, 66 | .image_subresource = vk.ImageSubresourceLayers { 67 | .aspect_mask = .{ .color_bit = true }, 68 | .mip_level = 0, 69 | .base_array_layer = 0, 70 | .layer_count = 1, 71 | }, 72 | .image_offset = offset, 73 | .image_extent = vk.Extent3D { 74 | .width = 1, 75 | .height = 1, 76 | .depth = 1, 77 | } 78 | })[0..1]); 79 | try self.encoder.submit(vc.queue, .{ .fence = self.ready_fence }); 80 | 81 | _ = try vc.device.waitForFences(1, (&self.ready_fence)[0..1], vk.TRUE, std.math.maxInt(u64)); 82 | try vc.device.resetFences(1, (&self.ready_fence)[0..1]); 83 | try vc.device.resetCommandPool(self.encoder.pool, .{}); 84 | 85 | return @as(*PixelType, @ptrCast(@alignCast(self.buffer.mapped))).*; 86 | } 87 | 88 | pub fn destroy(self: *Self, vc: *const VulkanContext) void { 89 | self.buffer.destroy(vc); 90 | self.encoder.destroy(vc); 91 | vc.device.destroyFence(self.ready_fence, null); 92 | } 93 | -------------------------------------------------------------------------------- /src/bin/online/input.hlsl: -------------------------------------------------------------------------------- 1 | #include "../../lib/hrtsystem/shaders/camera.hlsl" 2 | #include "../../lib/hrtsystem/shaders/intersection.hlsl" 3 | 4 | [[vk::binding(0, 0)]] RaytracingAccelerationStructure TLAS; 5 | [[vk::binding(1, 0)]] RWTexture2D dOutputImage; 6 | [[vk::binding(2, 0)]] RWStructuredBuffer click_data; 7 | 8 | struct PushConsts { 9 | Camera camera; 10 | float2 coords; 11 | }; 12 | [[vk::push_constant]] PushConsts pushConsts; 13 | 14 | [numthreads(1, 1, 1)] 15 | void main() { 16 | Camera camera = pushConsts.camera; 17 | // make camera have perfect focus 18 | camera.thinLens.focusDistance = 1.0f; 19 | camera.thinLens.aperture = 0.0f; 20 | Ray ray = pushConsts.camera.generateRay(pushConsts.coords, float2(0, 0)); 21 | 22 | click_data[0] = Intersection::find(TLAS, ray); 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/README.md: -------------------------------------------------------------------------------- 1 | # Core engine 2 | 3 | ### Systems 4 | Each "system" is a relatively self-contained... system that is designed to do a specific thing. 5 | Systems require certain instance/device functions to be enabled in order to use them, so they export them from their root file. 6 | 7 | #### Current systems 8 | * `core` - basic things like context, allocation, commonly used utilities, etc 9 | * `hrtsystem` - general hardware-RT ray traced rendering tasks and scene representation 10 | * `displaysystem` - platform-agnostic abstraction for rendering images and managing e.g., swapchain, double-buffering, presentation, etc 11 | -------------------------------------------------------------------------------- /src/lib/Window.zig: -------------------------------------------------------------------------------- 1 | // Thin wrapper for GLFW atm 2 | 3 | const c = @import("glfw"); 4 | const vk = @import("vulkan"); 5 | const std = @import("std"); 6 | 7 | const Error = error { 8 | InitFail, 9 | WindowCreateFail, 10 | SurfaceCreateFail, 11 | }; 12 | 13 | const Self = @This(); 14 | 15 | handle: *c.GLFWwindow, 16 | 17 | pub fn create(width: u32, height: u32, app_name: [*:0]const u8) Error!Self { 18 | const Callback = struct { 19 | fn callback(code: c_int, message: [*c]const u8) callconv(.C) void { 20 | std.log.warn("glfw: {}: {s}", .{code, message}); 21 | } 22 | }; 23 | _ = c.glfwSetErrorCallback(Callback.callback); 24 | 25 | if (c.glfwInit() != c.GLFW_TRUE) return Error.InitFail; 26 | 27 | c.glfwWindowHint(c.GLFW_CLIENT_API, c.GLFW_NO_API); 28 | 29 | const handle = c.glfwCreateWindow(@intCast(width), @intCast(height), app_name, null, null) orelse { 30 | c.glfwTerminate(); 31 | return Error.WindowCreateFail; 32 | }; 33 | 34 | return Self { 35 | .handle = handle, 36 | }; 37 | } 38 | 39 | pub fn getPhysicalDevicePresentationSupport(instance: vk.Instance, device: vk.PhysicalDevice, idx: u32) bool { 40 | return c.glfwGetPhysicalDevicePresentationSupport(instance, device, idx) == c.GLFW_TRUE; 41 | } 42 | 43 | // abusing the fact a little bit that we know that glfw always asks for two extensions 44 | pub fn getRequiredInstanceExtensions(self: *const Self) [2][*:0]const u8 { 45 | _ = self; // ensure we're initialized 46 | 47 | var glfw_extension_count: u32 = 0; 48 | const extensions = c.glfwGetRequiredInstanceExtensions(&glfw_extension_count); 49 | std.debug.assert(glfw_extension_count == 2); 50 | 51 | return @as([*]const [*:0]const u8, @ptrCast(extensions))[0..2].*; 52 | } 53 | 54 | pub fn shouldClose(self: *const Self) bool { 55 | return c.glfwWindowShouldClose(self.handle) == c.GLFW_TRUE; 56 | } 57 | 58 | pub const Mode = enum(c_int) { 59 | normal = c.GLFW_CURSOR_NORMAL, 60 | hidden = c.GLFW_CURSOR_HIDDEN, 61 | disabled = c.GLFW_CURSOR_DISABLED, 62 | }; 63 | pub fn setCursorMode(self: *const Self, value: Mode) void { 64 | c.glfwSetInputMode(self.handle, c.GLFW_CURSOR, @intFromEnum(value)); 65 | } 66 | 67 | pub fn pollEvents(self: *const Self) void { 68 | _ = self; // just ensure we're initialized 69 | c.glfwPollEvents(); 70 | } 71 | 72 | pub fn createSurface(self: *const Self, instance: vk.Instance) Error!vk.SurfaceKHR { 73 | var surface: vk.SurfaceKHR = undefined; 74 | if (c.glfwCreateWindowSurface(instance, self.handle, null, &surface) != vk.Result.success) return Error.SurfaceCreateFail; // this could give more details 75 | return surface; 76 | } 77 | 78 | pub fn getExtent(self: *const Self) vk.Extent2D { 79 | var width: c_int = undefined; 80 | var height: c_int = undefined; 81 | c.glfwGetFramebufferSize(self.handle, &width, &height); 82 | return vk.Extent2D { 83 | .width = @intCast(width), 84 | .height = @intCast(height), 85 | }; 86 | } 87 | 88 | pub fn destroy(self: *const Self) void { 89 | c.glfwDestroyWindow(self.handle); 90 | c.glfwTerminate(); 91 | } 92 | -------------------------------------------------------------------------------- /src/lib/core/DestructionQueue.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const vk = @import("vulkan"); 3 | 4 | const core = @import("./core.zig"); 5 | const VulkanContext = core.VulkanContext; 6 | const DeviceBuffer = core.Allocator.DeviceBuffer; 7 | 8 | // type erased Vulkan object 9 | const Destruction = struct { 10 | object_type: vk.ObjectType, 11 | destroyee: u64, 12 | 13 | fn destroy(self: Destruction, vc: *const VulkanContext) void { 14 | switch (self.object_type) { 15 | .swapchain_khr => if (comptime @hasField(VulkanContext.Device.Wrapper.Dispatch, "vkDestroySwapchainKHR")) vc.device.destroySwapchainKHR(@enumFromInt(self.destroyee), null) else unreachable, 16 | .pipeline_layout => vc.device.destroyPipelineLayout(@enumFromInt(self.destroyee), null), 17 | .pipeline => vc.device.destroyPipeline(@enumFromInt(self.destroyee), null), 18 | .buffer => vc.device.destroyBuffer(@enumFromInt(self.destroyee), null), 19 | .image_view => vc.device.destroyImageView(@enumFromInt(self.destroyee), null), 20 | .image => vc.device.destroyImage(@enumFromInt(self.destroyee), null), 21 | .device_memory => vc.device.freeMemory(@enumFromInt(self.destroyee), null), 22 | .acceleration_structure_khr => vc.device.destroyAccelerationStructureKHR(@enumFromInt(self.destroyee), null), 23 | else => unreachable, // TODO 24 | } 25 | } 26 | }; 27 | 28 | // TODO: this should be an SoA type of thing like list((tag, list(union))) 29 | queue: std.ArrayListUnmanaged(Destruction) = .{}, 30 | 31 | const Self = @This(); 32 | 33 | // works on any type exclusively made up of Vulkan objects and primitives 34 | pub fn append(self: *Self, allocator: std.mem.Allocator, item: anytype) !void { 35 | const T = @TypeOf(item); 36 | 37 | if (comptime @typeInfo(T) == .@"struct") { 38 | inline for (@typeInfo(T).@"struct".fields) |field| { 39 | if (field.type != void and @typeInfo(field.type) != .int) { 40 | try self.append(allocator, @field(item, field.name)); 41 | } 42 | } 43 | } else { 44 | try self.queue.append(allocator, Destruction { 45 | .object_type = comptime core.vk_helpers.typeToObjectType(T), 46 | .destroyee = @intFromEnum(item), 47 | }); 48 | } 49 | } 50 | 51 | pub fn clear(self: *Self, vc: *const VulkanContext) void { 52 | for (self.queue.items) |*item| item.destroy(vc); 53 | self.queue.clearRetainingCapacity(); 54 | } 55 | 56 | pub fn destroy(self: *Self, vc: *const VulkanContext, allocator: std.mem.Allocator) void { 57 | self.clear(vc); 58 | self.queue.deinit(allocator); 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/core/Image.zig: -------------------------------------------------------------------------------- 1 | const vk = @import("vulkan"); 2 | const std = @import("std"); 3 | 4 | const engine = @import("../engine.zig"); 5 | const core = engine.core; 6 | const vk_helpers = core.vk_helpers; 7 | const VulkanContext = core.VulkanContext; 8 | 9 | const Self = @This(); 10 | 11 | handle: vk.Image, 12 | view: vk.ImageView, 13 | memory: vk.DeviceMemory, 14 | 15 | pub fn create(vc: *const VulkanContext, size: vk.Extent2D, usage: vk.ImageUsageFlags, format: vk.Format, with_mips: bool, name: [:0]const u8) !Self { 16 | const extent = vk.Extent3D { 17 | .width = size.width, 18 | .height = size.height, 19 | .depth = 1, 20 | }; 21 | 22 | const image_create_info = vk.ImageCreateInfo { 23 | .image_type = if (extent.height == 1 and extent.width != 1) .@"1d" else .@"2d", 24 | .format = format, 25 | .extent = extent, 26 | .mip_levels = if (with_mips) std.math.log2(@max(extent.width, extent.height, extent.depth)) + 1 else 1, 27 | .array_layers = 1, 28 | .samples = .{ .@"1_bit" = true }, 29 | .tiling = .optimal, 30 | .usage = usage, 31 | .sharing_mode = .exclusive, 32 | .queue_family_index_count = 0, 33 | .p_queue_family_indices = undefined, 34 | .initial_layout = .undefined, 35 | }; 36 | 37 | const handle = try vc.device.createImage(&image_create_info, null); 38 | errdefer vc.device.destroyImage(handle, null); 39 | if (name.len != 0) try vk_helpers.setDebugName(vc.device, handle, name); 40 | 41 | const mem_requirements = vc.device.getImageMemoryRequirements(handle); 42 | 43 | const memory = try vc.device.allocateMemory(&.{ 44 | .allocation_size = mem_requirements.size, 45 | .memory_type_index = try vc.findMemoryType(mem_requirements.memory_type_bits, .{ .device_local_bit = true }), 46 | }, null); 47 | errdefer vc.device.freeMemory(memory, null); 48 | 49 | try vc.device.bindImageMemory(handle, memory, 0); 50 | 51 | const view_create_info = vk.ImageViewCreateInfo { 52 | .flags = .{}, 53 | .image = handle, 54 | .view_type = if (extent.height == 1 and extent.width != 1) vk.ImageViewType.@"1d" else vk.ImageViewType.@"2d", 55 | .format = format, 56 | .components = .{ 57 | .r = .identity, 58 | .g = .identity, 59 | .b = .identity, 60 | .a = .identity, 61 | }, 62 | .subresource_range = .{ 63 | .aspect_mask = .{ .color_bit = true }, 64 | .base_mip_level = 0, 65 | .level_count = vk.REMAINING_MIP_LEVELS, 66 | .base_array_layer = 0, 67 | .layer_count = vk.REMAINING_ARRAY_LAYERS, 68 | }, 69 | }; 70 | 71 | const view = try vc.device.createImageView(&view_create_info, null); 72 | errdefer vc.device.destroyImageView(view, null); 73 | 74 | return Self { 75 | .memory = memory, 76 | .handle = handle, 77 | .view = view, 78 | }; 79 | } 80 | 81 | pub fn destroy(self: Self, vc: *const VulkanContext) void { 82 | vc.device.destroyImageView(self.view, null); 83 | vc.device.destroyImage(self.handle, null); 84 | vc.device.freeMemory(self.memory, null); 85 | } 86 | -------------------------------------------------------------------------------- /src/lib/core/core.zig: -------------------------------------------------------------------------------- 1 | pub const VulkanContext = @import("./VulkanContext.zig"); 2 | pub const Encoder = @import("./Encoder.zig"); 3 | pub const DestructionQueue = @import("./DestructionQueue.zig"); 4 | pub const Image = @import("./Image.zig"); 5 | 6 | pub const mem = @import("./mem.zig"); 7 | pub const descriptor = @import("./descriptor.zig"); 8 | pub const pipeline = @import("./pipeline.zig"); 9 | 10 | pub const vk_helpers = @import("./vk_helpers.zig"); 11 | -------------------------------------------------------------------------------- /src/lib/core/descriptor.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const vk = @import("vulkan"); 3 | const core = @import("../engine.zig").core; 4 | const vk_helpers = core.vk_helpers; 5 | const VulkanContext = core.VulkanContext; 6 | 7 | pub const Binding = struct { 8 | descriptor_type: vk.DescriptorType, 9 | descriptor_count: u32, 10 | stage_flags: vk.ShaderStageFlags, 11 | binding_flags: vk.DescriptorBindingFlags = .{}, 12 | }; 13 | 14 | pub fn DescriptorLayout(comptime bindings: []const Binding, comptime layout_flags: vk.DescriptorSetLayoutCreateFlags, comptime max_sets: comptime_int, comptime debug_name: [*:0]const u8) type { 15 | return struct { 16 | handle: vk.DescriptorSetLayout, 17 | pool: if (is_push_descriptor) void else vk.DescriptorPool, 18 | 19 | const Self = @This(); 20 | 21 | pub const sampler_count = blk: { 22 | var count = 0; 23 | for (bindings) |binding| { 24 | if (binding.descriptor_type == .sampler or binding.descriptor_type == .combined_image_sampler) { 25 | count += binding.descriptor_count; 26 | } 27 | } 28 | break :blk count; 29 | }; 30 | 31 | const is_push_descriptor = layout_flags.contains(.{ .push_descriptor_bit = true }); 32 | 33 | pub fn create(vc: *const VulkanContext, samplers: [sampler_count]vk.Sampler) !Self { 34 | var vk_bindings: [bindings.len]vk.DescriptorSetLayoutBinding = undefined; 35 | const vk_binding_flags = blk: { 36 | comptime var vk_binding_flags: [bindings.len]vk.DescriptorBindingFlags = undefined; 37 | comptime var samplers_so_far = 0; 38 | inline for (bindings, &vk_bindings, &vk_binding_flags, 0..) |binding, *vk_binding, *vk_binding_flag, binding_index| { 39 | vk_binding.* = vk.DescriptorSetLayoutBinding { 40 | .binding = binding_index, 41 | .descriptor_type = binding.descriptor_type, 42 | .descriptor_count = binding.descriptor_count, 43 | .stage_flags = binding.stage_flags, 44 | }; 45 | if (binding.descriptor_type == .sampler or binding.descriptor_type == .combined_image_sampler) { 46 | vk_binding.p_immutable_samplers = samplers[samplers_so_far..samplers_so_far + binding.descriptor_count]; 47 | samplers_so_far += binding.descriptor_count; 48 | } 49 | vk_binding_flag.* = binding.binding_flags; 50 | } 51 | break :blk vk_binding_flags; 52 | }; 53 | const create_info = vk.DescriptorSetLayoutCreateInfo { 54 | .flags = layout_flags, 55 | .binding_count = bindings.len, 56 | .p_bindings = &vk_bindings, 57 | .p_next = (&vk.DescriptorSetLayoutBindingFlagsCreateInfo { 58 | .binding_count = bindings.len, 59 | .p_binding_flags = &vk_binding_flags, 60 | })[0..1], 61 | }; 62 | const handle = try vc.device.createDescriptorSetLayout(&create_info, null); 63 | errdefer vc.device.destroyDescriptorSetLayout(handle, null); 64 | 65 | try vk_helpers.setDebugName(vc.device, handle, debug_name); 66 | 67 | const pool_sizes = blk: { 68 | comptime var pool_sizes: [bindings.len]vk.DescriptorPoolSize = undefined; 69 | comptime for (&pool_sizes, bindings) |*pool_size, binding| { 70 | pool_size.* = .{ 71 | .type = binding.descriptor_type, 72 | .descriptor_count = binding.descriptor_count * max_sets, 73 | }; 74 | }; 75 | break :blk pool_sizes; 76 | }; 77 | 78 | return Self { 79 | .handle = handle, 80 | .pool = if (is_push_descriptor) {} else try vc.device.createDescriptorPool(&.{ 81 | .flags = .{}, 82 | .max_sets = max_sets, 83 | .pool_size_count = pool_sizes.len, 84 | .p_pool_sizes = &pool_sizes, 85 | }, null), 86 | }; 87 | } 88 | 89 | pub usingnamespace if (is_push_descriptor) struct {} else struct { 90 | pub fn allocate_set(self: *const Self, vc: *const VulkanContext, writes: [bindings.len]vk.WriteDescriptorSet) !vk.DescriptorSet { 91 | var descriptor_set: vk.DescriptorSet = undefined; 92 | 93 | try vc.device.allocateDescriptorSets(&vk.DescriptorSetAllocateInfo { 94 | .descriptor_pool = self.pool, 95 | .descriptor_set_count = 1, 96 | .p_set_layouts = (&self.handle)[0..1], 97 | }, (&descriptor_set)[0..1]); 98 | 99 | // avoid writing descriptors in certain invalid states: 100 | // 1. descriptor type is sampler (only immutable samplers are supported) 101 | // 2. descriptor count is zero 102 | // 3. buffer is vk_null_handle 103 | var valid_writes: [bindings.len]vk.WriteDescriptorSet = undefined; 104 | var valid_write_count: u32 = 0; 105 | for (writes) |write| { 106 | if (write.descriptor_type == .sampler) continue; 107 | if (write.descriptor_count == 0) continue; 108 | switch (write.descriptor_type) { 109 | .storage_buffer => if (write.p_buffer_info[0].buffer == .null_handle) continue, 110 | else => {}, 111 | } 112 | 113 | valid_writes[valid_write_count] = write; 114 | valid_writes[valid_write_count].dst_set = descriptor_set; 115 | valid_write_count += 1; 116 | } 117 | 118 | vc.device.updateDescriptorSets(valid_write_count, &valid_writes, 0, undefined); 119 | 120 | return descriptor_set; 121 | } 122 | }; 123 | 124 | pub fn destroy(self: *Self, vc: *const VulkanContext) void { 125 | if (!is_push_descriptor) vc.device.destroyDescriptorPool(self.pool, null); 126 | vc.device.destroyDescriptorSetLayout(self.handle, null); 127 | } 128 | }; 129 | } 130 | -------------------------------------------------------------------------------- /src/lib/core/shader_source.zig: -------------------------------------------------------------------------------- 1 | name: [:0]const u8, 2 | code: []const u32, 3 | command: []const []const u8, -------------------------------------------------------------------------------- /src/lib/core/vk_helpers.zig: -------------------------------------------------------------------------------- 1 | const vk = @import("vulkan"); 2 | const std = @import("std"); 3 | const VulkanContext = @import("./VulkanContext.zig"); 4 | const build_options = @import("build_options"); 5 | 6 | pub fn typeToObjectType(comptime in: type) vk.ObjectType { 7 | return switch(in) { 8 | vk.DescriptorSetLayout => .descriptor_set_layout, 9 | vk.DescriptorSet => .descriptor_set, 10 | vk.Buffer => .buffer, 11 | vk.CommandBuffer => .command_buffer, 12 | vk.Image => .image, 13 | vk.Pipeline => .pipeline, 14 | vk.PipelineLayout => .pipeline_layout, 15 | vk.ShaderModule => .shader_module, 16 | vk.DeviceMemory => .device_memory, 17 | vk.SwapchainKHR => .swapchain_khr, 18 | vk.ImageView => .image_view, 19 | vk.AccelerationStructureKHR => .acceleration_structure_khr, 20 | else => @compileError("unknown type " ++ @typeName(in)), // TODO: add more 21 | }; 22 | } 23 | 24 | pub fn setDebugName(device: VulkanContext.Device, object: anytype, name: [*:0]const u8) !void { 25 | // TODO: this does not make sense, debug names can be needed separately from validation 26 | // for other types of debugging/profiling 27 | const want_debug_names = build_options.vk_validation != .ignore; 28 | 29 | if ((comptime want_debug_names) and object != .null_handle) { 30 | try device.setDebugUtilsObjectNameEXT(&.{ 31 | .object_type = comptime typeToObjectType(@TypeOf(object)), 32 | .object_handle = @intFromEnum(object), 33 | .p_object_name = name, 34 | }); 35 | } 36 | } 37 | 38 | pub fn texelBlockSize(format: vk.Format) vk.DeviceSize { 39 | return switch (format) { 40 | .r8_unorm => 1, 41 | .r8g8_unorm => 2, 42 | .r8g8b8a8_srgb, .r32_sfloat => 4, 43 | .r32g32_sfloat => 8, 44 | .r32g32b32a32_sfloat => 16, 45 | else => unreachable, // TODO 46 | }; 47 | } 48 | 49 | pub fn typeToFormat(comptime in: type) vk.Format { 50 | const vector = @import("../engine.zig").vector; 51 | return switch (in) { 52 | u8 => .r8_unorm, 53 | vector.Vec2(u8) => .r8g8_unorm, 54 | vector.Vec4(u8) => .r8g8b8a8_srgb, // TODO: not assume srgb 55 | f32 => .r32_sfloat, 56 | vector.Vec2(f32) => .r32g32_sfloat, 57 | vector.Vec4(f32) => .r32g32b32a32_sfloat, 58 | else => unreachable, // TODO 59 | }; 60 | } 61 | 62 | 63 | fn GetVkSliceInternal(comptime func: anytype) type { 64 | const params = @typeInfo(@TypeOf(func)).@"fn".params; 65 | const OptionalType = params[params.len - 1].type.?; 66 | const PtrType = @typeInfo(OptionalType).optional.child; 67 | return @typeInfo(PtrType).pointer.child; 68 | } 69 | 70 | fn GetVkSliceBoundedReturn(comptime max_size: comptime_int, comptime func: anytype) type { 71 | const FuncReturn = @typeInfo(@TypeOf(func)).@"fn".return_type.?; 72 | 73 | const BaseReturn = std.BoundedArray(GetVkSliceInternal(func), max_size); 74 | 75 | return switch (@typeInfo(FuncReturn)) { 76 | .error_union => |err| err.error_set!BaseReturn, 77 | .void => BaseReturn, 78 | else => unreachable, 79 | }; 80 | } 81 | 82 | // wrapper around functions like vkGetPhysicalDeviceSurfacePresentModesKHR 83 | // they have some initial args, then a length and output address 84 | // this is designed for small stuff to allocate on stack, where size maximum size is comptime known 85 | // 86 | // in optimized modes, only calls the actual function only once, based on the assumption that the caller correctly predicts max possible return size. 87 | // in safe modes, checks that the actual size does not exceed the maximum size, which may call the function more than once 88 | pub fn getVkSliceBounded(comptime max_size: comptime_int, func: anytype, partial_args: anytype) GetVkSliceBoundedReturn(max_size, func) { 89 | const T = GetVkSliceInternal(func); 90 | 91 | var buffer: [max_size]T = undefined; 92 | var len: u32 = max_size; 93 | 94 | const always_succeeds = @typeInfo(@TypeOf(func)).@"fn".return_type == void; 95 | if (always_succeeds) { 96 | if (std.debug.runtime_safety) { 97 | var actual_len: u32 = undefined; 98 | @call(.auto, func, partial_args ++ .{ &actual_len, null }); 99 | std.debug.assert(actual_len < max_size); 100 | } 101 | @call(.auto, func, partial_args ++ .{ &len, &buffer }); 102 | } else { 103 | const res = try @call(.auto, func, partial_args ++ .{ &len, &buffer }); 104 | std.debug.assert(res != .incomplete); 105 | } 106 | 107 | return std.BoundedArray(T, max_size) { 108 | .buffer = buffer, 109 | .len = @intCast(len), 110 | }; 111 | } 112 | 113 | pub fn getVkSlice(allocator: std.mem.Allocator, func: anytype, partial_args: anytype) ![]GetVkSliceInternal(func) { 114 | const T = GetVkSliceInternal(func); 115 | 116 | var len: u32 = undefined; 117 | _ = try @call(.auto, func, partial_args ++ .{ &len, null }); 118 | 119 | const buffer = try allocator.alloc(T, len); 120 | 121 | const res = try @call(.auto, func, partial_args ++ .{ &len, buffer.ptr }); 122 | std.debug.assert(res != .incomplete); 123 | 124 | return buffer; 125 | } 126 | -------------------------------------------------------------------------------- /src/lib/displaysystem/Display.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const vk = @import("vulkan"); 3 | 4 | const engine = @import("../engine.zig"); 5 | const core = engine.core; 6 | const VulkanContext = core.VulkanContext; 7 | const DestructionQueue = core.DestructionQueue; 8 | const Encoder = core.Encoder; 9 | const vk_helpers = core.vk_helpers; 10 | 11 | const Swapchain = engine.displaysystem.Swapchain; 12 | 13 | const metrics = @import("build_options").vk_metrics; 14 | 15 | // DOUBLE BUFFER STRATEGY 16 | // This uses the strategy that I think is most decent while being the simplest to implement. 17 | // 18 | // We have two "frames in flight", frame A and frame B, which cycle. 19 | // While frame A is being processed by the GPU and displayed, we are recording frame B. 20 | // Essentially this means that while frame A is doing GPU work, frame B is doing CPU work. 21 | // 22 | // This means that for e.g., vertex animation, we don't need to keep two separate GPU vertex buffers, 23 | // as just one GPU task is being done at a time. We just queue the update in command buffer B via a 24 | // buffer copy or update command, which doesn't affect the work of command buffer A. 25 | // 26 | // The catch here is that we must keep two copies of non-static GPU-accesible host data, as when we are updating 27 | // the data for command buffer B, command buffer A could be using that data in another operation. 28 | // In many cases, this can be avoided by using vkCmdUpdateBuffer rather than a transfer operation. 29 | pub const frames_in_flight = 2; 30 | 31 | const Self = @This(); 32 | 33 | frames: [frames_in_flight]Frame, 34 | frame_index: u8, 35 | 36 | swapchain: Swapchain, 37 | 38 | timestamp_period: if (metrics) f32 else void, 39 | last_frame_time_ns: if (metrics) f64 else void, 40 | 41 | // uses initial_extent as the render extent -- that is, the buffer that is actually being rendered into, irrespective of window size 42 | // then during rendering the render buffer is blitted into the swapchain images 43 | pub fn create(vc: *const VulkanContext, initial_extent: vk.Extent2D, surface: vk.SurfaceKHR) !Self { 44 | var swapchain = try Swapchain.create(vc, initial_extent, surface); 45 | errdefer swapchain.destroy(vc); 46 | 47 | var frames: [frames_in_flight]Frame = undefined; 48 | inline for (&frames, 0..) |*frame, i| { 49 | frame.* = try Frame.create(vc, std.fmt.comptimePrint("frame {}", .{i}), i != 0); 50 | } 51 | 52 | const timestamp_period = if (metrics) blk: { 53 | var properties = vk.PhysicalDeviceProperties2 { 54 | .properties = undefined, 55 | }; 56 | 57 | vc.instance.getPhysicalDeviceProperties2(vc.physical_device.handle, &properties); 58 | 59 | break :blk properties.properties.limits.timestamp_period; 60 | } else {}; 61 | 62 | return Self { 63 | .swapchain = swapchain, 64 | .frames = frames, 65 | .frame_index = 0, 66 | 67 | .timestamp_period = timestamp_period, 68 | .last_frame_time_ns = if (metrics) 0.0 else {}, 69 | }; 70 | } 71 | 72 | pub fn destroy(self: *Self, vc: *const VulkanContext) void { 73 | self.swapchain.destroy(vc); 74 | inline for (&self.frames) |*frame| { 75 | frame.destroy(vc); 76 | } 77 | } 78 | 79 | pub fn startFrame(self: *Self, vc: *const VulkanContext) !*Encoder { 80 | const frame = &self.frames[self.frame_index]; 81 | 82 | _ = try self.swapchain.acquireNextImage(vc, frame.image_acquired); 83 | 84 | try frame.encoder.begin(); 85 | 86 | if (metrics) frame.encoder.buffer.writeTimestamp2(.{ .top_of_pipe_bit = true }, frame.query_pool, 0); 87 | 88 | return &frame.encoder; 89 | } 90 | 91 | pub fn recreate(self: *Self, vc: *const VulkanContext, new_extent: vk.Extent2D) !vk.SwapchainKHR { 92 | return try self.swapchain.recreate(vc, new_extent); 93 | } 94 | 95 | pub fn endFrame(self: *Self, vc: *const VulkanContext) !vk.Result { 96 | const result = blk: { 97 | const frame = self.frames[self.frame_index]; 98 | 99 | if (metrics) frame.encoder.buffer.writeTimestamp2(.{ .bottom_of_pipe_bit = true }, frame.query_pool, 1); 100 | 101 | try frame.encoder.submit(vc.queue, .{ 102 | .wait_semaphore_infos = &[_]vk.SemaphoreSubmitInfoKHR { 103 | .{ 104 | .semaphore = frame.image_acquired, 105 | .value = 0, 106 | .stage_mask = .{ .color_attachment_output_bit = true }, 107 | .device_index = 0, 108 | } 109 | }, 110 | .signal_semaphore_infos = &[_]vk.SemaphoreSubmitInfoKHR { 111 | .{ 112 | .semaphore = frame.command_completed, 113 | .value = 0, 114 | .stage_mask = .{ .color_attachment_output_bit = true }, 115 | .device_index = 0, 116 | } 117 | }, 118 | .fence = frame.fence, 119 | }); 120 | 121 | // TODO: the synchronization here is not sufficient, as the semaphore 122 | // may still be in use by the previous usage of this swapchain image. 123 | // fix this with VK_EXT_swapchain_maintenance1 once it becomes 124 | // more widely supported. 125 | break :blk self.swapchain.present(vc, frame.command_completed); 126 | }; 127 | 128 | self.frame_index = (self.frame_index + 1) % frames_in_flight; 129 | 130 | // wait for next frame to ensure CPU is not too far ahead of GPU 131 | var next_frame = &self.frames[self.frame_index]; 132 | _ = try vc.device.waitForFences(1, (&next_frame.fence)[0..1], vk.TRUE, std.math.maxInt(u64)); 133 | 134 | // collect metrics if enabled 135 | if (metrics) { 136 | var timestamps: [2]u64 = undefined; 137 | const query_result = try vc.device.getQueryPoolResults(next_frame.query_pool, 0, 2, 2 * @sizeOf(u64), ×tamps, @sizeOf(u64), .{.@"64_bit" = true }); 138 | self.last_frame_time_ns = if (query_result == .success) @as(f64, @floatFromInt(timestamps[1] - timestamps[0])) * self.timestamp_period else std.math.nan(f64); 139 | } 140 | 141 | // reset resources associated with next frame so it is ready for use on next startFrame 142 | try next_frame.reset(vc); 143 | 144 | return result; 145 | } 146 | 147 | const Frame = struct { 148 | image_acquired: vk.Semaphore, 149 | command_completed: vk.Semaphore, 150 | fence: vk.Fence, 151 | 152 | encoder: Encoder, 153 | 154 | query_pool: if (metrics) vk.QueryPool else void, 155 | 156 | fn create(vc: *const VulkanContext, name: [*:0]const u8, fence_initially_signaled: bool) !Frame { 157 | const image_acquired = try vc.device.createSemaphore(&.{}, null); 158 | errdefer vc.device.destroySemaphore(image_acquired, null); 159 | 160 | const command_completed = try vc.device.createSemaphore(&.{}, null); 161 | errdefer vc.device.destroySemaphore(command_completed, null); 162 | 163 | const fence = try vc.device.createFence(&.{ 164 | .flags = if (fence_initially_signaled) .{ .signaled_bit = true } else .{}, 165 | }, null); 166 | 167 | var encoder = try Encoder.create(vc, name); 168 | errdefer encoder.destroy(vc); 169 | 170 | const query_pool = if (metrics) try vc.device.createQueryPool(&.{ 171 | .query_type = .timestamp, 172 | .query_count = 2, 173 | }, null) else undefined; 174 | errdefer if (metrics) vc.device.destroyQueryPool(query_pool, null); 175 | if (metrics) vc.device.resetQueryPool(query_pool, 0, 2); 176 | 177 | return Frame { 178 | .image_acquired = image_acquired, 179 | .command_completed = command_completed, 180 | .fence = fence, 181 | 182 | .encoder = encoder, 183 | 184 | .query_pool = query_pool, 185 | }; 186 | } 187 | 188 | // frame must not be in use 189 | fn reset(self: *Frame, vc: *const VulkanContext) !void { 190 | try vc.device.resetFences(1, (&self.fence)[0..1]); 191 | 192 | if (metrics) { 193 | vc.device.resetQueryPool(self.query_pool, 0, 2); 194 | } 195 | try vc.device.resetCommandPool(self.encoder.pool, .{}); 196 | self.encoder.clearResources(vc); 197 | } 198 | 199 | fn destroy(self: *Frame, vc: *const VulkanContext) void { 200 | vc.device.destroySemaphore(self.image_acquired, null); 201 | vc.device.destroySemaphore(self.command_completed, null); 202 | vc.device.destroyFence(self.fence, null); 203 | self.encoder.destroy(vc); 204 | if (metrics) vc.device.destroyQueryPool(self.query_pool, null); 205 | } 206 | }; 207 | -------------------------------------------------------------------------------- /src/lib/displaysystem/Swapchain.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const vk = @import("vulkan"); 3 | 4 | const engine = @import("../engine.zig"); 5 | const VulkanContext = engine.core.VulkanContext; 6 | const vk_helpers = engine.core.vk_helpers; 7 | 8 | const SwapchainError = error { 9 | InvalidSurfaceDimensions, 10 | }; 11 | 12 | pub const max_image_count = 3; 13 | 14 | surface: vk.SurfaceKHR, 15 | handle: vk.SwapchainKHR, 16 | images: std.BoundedArray(vk.Image, max_image_count), 17 | image_index: u32, 18 | extent: vk.Extent2D, 19 | image_format: vk.Format, 20 | 21 | const Self = @This(); 22 | 23 | // takes ownership of surface 24 | pub fn create(vc: *const VulkanContext, ideal_extent: vk.Extent2D, surface: vk.SurfaceKHR) !Self { 25 | return try createFromOld(vc, ideal_extent, surface, .null_handle); 26 | } 27 | 28 | fn createFromOld(vc: *const VulkanContext, ideal_extent: vk.Extent2D, surface: vk.SurfaceKHR, old_handle: vk.SwapchainKHR) !Self { 29 | const settings = try SwapSettings.find(vc, ideal_extent, surface); 30 | 31 | const queue_family_indices = [_]u32{ vc.physical_device.queue_family_index }; 32 | 33 | const handle = try vc.device.createSwapchainKHR(&.{ 34 | .surface = surface, 35 | .min_image_count = settings.image_count, 36 | .image_format = settings.format.format, 37 | .image_color_space = settings.format.color_space, 38 | .image_extent = settings.extent, 39 | .image_array_layers = 1, 40 | .image_usage = .{ .color_attachment_bit = true, .transfer_dst_bit = true }, 41 | .image_sharing_mode = settings.image_sharing_mode, 42 | .queue_family_index_count = @as(u32, @intCast(queue_family_indices.len)), 43 | .p_queue_family_indices = &queue_family_indices, 44 | .pre_transform = settings.pre_transform, 45 | .composite_alpha = .{ .opaque_bit_khr = true }, 46 | .present_mode = settings.present_mode, 47 | .clipped = vk.TRUE, 48 | .old_swapchain = old_handle, 49 | }, null); 50 | errdefer vc.device.destroySwapchainKHR(handle, null); 51 | 52 | const images = try vk_helpers.getVkSliceBounded(max_image_count, @TypeOf(vc.device).getSwapchainImagesKHR, .{ vc.device, handle }); 53 | 54 | return Self { 55 | .surface = surface, 56 | .handle = handle, 57 | .images = images, 58 | .image_format = settings.format.format, 59 | .image_index = undefined, // this is odd, is it the best? 60 | .extent = settings.extent, 61 | }; 62 | } 63 | 64 | pub fn currentImage(self: *const Self) vk.Image { 65 | return self.images.get(self.image_index); 66 | } 67 | 68 | // returns old handle 69 | pub fn recreate(self: *Self, vc: *const VulkanContext, extent: vk.Extent2D) !vk.SwapchainKHR { 70 | const old = self.handle; 71 | self.* = try createFromOld(vc, extent, self.surface, self.handle); 72 | return old; 73 | } 74 | 75 | pub fn acquireNextImage(self: *Self, vc: *const VulkanContext, semaphore: vk.Semaphore) !vk.Result { 76 | const result = try vc.device.acquireNextImage2KHR(&.{ 77 | .swapchain = self.handle, 78 | .timeout = std.math.maxInt(u64), 79 | .semaphore = semaphore, 80 | .fence = .null_handle, 81 | .device_mask = 1, 82 | }); 83 | self.image_index = result.image_index; 84 | return result.result; 85 | } 86 | 87 | pub fn present(self: *const Self, vc: *const VulkanContext, semaphore: vk.Semaphore) !vk.Result { 88 | return try vc.queue.presentKHR(&vk.PresentInfoKHR { 89 | .wait_semaphore_count = 1, 90 | .p_wait_semaphores = (&semaphore)[0..1], 91 | .swapchain_count = 1, 92 | .p_swapchains = (&self.handle)[0..1], 93 | .p_image_indices = (&self.image_index)[0..1], 94 | }); 95 | } 96 | 97 | pub fn destroy(self: *const Self, vc: *const VulkanContext) void { 98 | vc.device.destroySwapchainKHR(self.handle, null); 99 | vc.instance.destroySurfaceKHR(self.surface, null); 100 | } 101 | 102 | const SwapSettings = struct { 103 | format: vk.SurfaceFormatKHR, 104 | present_mode: vk.PresentModeKHR, 105 | image_count: u32, 106 | image_sharing_mode: vk.SharingMode, 107 | pre_transform: vk.SurfaceTransformFlagsKHR, 108 | extent: vk.Extent2D, 109 | 110 | // updates mutable extent 111 | pub fn find(vc: *const VulkanContext, extent: vk.Extent2D, surface: vk.SurfaceKHR) !SwapSettings { 112 | const caps = try vc.instance.getPhysicalDeviceSurfaceCapabilitiesKHR(vc.physical_device.handle, surface); 113 | 114 | return SwapSettings { 115 | .format = try findFormat(vc, surface), 116 | .present_mode = try findPresentMode(vc, surface), 117 | .image_count = if (caps.max_image_count == 0) caps.min_image_count + 1 else @min(caps.min_image_count + 1, caps.max_image_count), 118 | .image_sharing_mode = .exclusive, 119 | .pre_transform = caps.current_transform, 120 | .extent = try calculateExtent(extent, caps), 121 | }; 122 | } 123 | 124 | pub fn findPresentMode(vc: *const VulkanContext, surface: vk.SurfaceKHR) !vk.PresentModeKHR { 125 | const ideal = vk.PresentModeKHR.immediate_khr; 126 | 127 | const present_modes = (try vk_helpers.getVkSliceBounded(8, @TypeOf(vc.instance).getPhysicalDeviceSurfacePresentModesKHR, .{ vc.instance, vc.physical_device.handle, surface })).slice(); 128 | 129 | for (present_modes) |present_mode| { 130 | if (std.meta.eql(present_mode, ideal)) { 131 | return ideal; 132 | } 133 | } 134 | 135 | return present_modes[0]; 136 | } 137 | 138 | pub fn findFormat(vc: *const VulkanContext, surface: vk.SurfaceKHR) !vk.SurfaceFormatKHR { 139 | const ideal = vk.SurfaceFormatKHR { 140 | .format = .b8g8r8a8_srgb, 141 | .color_space = .srgb_nonlinear_khr, 142 | }; 143 | 144 | const formats = (try vk_helpers.getVkSliceBounded(8, @TypeOf(vc.instance).getPhysicalDeviceSurfaceFormatsKHR, .{ vc.instance, vc.physical_device.handle, surface })).slice(); 145 | 146 | for (formats) |format| { 147 | if (std.meta.eql(format, ideal)) { 148 | return ideal; 149 | } 150 | } 151 | 152 | return formats[0]; 153 | } 154 | 155 | pub fn calculateExtent(extent: vk.Extent2D, caps: vk.SurfaceCapabilitiesKHR) !vk.Extent2D { 156 | if (caps.current_extent.width == std.math.maxInt(u32)) { 157 | return vk.Extent2D { 158 | .width = std.math.clamp(extent.width, caps.min_image_extent.width, caps.max_image_extent.width), 159 | .height = std.math.clamp(extent.height, caps.min_image_extent.height, caps.max_image_extent.height), 160 | }; 161 | } else { 162 | return caps.current_extent; 163 | } 164 | if (extent.height == 0 and extent.width == 0) { 165 | return SwapchainError.InvalidSurfaceDimensions; 166 | } 167 | } 168 | }; 169 | -------------------------------------------------------------------------------- /src/lib/displaysystem/displaysystem.zig: -------------------------------------------------------------------------------- 1 | pub const Display = @import("./Display.zig"); 2 | pub const Swapchain = @import("./Swapchain.zig"); 3 | 4 | const vk = @import("vulkan"); 5 | 6 | pub const required_device_extensions = [_][*:0]const u8{ 7 | vk.extensions.khr_swapchain.name, 8 | }; 9 | -------------------------------------------------------------------------------- /src/lib/engine.zig: -------------------------------------------------------------------------------- 1 | // systems 2 | pub const hrtsystem = @import("./hrtsystem/hrtsystem.zig"); 3 | pub const fileformats = @import("./fileformats/fileformats.zig"); 4 | pub const displaysystem = @import("./displaysystem/displaysystem.zig"); 5 | pub const core = @import("./core/core.zig"); 6 | pub const Window = @import("./Window.zig"); 7 | pub const gui = @import("./gui/gui.zig"); 8 | 9 | // utils 10 | pub const vector = @import("./vector.zig"); 11 | -------------------------------------------------------------------------------- /src/lib/fileformats/dds.zig: -------------------------------------------------------------------------------- 1 | const vk = @import("vulkan"); 2 | const std = @import("std"); 3 | 4 | // https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds-reference 5 | pub const PixelFormat = extern struct { 6 | size: u32, // expected to be 32 7 | flags: u32, // flags for pixel format 8 | four_cc: u32, // four characters indicating format type - for us, expected to be "DX10" 9 | rgb_bit_count: u32, // "Number of bits in an RGB (possibly including alpha) format" 10 | r_bit_mask: u32, // mask for red data 11 | g_bit_mask: u32, // mask for green data 12 | b_bit_mask: u32, // mask for blue data 13 | a_bit_mask: u32, // mask for alpha data 14 | 15 | fn verify(self: *const PixelFormat) void { 16 | std.debug.assert(self.size == 32); 17 | std.debug.assert(self.four_cc == comptime std.mem.readIntNative(u32, "DX10")); 18 | } 19 | }; 20 | 21 | pub const Header = extern struct { 22 | size: u32, // expected to be 124 23 | flags: u32, // flags indicating which fields below have valid data 24 | height: u32, // height of image 25 | width: u32, // width of image 26 | pitch_or_linear_size: u32, // "The pitch or number of bytes per scan line in an uncompressed texture" 27 | depth: u32, // depth of image 28 | mip_map_count: u32, // number of mipmaps in image 29 | reserved_1: [11]u32, // unused 30 | ddspf: PixelFormat, // details about pixels 31 | caps: u32, // "Specifies the complexity of the surfaces stored." 32 | caps2: u32, // "Additional detail about the surfaces stored." 33 | caps3: u32, // unused 34 | caps4: u32, // unused 35 | reserved_2: u32, // unused 36 | 37 | fn verify(self: *const Header) void { 38 | std.debug.assert(self.size == 124); 39 | self.ddspf.verify(); 40 | } 41 | }; 42 | 43 | pub const HeaderDXT10 = extern struct { 44 | dxgi_format: u32, // the surface pixel format; this should probably be an enum 45 | resource_dimension: u32, // texture dimension 46 | misc_flag: u32, // misc flags 47 | array_size: u32, // number of elements in array 48 | misc_flags_2: u32, // additional metadata 49 | }; 50 | 51 | pub const FileInfo = extern struct { 52 | magic: u32, // expected to be 542327876, hex for "DDS" 53 | header: Header, // first header 54 | header_10: HeaderDXT10, // second header 55 | 56 | // just some random sanity checks to make sure we actually are getting a DDS file 57 | pub fn verify(self: *const FileInfo) void { 58 | std.debug.assert(self.magic == 542327876); 59 | self.header.verify(); 60 | } 61 | 62 | pub fn getExtent(self: *const FileInfo) vk.Extent2D { 63 | return vk.Extent2D { 64 | .width = self.header.width, 65 | .height = self.header.height, 66 | }; 67 | } 68 | 69 | pub fn getFormat(self: *const FileInfo) vk.Format { 70 | return switch (self.header_10.dxgi_format) { 71 | 71 => .bc1_rgb_srgb_block, 72 | 80 => .bc4_unorm_block, 73 | 83 => .bc5_unorm_block, 74 | 95 => .bc6h_ufloat_block, 75 | 96 => .bc6h_sfloat_block, 76 | else => unreachable, // TODO 77 | }; 78 | } 79 | 80 | pub fn isCubemap(self: *const FileInfo) bool { 81 | return self.header_10.misc_flag == 4; 82 | } 83 | }; -------------------------------------------------------------------------------- /src/lib/fileformats/exr.zig: -------------------------------------------------------------------------------- 1 | // at top level this just has ziggified C API -- but also have a higher-level helpers namespace 2 | 3 | const c = @import("tinyexr"); 4 | const std = @import("std"); 5 | 6 | pub const TinyExrError = error { 7 | InvalidMagicNumber, 8 | InvalidExrVersion, 9 | InvalidArgument, 10 | InvalidData, 11 | InvalidFile, 12 | InvalidParameter, 13 | CantOpenFile, 14 | UnsupportedFormat, 15 | InvalidHeader, 16 | UnsupportedFeature, 17 | CantWriteFile, 18 | SerializationFailed, 19 | LayerNotFound, 20 | DataTooLarge, 21 | }; 22 | 23 | pub const Image = c.EXRImage; 24 | pub const Header = c.EXRHeader; 25 | pub const Version = c.EXRVersion; 26 | pub const ChannelInfo = c.EXRChannelInfo; 27 | 28 | pub const PixelType = enum(c_int) { 29 | uint = 0, 30 | half = 1, 31 | float = 2, 32 | }; 33 | 34 | fn intToStatus(err_code: c_int) TinyExrError!void { 35 | return switch (err_code) { 36 | 0 => {}, 37 | -1 => TinyExrError.InvalidMagicNumber, 38 | -2 => TinyExrError.InvalidExrVersion, 39 | -3 => TinyExrError.InvalidArgument, 40 | -4 => TinyExrError.InvalidData, 41 | -5 => TinyExrError.InvalidFile, 42 | -6 => TinyExrError.InvalidParameter, 43 | -7 => TinyExrError.CantOpenFile, 44 | -8 => TinyExrError.UnsupportedFormat, 45 | -9 => TinyExrError.InvalidHeader, 46 | -10 => TinyExrError.UnsupportedFeature, 47 | -11 => TinyExrError.CantWriteFile, 48 | -12 => TinyExrError.SerializationFailed, 49 | -13 => TinyExrError.LayerNotFound, 50 | -14 => TinyExrError.DataTooLarge, 51 | else => unreachable, 52 | }; 53 | } 54 | 55 | pub fn RetType(comptime T: type) type { 56 | return switch (@typeInfo(T).@"fn".return_type.?) { 57 | c_int => void, 58 | usize => usize, 59 | else => unreachable, // TODO 60 | }; 61 | } 62 | 63 | pub fn handleError(func: anytype, args: anytype) TinyExrError!RetType(@TypeOf(func)) { 64 | var err_message: [*c]const u8 = undefined; 65 | const ref = &err_message; 66 | const new_args = args ++ .{ ref }; 67 | switch (@typeInfo(@TypeOf(func)).@"fn".return_type.?) { 68 | c_int => intToStatus(@call(.auto, func, new_args)) catch |err| { 69 | std.log.err("tinyexr: {}: {s}", .{ err, err_message }); 70 | c.FreeEXRErrorMessage(err_message); 71 | return err; 72 | }, 73 | usize => { 74 | const n = @call(.auto, func, new_args); 75 | if (n == 0) { 76 | std.log.err("tinyexr: {s}", .{ err_message }); 77 | c.FreeEXRErrorMessage(err_message); 78 | return TinyExrError.InvalidData; // want to return some sort of error here but not sure what 79 | } else return n; 80 | }, 81 | else => comptime unreachable, 82 | } 83 | } 84 | 85 | pub fn saveExrImageToFile(image: *const Image, header: *const Header, filename: [*:0]const u8) TinyExrError!void { 86 | return handleError(c.SaveEXRImageToFile, .{ image, header, filename }); 87 | } 88 | 89 | pub fn saveEXRImageToMemory(image: *const Image, header: *const Header, memory: *[*c]u8) TinyExrError!usize { 90 | return handleError(c.SaveEXRImageToMemory, .{ image, header, memory }); 91 | } 92 | 93 | pub fn loadExrImageFromFile(image: *Image, header: *const Header, filename: [*:0]const u8) TinyExrError!void { 94 | return handleError(c.LoadExrImageFromFile, .{ image, header, filename }); 95 | } 96 | 97 | pub fn parseExrHeaderFromFile(header: *Header, version: *const Version, filename: [*:0]const u8) TinyExrError!void { 98 | return handleError(c.ParseEXRHeaderFromFile, .{ header, version, filename }); 99 | } 100 | 101 | pub fn parseExrVersionFromFile(version: *Version, filename: [*:0]const u8) TinyExrError!void { 102 | return intToStatus(c.ParseEXRVersionFromFile(version, filename)); 103 | } 104 | 105 | pub fn loadEXR(out_rgba: *[*c]f32, width: *c_int, height: *c_int, filename: [*:0]const u8) TinyExrError!void { 106 | return handleError(c.LoadEXR, .{ out_rgba, width, height, filename }); 107 | } 108 | 109 | pub fn loadEXRFromMemory(out_rgba: *[*c]f32, width: *c_int, height: *c_int, memory: [*]const u8, size: usize) TinyExrError!void { 110 | return handleError(c.LoadEXRFromMemory, .{ out_rgba, width, height, memory, size }); 111 | } 112 | 113 | pub fn initExrHeader(header: *Header) void { 114 | c.InitEXRHeader(header); 115 | } 116 | 117 | pub fn initExrImage(image: *Image) void { 118 | c.InitEXRImage(image); 119 | } 120 | 121 | // things that don't actually correspond to things in tinyexr but are convenient for this project 122 | pub const helpers = struct { 123 | const vk = @import("vulkan"); 124 | 125 | // RGB image stored in memory as RGBA buffer 126 | pub const Rgba2D = struct { 127 | ptr: [*][4]f32, 128 | extent: vk.Extent2D, 129 | 130 | pub fn asSlice(self: Rgba2D) [][4]f32 { 131 | var slice: [][4]f32 = undefined; 132 | slice.ptr = self.ptr; 133 | slice.len = self.extent.width * self.extent.height; 134 | return slice; 135 | } 136 | 137 | pub fn save(self: Rgba2D, allocator: std.mem.Allocator, out_filename: []const u8) !void { 138 | const channel_count = 3; 139 | 140 | var header: Header = undefined; 141 | initExrHeader(&header); 142 | 143 | var image: Image = undefined; 144 | initExrImage(&image); 145 | 146 | const pixel_count = self.extent.width * self.extent.height; 147 | 148 | const ImageChannels = std.MultiArrayList(struct { 149 | r: f32, 150 | g: f32, 151 | b: f32, 152 | }); 153 | var image_channels = ImageChannels {}; 154 | defer image_channels.deinit(allocator); 155 | 156 | try image_channels.ensureUnusedCapacity(allocator, pixel_count); 157 | 158 | for (0..pixel_count) |i| { 159 | image_channels.appendAssumeCapacity(.{ 160 | .r = self.asSlice()[i][0], 161 | .g = self.asSlice()[i][1], 162 | .b = self.asSlice()[i][2], 163 | }); 164 | } 165 | 166 | const image_channels_slice = image_channels.slice(); 167 | image.num_channels = channel_count; 168 | image.images = @constCast(&[3][*c]u8 { 169 | image_channels_slice.ptrs[2], 170 | image_channels_slice.ptrs[1], 171 | image_channels_slice.ptrs[0], 172 | }); 173 | image.width = @intCast(self.extent.width); 174 | image.height = @intCast(self.extent.height); 175 | 176 | const header_channels = try allocator.alloc(ChannelInfo, channel_count); 177 | defer allocator.free(header_channels); 178 | 179 | const pixel_types = try allocator.alloc(c_int, channel_count); 180 | defer allocator.free(pixel_types); 181 | const requested_pixel_types = try allocator.alloc(c_int, channel_count); 182 | defer allocator.free(requested_pixel_types); 183 | 184 | header.num_channels = channel_count; 185 | header.channels = header_channels.ptr; 186 | 187 | header.channels[0].name[0] = 'B'; 188 | header.channels[0].name[1] = 0; 189 | header.channels[1].name[0] = 'G'; 190 | header.channels[1].name[1] = 0; 191 | header.channels[2].name[0] = 'R'; 192 | header.channels[2].name[1] = 0; 193 | 194 | header.pixel_types = pixel_types.ptr; 195 | header.requested_pixel_types = requested_pixel_types.ptr; 196 | 197 | inline for (0..channel_count) |i| { 198 | header.pixel_types[i] = @intFromEnum(PixelType.float); 199 | header.requested_pixel_types[i] = @intFromEnum(PixelType.float); 200 | } 201 | 202 | var data: [*c]u8 = undefined; 203 | const file_size = try saveEXRImageToMemory(&image, &header, &data); 204 | defer std.c.free(data); 205 | try std.fs.cwd().writeFile(.{ .sub_path = out_filename, .data = data[0..file_size]}); 206 | } 207 | 208 | pub fn load(allocator: std.mem.Allocator, filename: []const u8) !Rgba2D { 209 | const file_content = try std.fs.cwd().readFileAlloc(allocator, filename, std.math.maxInt(usize)); 210 | defer allocator.free(file_content); 211 | 212 | var out_rgba: [*c]f32 = undefined; 213 | var width: c_int = undefined; 214 | var height: c_int = undefined; 215 | try loadEXRFromMemory(&out_rgba, &width, &height, file_content.ptr, file_content.len); 216 | defer std.c.free(out_rgba); 217 | const malloc_slice = Rgba2D { 218 | .ptr = @ptrCast(out_rgba), 219 | .extent = vk.Extent2D { 220 | .width = @intCast(width), 221 | .height = @intCast(height), 222 | }, 223 | }; 224 | const out = Rgba2D { 225 | .ptr = (try allocator.dupe([4]f32, malloc_slice.asSlice())).ptr, // copy into zig allocator 226 | .extent = malloc_slice.extent, 227 | }; 228 | return out; 229 | } 230 | }; 231 | }; 232 | -------------------------------------------------------------------------------- /src/lib/fileformats/fileformats.zig: -------------------------------------------------------------------------------- 1 | pub const exr = @import("exr.zig"); 2 | pub const wuffs = @import("wuffs.zig"); 3 | pub const dds = @import("dds.zig"); 4 | -------------------------------------------------------------------------------- /src/lib/fileformats/wuffs.zig: -------------------------------------------------------------------------------- 1 | const c = @import("wuffs"); 2 | const std = @import("std"); 3 | 4 | fn ToWuffsSliceType(comptime T: type) type { 5 | return switch (T) { 6 | u8 => c.wuffs_base__slice_u8, 7 | u16 => c.wuffs_base__slice_u16, 8 | u32 => c.wuffs_base__slice_u32, 9 | u64 => c.wuffs_base__slice_u64, 10 | else => @compileError(@typeName(T) ++ " is not a valid wuffs slice type"), 11 | }; 12 | } 13 | 14 | fn toWuffsSlice(comptime T: type, slice: []T) ToWuffsSliceType(T) { 15 | const f = switch (T) { 16 | u8 => c.wuffs_base__make_slice_u8, 17 | u16 => c.wuffs_base__make_slice_u16, 18 | u32 => c.wuffs_base__make_slice_u32, 19 | u64 => c.wuffs_base__make_slice_u64, 20 | else => @compileError(@typeName(T) ++ " is not a valid wuffs slice type"), 21 | }; 22 | return f(slice.ptr, slice.len); 23 | } 24 | 25 | fn statusToError(status: c.wuffs_base__status) !void { 26 | // TODO: make this actually useful 27 | if (!c.wuffs_base__status__is_ok(&status)) return error.WuffsError; 28 | } 29 | 30 | const FourCC = enum(u32) { 31 | unknown = 0, 32 | abxr = c.WUFFS_BASE__FOURCC__ABXR, 33 | abxs = c.WUFFS_BASE__FOURCC__ABXS, 34 | bgcl = c.WUFFS_BASE__FOURCC__BGCL, 35 | bmp = c.WUFFS_BASE__FOURCC__BMP, 36 | brtl = c.WUFFS_BASE__FOURCC__BRTL, 37 | bz2 = c.WUFFS_BASE__FOURCC__BZ2, 38 | cbor = c.WUFFS_BASE__FOURCC__CBOR, 39 | chrm = c.WUFFS_BASE__FOURCC__CHRM, 40 | css = c.WUFFS_BASE__FOURCC__CSS, 41 | eps = c.WUFFS_BASE__FOURCC__EPS, 42 | etc2 = c.WUFFS_BASE__FOURCC__ETC2, 43 | exif = c.WUFFS_BASE__FOURCC__EXIF, 44 | flac = c.WUFFS_BASE__FOURCC__FLAC, 45 | gama = c.WUFFS_BASE__FOURCC__GAMA, 46 | gif = c.WUFFS_BASE__FOURCC__GIF, 47 | gz = c.WUFFS_BASE__FOURCC__GZ, 48 | heif = c.WUFFS_BASE__FOURCC__HEIF, 49 | html = c.WUFFS_BASE__FOURCC__HTML, 50 | iccp = c.WUFFS_BASE__FOURCC__ICCP, 51 | ico = c.WUFFS_BASE__FOURCC__ICO, 52 | icvg = c.WUFFS_BASE__FOURCC__ICVG, 53 | ini = c.WUFFS_BASE__FOURCC__INI, 54 | jpeg = c.WUFFS_BASE__FOURCC__JPEG, 55 | js = c.WUFFS_BASE__FOURCC__JS, 56 | json = c.WUFFS_BASE__FOURCC__JSON, 57 | jwcc = c.WUFFS_BASE__FOURCC__JWCC, 58 | kvp = c.WUFFS_BASE__FOURCC__KVP, 59 | kvpk = c.WUFFS_BASE__FOURCC__KVPK, 60 | kvpv = c.WUFFS_BASE__FOURCC__KVPV, 61 | lz4 = c.WUFFS_BASE__FOURCC__LZ4, 62 | lzip = c.WUFFS_BASE__FOURCC__LZIP, 63 | lzma = c.WUFFS_BASE__FOURCC__LZMA, 64 | md = c.WUFFS_BASE__FOURCC__MD, 65 | mtim = c.WUFFS_BASE__FOURCC__MTIM, 66 | mp3 = c.WUFFS_BASE__FOURCC__MP3, 67 | nie = c.WUFFS_BASE__FOURCC__NIE, 68 | npbm = c.WUFFS_BASE__FOURCC__NPBM, 69 | ofs2 = c.WUFFS_BASE__FOURCC__OFS2, 70 | otf = c.WUFFS_BASE__FOURCC__OTF, 71 | pdf = c.WUFFS_BASE__FOURCC__PDF, 72 | phyd = c.WUFFS_BASE__FOURCC__PHYD, 73 | png = c.WUFFS_BASE__FOURCC__PNG, 74 | ps = c.WUFFS_BASE__FOURCC__PS, 75 | qoi = c.WUFFS_BASE__FOURCC__QOI, 76 | rac = c.WUFFS_BASE__FOURCC__RAC, 77 | raw = c.WUFFS_BASE__FOURCC__RAW, 78 | riff = c.WUFFS_BASE__FOURCC__RIFF, 79 | rigl = c.WUFFS_BASE__FOURCC__RIGL, 80 | snpy = c.WUFFS_BASE__FOURCC__SNPY, 81 | srgb = c.WUFFS_BASE__FOURCC__SRGB, 82 | svg = c.WUFFS_BASE__FOURCC__SVG, 83 | tar = c.WUFFS_BASE__FOURCC__TAR, 84 | text = c.WUFFS_BASE__FOURCC__TEXT, 85 | tga = c.WUFFS_BASE__FOURCC__TGA, 86 | th = c.WUFFS_BASE__FOURCC__TH, 87 | tiff = c.WUFFS_BASE__FOURCC__TIFF, 88 | toml = c.WUFFS_BASE__FOURCC__TOML, 89 | wave = c.WUFFS_BASE__FOURCC__WAVE, 90 | wbmp = c.WUFFS_BASE__FOURCC__WBMP, 91 | webp = c.WUFFS_BASE__FOURCC__WEBP, 92 | woff = c.WUFFS_BASE__FOURCC__WOFF, 93 | xml = c.WUFFS_BASE__FOURCC__XML, 94 | xmp = c.WUFFS_BASE__FOURCC__XMP, 95 | xz = c.WUFFS_BASE__FOURCC__XZ, 96 | zip = c.WUFFS_BASE__FOURCC__ZIP, 97 | zlib = c.WUFFS_BASE__FOURCC__ZLIB, 98 | zstd = c.WUFFS_BASE__FOURCC__ZSTD, 99 | 100 | fn toDecodableImageFileFormat(self: FourCC) ?DecodableImageFileFormat { 101 | return switch (self) { 102 | .bmp => .bmp, 103 | .etc2 => .etc2, 104 | .gif => .gif, 105 | .jpeg => .jpeg, 106 | .nie => .nie, 107 | .npbm => .netpbm, 108 | .png => .png, 109 | .qoi => .qoi, 110 | .tga => .targa, 111 | .th => .thumbhash, 112 | .wbmp => .wbmp, 113 | .webp => .webp, 114 | else => null, 115 | }; 116 | } 117 | }; 118 | 119 | const DecodableImageFileFormat = enum { 120 | bmp, 121 | etc2, 122 | gif, 123 | jpeg, 124 | nie, 125 | netpbm, 126 | png, 127 | qoi, 128 | targa, 129 | thumbhash, 130 | wbmp, 131 | webp, 132 | }; 133 | 134 | fn guessFourCC(slice: []const u8) !FourCC { 135 | const res = c.wuffs_base__magic_number_guess_fourcc(toWuffsSlice(u8, @constCast(slice)), true); 136 | if (res == -1) return error.Incomplete else return @enumFromInt(res); 137 | } 138 | 139 | fn createDecoder(format: DecodableImageFileFormat, allocator: std.mem.Allocator) !*c.wuffs_base__image_decoder { 140 | inline for (comptime std.meta.tags(DecodableImageFileFormat)) |tag| { 141 | if (tag == format) { 142 | const size = @field(c, "sizeof__wuffs_" ++ @tagName(tag) ++ "__decoder")(); 143 | const slice = try allocator.alloc(u8, size); 144 | errdefer allocator.free(slice); 145 | 146 | try statusToError(@field(c, "wuffs_" ++ @tagName(tag) ++ "__decoder__initialize")(@ptrCast(slice), size, c.WUFFS_VERSION, c.WUFFS_INITIALIZE__LEAVE_INTERNAL_BUFFERS_UNINITIALIZED)); 147 | 148 | return @field(c, "wuffs_" ++ @tagName(tag) ++ "__decoder__upcast_as__wuffs_base__image_decoder")(@ptrCast(slice)).?; 149 | } 150 | } else unreachable; 151 | } 152 | 153 | fn destroyDecoder(format: DecodableImageFileFormat, allocator: std.mem.Allocator, decoder: *const c.wuffs_base__image_decoder) void { 154 | inline for (comptime std.meta.tags(DecodableImageFileFormat)) |tag| { 155 | if (tag == format) { 156 | const bytes: [*]const u8 = @ptrCast(decoder); 157 | const size = @field(c, "sizeof__wuffs_" ++ @tagName(tag) ++ "__decoder")(); 158 | const slice = bytes[0..size]; 159 | allocator.free(slice); 160 | return; 161 | } 162 | } else unreachable; 163 | } 164 | 165 | pub fn load(allocator: std.mem.Allocator, buffer: []const u8) !std.meta.Tuple(&.{[]const u8, u32, u32}) { 166 | var src = c.wuffs_base__ptr_u8__reader(@constCast(buffer.ptr), buffer.len, true); 167 | const format = (try guessFourCC(buffer)).toDecodableImageFileFormat() orelse return error.NotImage; 168 | 169 | const decoder = try createDecoder(format, allocator); 170 | defer destroyDecoder(format, allocator, decoder); 171 | 172 | var ic: c.wuffs_base__image_config = undefined; 173 | try statusToError(c.wuffs_base__image_decoder__decode_image_config(decoder, &ic, &src)); 174 | 175 | const width = c.wuffs_base__pixel_config__width(&ic.pixcfg); 176 | const height = c.wuffs_base__pixel_config__height(&ic.pixcfg); 177 | 178 | var pc: c.wuffs_base__pixel_config = undefined; 179 | c.wuffs_base__pixel_config__set(&pc, c.WUFFS_BASE__PIXEL_FORMAT__RGB, c.WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, width, height); 180 | 181 | const work_buffer = try allocator.alloc(u8, c.wuffs_base__image_decoder__workbuf_len(decoder).max_incl); 182 | defer allocator.free(work_buffer); 183 | 184 | const pixel_buffer = try allocator.alloc(u8, width * height * 3); 185 | 186 | var pb: c.wuffs_base__pixel_buffer = undefined; 187 | try statusToError(c.wuffs_base__pixel_buffer__set_from_slice(&pb, &pc, toWuffsSlice(u8, pixel_buffer))); 188 | 189 | try statusToError(c.wuffs_base__image_decoder__decode_frame(decoder, &pb, &src, c.WUFFS_BASE__PIXEL_BLEND__SRC, toWuffsSlice(u8, work_buffer), null)); 190 | 191 | return .{ pixel_buffer, width, height }; 192 | } -------------------------------------------------------------------------------- /src/lib/gui/gui.zig: -------------------------------------------------------------------------------- 1 | pub const Platform = @import("./Platform.zig"); 2 | pub const imgui = @import("./imgui.zig"); 3 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/BackgroundManager.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const vk = @import("vulkan"); 3 | 4 | const engine = @import("../engine.zig"); 5 | const VulkanContext = engine.core.VulkanContext; 6 | const Encoder = engine.core.Encoder; 7 | const Image = engine.core.Image; 8 | 9 | const vector = @import("../vector.zig"); 10 | const shaders = @import("hrtsystem_shaders"); 11 | 12 | const Rgba2D = engine.fileformats.exr.helpers.Rgba2D; 13 | const Mat3 = vector.Mat3(f32); 14 | 15 | pub const Background = struct { 16 | image: Image, 17 | transform: Mat3, 18 | }; 19 | 20 | backgrounds: std.ArrayListUnmanaged(Background), 21 | sampler: vk.Sampler, 22 | equirectangular_to_equal_area_pipeline: EquirectangularToEqualAreaPipeline, 23 | fold_pipeline: FoldPipeline, 24 | 25 | const Self = @This(); 26 | 27 | const EquirectangularToEqualAreaPipeline = engine.core.pipeline.Pipeline(.{ 28 | .shader_source = shaders.equirectangular_to_equal_area, 29 | .local_size = vk.Extent3D { .width = 8, .height = 8, .depth = 1 }, 30 | .PushSetBindings = struct { 31 | src_texture: engine.core.pipeline.CombinedImageSampler, 32 | dst_image: engine.core.pipeline.StorageImage, 33 | } 34 | }); 35 | 36 | const FoldPipeline = engine.core.pipeline.Pipeline(.{ 37 | .shader_source = shaders.background_fold, 38 | .local_size = vk.Extent3D { .width = 8, .height = 8, .depth = 1 }, 39 | .PushSetBindings = struct { 40 | src_mip: engine.core.pipeline.SampledImage, 41 | dst_mip: engine.core.pipeline.StorageImage, 42 | } 43 | }); 44 | 45 | pub fn create(vc: *const VulkanContext, allocator: std.mem.Allocator) !Self { 46 | const sampler = try vc.device.createSampler(&.{ 47 | .flags = .{}, 48 | .mag_filter = .linear, 49 | .min_filter = .linear, 50 | .mipmap_mode = .nearest, 51 | .address_mode_u = .mirrored_repeat, 52 | .address_mode_v = .mirrored_repeat, 53 | .address_mode_w = .mirrored_repeat, 54 | .mip_lod_bias = 0.0, 55 | .anisotropy_enable = vk.FALSE, 56 | .max_anisotropy = 0.0, 57 | .compare_enable = vk.FALSE, 58 | .compare_op = .always, 59 | .min_lod = 0.0, 60 | .max_lod = 0.0, 61 | .border_color = .float_opaque_white, 62 | .unnormalized_coordinates = vk.FALSE, 63 | }, null); 64 | errdefer vc.device.destroySampler(sampler, null); 65 | 66 | var equirectangular_to_equal_area_pipeline = try EquirectangularToEqualAreaPipeline.create(vc, allocator, .{}, .{ sampler }, .{}); 67 | errdefer equirectangular_to_equal_area_pipeline.destroy(vc); 68 | 69 | var fold_pipeline = try FoldPipeline.create(vc, allocator, .{}, .{}, .{}); 70 | errdefer fold_pipeline.destroy(vc); 71 | 72 | return Self { 73 | .backgrounds = .{}, 74 | .sampler = sampler, 75 | .equirectangular_to_equal_area_pipeline = equirectangular_to_equal_area_pipeline, 76 | .fold_pipeline = fold_pipeline, 77 | }; 78 | } 79 | 80 | pub fn addDefaultBackground(self: *Self, vc: *const VulkanContext, allocator: std.mem.Allocator, encoder: *Encoder) !Handle { 81 | var color = [4]f32 { 1.0, 1.0, 1.0, 1.0 }; 82 | const rgba = Rgba2D { 83 | .ptr = @ptrCast(&color), 84 | .extent = .{ 85 | .width = 1, 86 | .height = 1, 87 | } 88 | }; 89 | return try self.addBackground(vc, allocator, encoder, rgba, Mat3.identity, "default white"); 90 | } 91 | 92 | // this should probably be a parameter, or should infer proper value for this 93 | // 94 | // the equal area map size will be the biggest power of two greater than 95 | // or equal to the equirectangular height, clamped to maximum_equal_area_map_size 96 | const maximum_equal_area_map_size = 16384; 97 | const shader_local_size = 8; // must be kept in sync with shader -- looks like HLSL doesn't support setting this via spec constants 98 | 99 | pub const Handle = u32; 100 | // color_image should be equirectangular, which is converted to equal area. 101 | // 102 | // in "Parameterization-Independent Importance Sampling of Environment Maps", 103 | // the author retains the original environment map for illumination, 104 | // only using equal area for importance sampling. 105 | // I tried that here but it seems to produce noisier results for e.g., sunny skies 106 | // compared to just keeping everything in the same parameterization. 107 | pub fn addBackground(self: *Self, vc: *const VulkanContext, allocator: std.mem.Allocator, encoder: *Encoder, color_image: Rgba2D, transform: Mat3, name: []const u8) !Handle { 108 | const equirectangular_extent = color_image.extent; 109 | 110 | const texture_name_equirectangular = try std.fmt.allocPrintZ(allocator, "background {s} equirectangular", .{ name }); 111 | defer allocator.free(texture_name_equirectangular); 112 | const equirectangular_image = try Image.create(vc, equirectangular_extent, .{ .transfer_dst_bit = true, .sampled_bit = true }, .r32g32b32a32_sfloat, false, texture_name_equirectangular); 113 | try encoder.attachResource(equirectangular_image); 114 | 115 | const equirectangular_image_host = try encoder.uploadAllocator().alignedAlloc([4]f32, engine.core.vk_helpers.texelBlockSize(.r32g32b32a32_sfloat), color_image.asSlice().len); 116 | @memcpy(equirectangular_image_host, color_image.asSlice()); 117 | 118 | const equal_area_map_size: u32 = @min(std.math.ceilPowerOfTwoAssert(u32, color_image.extent.width), maximum_equal_area_map_size); 119 | const equal_area_extent = vk.Extent2D { .width = equal_area_map_size, .height = equal_area_map_size }; 120 | 121 | const texture_name_equal_area = try std.fmt.allocPrintZ(allocator, "background {s} equal area", .{ name }); 122 | defer allocator.free(texture_name_equal_area); 123 | const equal_area_image = try Image.create(vc, equal_area_extent, .{ .storage_bit = true, .sampled_bit = true }, .r32g32b32a32_sfloat, true, texture_name_equal_area); 124 | errdefer equal_area_image.destroy(vc); 125 | 126 | const actual_mip_count = std.math.log2(equal_area_map_size) + 1; 127 | const maximum_mip_count = comptime std.math.log2(maximum_equal_area_map_size) + 1; 128 | var mip_views = std.BoundedArray(vk.ImageView, maximum_mip_count) {}; 129 | for (0..actual_mip_count) |level_index| { 130 | const view = try vc.device.createImageView(&vk.ImageViewCreateInfo { 131 | .flags = .{}, 132 | .image = equal_area_image.handle, 133 | .view_type = vk.ImageViewType.@"2d", 134 | .format = .r32g32b32a32_sfloat, 135 | .components = .{ 136 | .r = .identity, 137 | .g = .identity, 138 | .b = .identity, 139 | .a = .identity, 140 | }, 141 | .subresource_range = .{ 142 | .aspect_mask = .{ .color_bit = true }, 143 | .base_mip_level = @intCast(level_index), 144 | .level_count = 1, 145 | .base_array_layer = 0, 146 | .layer_count = vk.REMAINING_ARRAY_LAYERS, 147 | }, 148 | }, null); 149 | try mip_views.append(view); 150 | try encoder.attachResource(view); 151 | } 152 | 153 | // copy equirectangular image to device 154 | encoder.barrier(&[_]Encoder.ImageBarrier { 155 | .{ 156 | .dst_stage_mask = .{ .copy_bit = true }, 157 | .dst_access_mask = .{ .transfer_write_bit = true }, 158 | .old_layout = .undefined, 159 | .new_layout = .transfer_dst_optimal, 160 | .image = equirectangular_image.handle, 161 | }, 162 | .{ 163 | .dst_stage_mask = .{ .compute_shader_bit = true }, 164 | .dst_access_mask = .{ .shader_write_bit = true }, 165 | .old_layout = .undefined, 166 | .new_layout = .general, 167 | .image = equal_area_image.handle, 168 | }, 169 | }, &.{}); 170 | 171 | const equirectangular_image_host_slice = encoder.upload_allocator.getBufferSlice(equirectangular_image_host); 172 | encoder.copyBufferToImage(equirectangular_image_host_slice.handle, equirectangular_image_host_slice.offset, equirectangular_image.handle, .transfer_dst_optimal, equirectangular_extent); 173 | 174 | encoder.barrier(&[_]Encoder.ImageBarrier { 175 | .{ 176 | .src_stage_mask = .{ .copy_bit = true }, 177 | .src_access_mask = .{ .transfer_write_bit = true }, 178 | .dst_stage_mask = .{ .compute_shader_bit = true }, 179 | .dst_access_mask = .{ .shader_read_bit = true }, 180 | .old_layout = .transfer_dst_optimal, 181 | .new_layout = .shader_read_only_optimal, 182 | .image = equirectangular_image.handle, 183 | }, 184 | }, &.{}); 185 | 186 | // do conversion 187 | self.equirectangular_to_equal_area_pipeline.recordBindPipeline(encoder.buffer); 188 | self.equirectangular_to_equal_area_pipeline.recordPushDescriptors(encoder.buffer, .{ 189 | .src_texture = .{ .view = equirectangular_image.view }, 190 | .dst_image = .{ .view = equal_area_image.view }, 191 | }); 192 | const dispatch_size = if (equal_area_map_size > shader_local_size) @divExact(equal_area_map_size, shader_local_size) else 1; 193 | self.equirectangular_to_equal_area_pipeline.recordDispatchWorkgroups(encoder.buffer, .{ .width = dispatch_size, .height = dispatch_size, .depth = 1 }); 194 | 195 | self.fold_pipeline.recordBindPipeline(encoder.buffer); 196 | for (1..mip_views.len) |dst_mip_level| { 197 | encoder.barrier(&[_]Encoder.ImageBarrier { 198 | .{ 199 | .src_stage_mask = .{ .compute_shader_bit = true }, 200 | .src_access_mask = .{ .shader_write_bit = true }, 201 | .dst_stage_mask = .{ .compute_shader_bit = true }, 202 | .dst_access_mask = .{ .shader_read_bit = true }, 203 | .old_layout = .general, 204 | .new_layout = .shader_read_only_optimal, 205 | .image = equal_area_image.handle, 206 | .base_mip_level = @intCast(dst_mip_level - 1), 207 | .level_count = 1, 208 | }, 209 | }, &.{}); 210 | self.fold_pipeline.recordPushDescriptors(encoder.buffer, .{ 211 | .src_mip = .{ .view = mip_views.get(dst_mip_level - 1) }, 212 | .dst_mip = .{ .view = mip_views.get(dst_mip_level) }, 213 | }); 214 | const dst_mip_size = std.math.pow(u32, 2, @intCast(mip_views.len - dst_mip_level)); 215 | const mip_dispatch_size = if (dst_mip_size > shader_local_size) @divExact(dst_mip_size, shader_local_size) else 1; 216 | self.fold_pipeline.recordDispatchWorkgroups(encoder.buffer, .{ .width = mip_dispatch_size, .height = mip_dispatch_size, .depth = 1 }); 217 | } 218 | encoder.barrier(&[_]Encoder.ImageBarrier { 219 | .{ 220 | .src_stage_mask = .{ .compute_shader_bit = true }, 221 | .src_access_mask = .{ .shader_write_bit = true }, 222 | .dst_stage_mask = .{ .compute_shader_bit = true }, 223 | .dst_access_mask = .{ .shader_read_bit = true }, 224 | .old_layout = .general, 225 | .new_layout = .shader_read_only_optimal, 226 | .image = equal_area_image.handle, 227 | .base_mip_level = @intCast(mip_views.len - 1), 228 | .level_count = 1, 229 | }, 230 | }, &.{}); 231 | 232 | try self.backgrounds.append(allocator, Background { 233 | .image = equal_area_image, 234 | .transform = transform, 235 | }); 236 | 237 | return @intCast(self.backgrounds.items.len - 1); 238 | } 239 | 240 | pub fn destroy(self: *Self, vc: *const VulkanContext, allocator: std.mem.Allocator) void { 241 | for (self.backgrounds.items) |background| { 242 | background.image.destroy(vc); 243 | } 244 | self.backgrounds.deinit(allocator); 245 | self.equirectangular_to_equal_area_pipeline.destroy(vc); 246 | self.fold_pipeline.destroy(vc); 247 | vc.device.destroySampler(self.sampler, null); 248 | } 249 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/CameraManager.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const vk = @import("vulkan"); 3 | const Gltf = @import("zgltf"); 4 | 5 | const engine = @import("../engine.zig"); 6 | const core = engine.core; 7 | const VulkanContext = core.VulkanContext; 8 | const Encoder = core.Encoder; 9 | 10 | const Sensor = engine.hrtsystem.Sensor; 11 | 12 | const Mat4 = engine.vector.Mat4(f32); 13 | const Mat4x3 = engine.vector.Mat4x3(f32); 14 | 15 | pub const Model = enum(u32) { 16 | thin_lens, 17 | orthographic, 18 | }; 19 | 20 | pub const ThinLens = extern struct { 21 | vfov: f32 = std.math.pi / 4.0, 22 | aperture: f32 = 0, 23 | focus_distance: f32 = 1, 24 | }; 25 | 26 | pub const Orthographic = extern struct { 27 | vscale: f32 = 1, 28 | }; 29 | 30 | // store camera models as a struct rather than a tagged union because: 31 | // 1. this way in interactive modes states of non-selected is saved 32 | // 2. can pass to spirv directly 33 | pub const Camera = extern struct { 34 | transform: Mat4x3 = Mat4.identity.truncateRow(), 35 | model: Model = .thin_lens, 36 | thin_lens: ThinLens = .{}, 37 | orthographic: Orthographic = .{}, 38 | }; 39 | 40 | sensors: std.ArrayListUnmanaged(Sensor) = .{}, 41 | cameras: std.ArrayListUnmanaged(std.meta.Tuple(&.{[:0]const u8, Camera })) = .{}, 42 | 43 | const Self = @This(); 44 | 45 | pub const SensorHandle = u32; 46 | pub fn appendSensor(self: *Self, vc: *const VulkanContext, allocator: std.mem.Allocator, extent: vk.Extent2D) !SensorHandle { 47 | var buf: [32]u8 = undefined; 48 | const name = try std.fmt.bufPrintZ(&buf, "render {}", .{self.sensors.items.len}); 49 | 50 | try self.sensors.append(allocator, try Sensor.create(vc, extent, name)); 51 | return @intCast(self.sensors.items.len - 1); 52 | } 53 | 54 | pub const CameraHandle = u32; 55 | pub fn appendCamera(self: *Self, allocator: std.mem.Allocator, camera: Camera, name: [:0]const u8) !CameraHandle { 56 | try self.cameras.append(allocator, .{name, camera}); 57 | return @intCast(self.cameras.items.len - 1); 58 | } 59 | 60 | pub fn clearAllSensors(self: *Self) void { 61 | for (self.sensors.items) |*sensor| { 62 | sensor.clear(); 63 | } 64 | } 65 | 66 | pub fn destroy(self: *Self, vc: *const VulkanContext, allocator: std.mem.Allocator) void { 67 | for (self.sensors.items) |*sensor| { 68 | sensor.destroy(vc); 69 | } 70 | for (self.cameras.items) |*camera| { 71 | allocator.free(camera[0]); 72 | } 73 | self.sensors.deinit(allocator); 74 | self.cameras.deinit(allocator); 75 | } 76 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/MeshManager.zig: -------------------------------------------------------------------------------- 1 | const vk = @import("vulkan"); 2 | const std = @import("std"); 3 | 4 | const engine = @import("../engine.zig"); 5 | 6 | const core = engine.core; 7 | const VulkanContext = core.VulkanContext; 8 | const Encoder = core.Encoder; 9 | 10 | const vector = @import("../vector.zig"); 11 | const U32x3 = vector.Vec3(u32); 12 | const F32x3 = vector.Vec3(f32); 13 | const F32x2 = vector.Vec2(f32); 14 | 15 | pub const Mesh = struct { 16 | pub const Parameters = struct { 17 | name: []const u8, 18 | // vertices 19 | positions: core.mem.BufferSlice(F32x3), 20 | normals: ?core.mem.BufferSlice(F32x3), 21 | texcoords: ?core.mem.BufferSlice(F32x2), 22 | 23 | // indices 24 | indices: ?core.mem.BufferSlice(U32x3), 25 | }; 26 | 27 | pub const Host = struct { 28 | position_buffer: core.mem.DeviceBuffer(F32x3, .{ .shader_device_address_bit = true, .transfer_dst_bit = true, .acceleration_structure_build_input_read_only_bit_khr = true }), 29 | texcoord_buffer: core.mem.DeviceBuffer(F32x2, .{ .shader_device_address_bit = true, .transfer_dst_bit = true }), 30 | normal_buffer: core.mem.DeviceBuffer(F32x3, .{ .shader_device_address_bit = true, .transfer_dst_bit = true }), 31 | 32 | vertex_count: u32, 33 | 34 | index_buffer: core.mem.DeviceBuffer(U32x3, .{ .shader_device_address_bit = true, .transfer_dst_bit = true, .acceleration_structure_build_input_read_only_bit_khr = true }), 35 | index_count: u32, 36 | 37 | pub fn triangleCount(self: @This()) u32 { 38 | return if (self.index_count != 0) self.index_count else @divExact(self.vertex_count, 3); 39 | } 40 | }; 41 | 42 | pub const Device = extern struct { 43 | position_address: vk.DeviceAddress, 44 | texcoord_address: vk.DeviceAddress, 45 | normal_address: vk.DeviceAddress, 46 | 47 | index_address: vk.DeviceAddress, 48 | 49 | triangle_count: u64, 50 | }; 51 | }; 52 | 53 | host: std.MultiArrayList(Mesh.Host) = .{}, 54 | device: core.mem.DeviceBuffer(Mesh.Device, .{ .transfer_dst_bit = true, .storage_buffer_bit = true }) = .{}, 55 | 56 | const Self = @This(); 57 | 58 | const max_meshes = 4096; // TODO: resizable buffers 59 | 60 | pub const Handle = u32; 61 | 62 | pub fn upload(self: *Self, vc: *const VulkanContext, allocator: std.mem.Allocator, encoder: *Encoder, parameters: Mesh.Parameters) !Handle { 63 | std.debug.assert(self.host.len < max_meshes); 64 | 65 | const position_buffer = blk: { 66 | const buffer_name = try std.fmt.allocPrintZ(allocator, "mesh {s} positions", .{ parameters.name }); 67 | defer allocator.free(buffer_name); 68 | const gpu_buffer = try core.mem.DeviceBuffer(F32x3, .{ .shader_device_address_bit = true, .transfer_dst_bit = true, .acceleration_structure_build_input_read_only_bit_khr = true }).create(vc, parameters.positions.len, buffer_name); 69 | gpu_buffer.uploadFrom(encoder, 0, parameters.positions); 70 | 71 | break :blk gpu_buffer; 72 | }; 73 | errdefer position_buffer.destroy(vc); 74 | 75 | const texcoord_buffer = blk: { 76 | if (parameters.texcoords) |texcoords| { 77 | const buffer_name = try std.fmt.allocPrintZ(allocator, "mesh {s} texcoords", .{ parameters.name }); 78 | defer allocator.free(buffer_name); 79 | const gpu_buffer = try core.mem.DeviceBuffer(F32x2, .{ .shader_device_address_bit = true, .transfer_dst_bit = true }).create(vc, texcoords.len, buffer_name); 80 | gpu_buffer.uploadFrom(encoder, 0, texcoords); 81 | break :blk gpu_buffer; 82 | } else { 83 | break :blk core.mem.DeviceBuffer(F32x2, .{ .shader_device_address_bit = true, .transfer_dst_bit = true }) {}; 84 | } 85 | }; 86 | errdefer texcoord_buffer.destroy(vc); 87 | 88 | const normal_buffer = blk: { 89 | if (parameters.normals) |normals| { 90 | const buffer_name = try std.fmt.allocPrintZ(allocator, "mesh {s} normals", .{ parameters.name }); 91 | defer allocator.free(buffer_name); 92 | const gpu_buffer = try core.mem.DeviceBuffer(F32x3, .{ .shader_device_address_bit = true, .transfer_dst_bit = true }).create(vc, normals.len, buffer_name); 93 | gpu_buffer.uploadFrom(encoder, 0, normals); 94 | break :blk gpu_buffer; 95 | } else { 96 | break :blk core.mem.DeviceBuffer(F32x3, .{ .shader_device_address_bit = true, .transfer_dst_bit = true }) {}; 97 | } 98 | }; 99 | errdefer normal_buffer.destroy(vc); 100 | 101 | const index_buffer = blk: { 102 | if (parameters.indices) |indices| { 103 | const buffer_name = try std.fmt.allocPrintZ(allocator, "mesh {s} incides", .{ parameters.name }); 104 | defer allocator.free(buffer_name); 105 | const gpu_buffer = try core.mem.DeviceBuffer(U32x3, .{ .shader_device_address_bit = true, .transfer_dst_bit = true, .acceleration_structure_build_input_read_only_bit_khr = true }).create(vc, indices.len, buffer_name); 106 | gpu_buffer.uploadFrom(encoder, 0, indices); 107 | break :blk gpu_buffer; 108 | } else { 109 | break :blk core.mem.DeviceBuffer(U32x3, .{ .shader_device_address_bit = true, .transfer_dst_bit = true, .acceleration_structure_build_input_read_only_bit_khr = true }) {}; 110 | } 111 | }; 112 | errdefer index_buffer.destroy(vc); 113 | 114 | const host = Mesh.Host { 115 | .position_buffer = position_buffer, 116 | .texcoord_buffer = texcoord_buffer, 117 | .normal_buffer = normal_buffer, 118 | 119 | .vertex_count = @intCast(parameters.positions.len), 120 | 121 | .index_buffer = index_buffer, 122 | .index_count = if (parameters.indices) |indices| @intCast(indices.len) else 0, 123 | }; 124 | 125 | const device = Mesh.Device { 126 | .position_address = position_buffer.getAddress(vc), 127 | .texcoord_address = texcoord_buffer.getAddress(vc), 128 | .normal_address = normal_buffer.getAddress(vc) , 129 | 130 | .index_address = index_buffer.getAddress(vc), 131 | 132 | .triangle_count = host.triangleCount(), 133 | }; 134 | 135 | if (self.device.isNull()) self.device = try core.mem.DeviceBuffer(Mesh.Device, .{ .transfer_dst_bit = true, .storage_buffer_bit = true }).create(vc, max_meshes, "meshes"); 136 | self.device.updateFrom(encoder, self.host.len, &.{ device }); 137 | 138 | try self.host.append(allocator, host); 139 | 140 | return @intCast(self.host.len - 1); 141 | } 142 | 143 | pub fn destroy(self: *Self, vc: *const VulkanContext, allocator: std.mem.Allocator) void { 144 | const slice = self.host.slice(); 145 | const position_buffers = slice.items(.position_buffer); 146 | const texcoord_buffers = slice.items(.texcoord_buffer); 147 | const normal_buffers = slice.items(.normal_buffer); 148 | const index_buffers = slice.items(.index_buffer); 149 | 150 | for (position_buffers, texcoord_buffers, normal_buffers, index_buffers) |position_buffer, texcoord_buffer, normal_buffer, index_buffer| { 151 | position_buffer.destroy(vc); 152 | texcoord_buffer.destroy(vc); 153 | normal_buffer.destroy(vc); 154 | index_buffer.destroy(vc); 155 | } 156 | self.host.deinit(allocator); 157 | 158 | self.device.destroy(vc); 159 | } 160 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/Scene.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const vk = @import("vulkan"); 3 | const Gltf = @import("zgltf"); 4 | 5 | const engine = @import("../engine.zig"); 6 | 7 | const core = engine.core; 8 | const VulkanContext = core.VulkanContext; 9 | const Encoder = core.Encoder; 10 | 11 | const Background = @import("./BackgroundManager.zig"); 12 | const World = @import("./World.zig"); 13 | const Camera = @import("./CameraManager.zig"); 14 | const Material = @import("./MaterialManager.zig"); 15 | 16 | const exr = engine.fileformats.exr; 17 | 18 | const vector = @import("../vector.zig"); 19 | const F32x3 = vector.Vec3(f32); 20 | const F32x4 = vector.Vec4(f32); 21 | const Mat3 = vector.Mat3(f32); 22 | const Mat4 = vector.Mat4(f32); 23 | const Mat4x3 = vector.Mat4x3(f32); 24 | 25 | const Self = @This(); 26 | 27 | world: World, 28 | background: Background, 29 | camera: Camera, 30 | global_volume: Material.Volume = .{}, 31 | 32 | // glTF doesn't correspond very well to the internal data structures here so this is very inefficient 33 | // also very inefficient because it's written very inefficiently, can remove a lot of copying, but that's a problem for another time 34 | // inspection bool specifies whether some buffers should be created with the `transfer_src_flag` for inspection 35 | pub fn fromGltfExr(vc: *const VulkanContext, allocator: std.mem.Allocator, encoder: *Encoder, gltf_filepath: []const u8, skybox_filepath: []const u8, extent: vk.Extent2D) !Self { 36 | var gltf = Gltf.init(allocator); 37 | defer gltf.deinit(); 38 | 39 | const buffer = try std.fs.cwd().readFileAllocOptions( 40 | allocator, 41 | gltf_filepath, 42 | std.math.maxInt(usize), 43 | null, 44 | 4, 45 | null 46 | ); 47 | defer allocator.free(buffer); 48 | try gltf.parse(buffer); 49 | 50 | var camera = Camera {}; 51 | errdefer camera.destroy(vc, allocator); 52 | _ = try camera.appendSensor(vc, allocator, extent); 53 | 54 | { 55 | // gltf spec: 56 | // > The camera is defined such that the local +X axis is to the right, 57 | // > the “lens” looks towards the local -Z axis, 58 | // > and the top of the camera is aligned with the local +Y axis. 59 | const msne_camera_to_gltf_camera = Mat4.fromRows(.{ 60 | .new(.{ 0, 1, 0, 0}), 61 | .new(.{ 0, 0,-1, 0}), 62 | .new(.{-1, 0, 0, 0}), 63 | .new(.{ 0, 0, 0, 1}), 64 | }); 65 | 66 | for (gltf.data.nodes.items) |node| { 67 | if (node.camera) |camera_idx| { 68 | const gltf_camera = gltf.data.cameras.items[camera_idx]; 69 | const mat_array = Gltf.getGlobalTransform(&gltf.data, node); 70 | const transform = Mat4.fromCols(.{ .new(mat_array[0]), .new(mat_array[1]), .new(mat_array[2]), .new(mat_array[3]) }); 71 | _ = try camera.appendCamera(allocator, Camera.Camera { 72 | .transform = transform.mul(msne_camera_to_gltf_camera).truncateRow(), 73 | .model = switch (gltf_camera.type) { 74 | .perspective => .thin_lens, 75 | .orthographic => .orthographic, 76 | }, 77 | .thin_lens = Camera.ThinLens { 78 | .vfov = if (gltf_camera.type == .perspective) gltf_camera.type.perspective.yfov else std.math.pi / 4.0, 79 | }, 80 | .orthographic = Camera.Orthographic { 81 | .vscale = if (gltf_camera.type == .orthographic) gltf_camera.type.orthographic.ymag else 1, 82 | }, 83 | }, try allocator.dupeZ(u8, gltf_camera.name)); 84 | } 85 | } 86 | 87 | // add default camera if none loaded 88 | if (camera.cameras.items.len == 0) { 89 | const transform = Mat4.fromRows(.{ 90 | .new(.{1, 0, 0, 0}), 91 | .new(.{0, 1, 0, 0}), 92 | .new(.{0, 0, 1, 5}), // looking at origin 93 | .new(.{0, 0, 0, 1}), 94 | }); 95 | _ = try camera.appendCamera(allocator, Camera.Camera { 96 | .transform = transform.mul(msne_camera_to_gltf_camera).truncateRow(), 97 | }, try allocator.dupeZ(u8, "default")); 98 | } 99 | } 100 | 101 | var world = try World.fromGltf(vc, allocator, encoder, gltf, std.fs.path.dirname(gltf_filepath)); 102 | errdefer world.destroy(vc, allocator); 103 | 104 | var background = try Background.create(vc, allocator); 105 | errdefer background.destroy(vc, allocator); 106 | { 107 | const skybox_image = try exr.helpers.Rgba2D.load(allocator, skybox_filepath); 108 | defer allocator.free(skybox_image.asSlice()); 109 | _ = try background.addBackground(vc, allocator, encoder, skybox_image, Mat3.fromRows(.{ 110 | // glTF assets have +Y as up, but our environment maps have +Z as up. swap the two. 111 | .new(.{ 1, 0, 0}), 112 | .new(.{ 0, 0, 1}), 113 | .new(.{ 0, 1, 0}), 114 | }), "exr"); 115 | } 116 | 117 | return Self { 118 | .world = world, 119 | .background = background, 120 | .camera = camera, 121 | }; 122 | } 123 | 124 | pub fn pushDescriptors(self: *const Self, camera: u32, sensor: u32, background: u32) engine.hrtsystem.pipeline.StandardBindings { 125 | _ = camera; 126 | return engine.hrtsystem.pipeline.StandardBindings { 127 | .tlas = self.world.accel.tlas_handle, 128 | .instances = self.world.accel.instances_device.deviceSlice(), 129 | .world_to_instances = self.world.accel.world_to_instance_device.deviceSlice(), 130 | .meshes = self.world.meshes.device.deviceSlice(), 131 | .geometries = self.world.models.geometries_device.deviceSlice(), 132 | .models = self.world.models.models_device.deviceSlice(), 133 | .materials = self.world.materials.materials.deviceSlice(), 134 | .instance_powers = self.world.accel.instance_powers.deviceSlice(), 135 | .background_image = .{ .view = self.background.backgrounds.items[background].image.view }, 136 | .output_image = .{ .view = self.camera.sensors.items[sensor].image.view }, 137 | }; 138 | } 139 | 140 | pub fn pushConstants(self: *const Self, camera: u32, sensor: u32, background: u32, seed: u32) engine.hrtsystem.pipeline.StandardPushConstants { 141 | return engine.hrtsystem.pipeline.StandardPushConstants { 142 | .instance_count = self.world.accel.instance_count, 143 | .camera = self.camera.cameras.items[camera][1], 144 | .aspect_ratio = self.camera.sensors.items[sensor].aspectRatio(), 145 | .sample_count = self.camera.sensors.items[sensor].sample_count, 146 | .global_volume = self.global_volume, 147 | .background_to_world = self.background.backgrounds.items[background].transform, 148 | .seed = seed, 149 | }; 150 | } 151 | 152 | pub fn destroy(self: *Self, vc: *const VulkanContext, allocator: std.mem.Allocator) void { 153 | self.world.destroy(vc, allocator); 154 | self.background.destroy(vc, allocator); 155 | self.camera.destroy(vc, allocator); 156 | } 157 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/Sensor.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const vk = @import("vulkan"); 3 | 4 | const engine = @import("../engine.zig"); 5 | const VulkanContext = engine.core.VulkanContext; 6 | const Encoder = engine.core.Encoder; 7 | const Image = engine.core.Image; 8 | 9 | image: Image, 10 | extent: vk.Extent2D, 11 | sample_count: u32, 12 | 13 | const Self = @This(); 14 | 15 | pub fn create(vc: *const VulkanContext, extent: vk.Extent2D, name: [:0]const u8) !Self { 16 | const image = try Image.create(vc, extent, .{ .storage_bit = true, .transfer_src_bit = true, }, .r32g32b32a32_sfloat, false, name); 17 | errdefer image.destroy(vc); 18 | 19 | return Self { 20 | .image = image, 21 | .extent = extent, 22 | .sample_count = 0, 23 | }; 24 | } 25 | 26 | pub fn aspectRatio(self: Self) f32 { 27 | return @as(f32, @floatFromInt(self.extent.width)) / @as(f32, @floatFromInt(self.extent.height)); 28 | } 29 | 30 | // intended to be used in a loop, e.g 31 | // 32 | // while rendering: 33 | // recordPrepareForCapture(...) 34 | // ... 35 | // recordPrepareForCopy(...) 36 | pub fn recordPrepareForCapture(self: *const Self, command_buffer: VulkanContext.CommandBuffer, capture_stage: vk.PipelineStageFlags2, copy_stage: vk.PipelineStageFlags2) void { 37 | command_buffer.pipelineBarrier2(&vk.DependencyInfo{ 38 | .image_memory_barrier_count = 1, 39 | .p_image_memory_barriers = (&vk.ImageMemoryBarrier2{ 40 | .src_stage_mask = copy_stage, 41 | .src_access_mask = if (!std.meta.eql(copy_stage, .{})) .{ .transfer_read_bit = true } else .{}, 42 | .dst_stage_mask = capture_stage, 43 | .dst_access_mask = if (self.sample_count == 0) .{ .shader_storage_write_bit = true } else .{ .shader_storage_write_bit = true, .shader_storage_read_bit = true }, 44 | .old_layout = if (self.sample_count == 0) .undefined else .transfer_src_optimal, 45 | .new_layout = .general, 46 | .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED, 47 | .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED, 48 | .image = self.image.handle, 49 | .subresource_range = .{ 50 | .aspect_mask = .{ .color_bit = true }, 51 | .base_mip_level = 0, 52 | .level_count = 1, 53 | .base_array_layer = 0, 54 | .layer_count = vk.REMAINING_ARRAY_LAYERS, 55 | }, 56 | })[0..1], 57 | }); 58 | } 59 | 60 | pub fn recordPrepareForCopy(self: *const Self, command_buffer: VulkanContext.CommandBuffer, capture_stage: vk.PipelineStageFlags2, copy_stage: vk.PipelineStageFlags2) void { 61 | command_buffer.pipelineBarrier2(&vk.DependencyInfo{ 62 | .image_memory_barrier_count = 1, 63 | .p_image_memory_barriers = (&vk.ImageMemoryBarrier2 { 64 | .src_stage_mask = capture_stage, 65 | .src_access_mask = if (self.sample_count == 0) .{ .shader_storage_write_bit = true } else .{ .shader_storage_write_bit = true, .shader_storage_read_bit = true }, 66 | .dst_stage_mask = copy_stage, 67 | .dst_access_mask = if (!std.meta.eql(copy_stage, .{})) .{ .transfer_read_bit = true } else .{}, 68 | .old_layout = .general, 69 | .new_layout = .transfer_src_optimal, 70 | .src_queue_family_index = vk.QUEUE_FAMILY_IGNORED, 71 | .dst_queue_family_index = vk.QUEUE_FAMILY_IGNORED, 72 | .image = self.image.handle, 73 | .subresource_range = .{ 74 | .aspect_mask = .{ .color_bit = true }, 75 | .base_mip_level = 0, 76 | .level_count = 1, 77 | .base_array_layer = 0, 78 | .layer_count = vk.REMAINING_ARRAY_LAYERS, 79 | }, 80 | })[0..1], 81 | }); 82 | } 83 | 84 | pub fn clear(self: *Self) void { 85 | self.sample_count = 0; 86 | } 87 | 88 | pub fn destroy(self: *Self, vc: *const VulkanContext) void { 89 | self.image.destroy(vc); 90 | } 91 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/hrtsystem.zig: -------------------------------------------------------------------------------- 1 | pub const Accel = @import("Accel.zig"); 2 | pub const CameraManager = @import("CameraManager.zig"); 3 | pub const MeshManager = @import("MeshManager.zig"); 4 | pub const MaterialManager = @import("MaterialManager.zig"); 5 | pub const ModelManager = @import("ModelManager.zig"); 6 | pub const BackgroundManager = @import("BackgroundManager.zig"); 7 | pub const pipeline = @import("pipeline.zig"); 8 | pub const World = @import("World.zig"); 9 | pub const Scene = @import("Scene.zig"); 10 | pub const ConstantSpectra = @import("ConstantSpectra.zig"); 11 | pub const Sensor = @import("Sensor.zig"); 12 | 13 | const vk = @import("vulkan"); 14 | 15 | pub const required_device_extensions = [_][*:0]const u8{ 16 | vk.extensions.khr_deferred_host_operations.name, 17 | vk.extensions.khr_acceleration_structure.name, 18 | vk.extensions.khr_ray_query.name, 19 | }; 20 | 21 | pub const required_device_features = vk.PhysicalDeviceRayQueryFeaturesKHR { 22 | .p_next = @constCast(&vk.PhysicalDeviceAccelerationStructureFeaturesKHR { 23 | .acceleration_structure = vk.TRUE, 24 | }), 25 | .ray_query = vk.TRUE, 26 | }; 27 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/pipeline.zig: -------------------------------------------------------------------------------- 1 | const shaders = @import("hrtsystem_shaders"); 2 | const vk = @import("vulkan"); 3 | const std = @import("std"); 4 | const build_options = @import("build_options"); 5 | 6 | const engine = @import("../engine.zig"); 7 | const core = engine.core; 8 | const Pipeline = core.pipeline.Pipeline; 9 | const VulkanContext = core.VulkanContext; 10 | const Encoder = core.Encoder; 11 | const descriptor = core.descriptor; 12 | 13 | const Camera = @import("./CameraManager.zig"); 14 | const Material = @import("./MaterialManager.zig"); 15 | 16 | const vector = engine.vector; 17 | const F32x2 = vector.Vec2(f32); 18 | const F32x3 = vector.Vec3(f32); 19 | const Mat3 = vector.Mat3(f32); 20 | const Mat4x3 = vector.Mat4x3(f32); 21 | 22 | pub const StandardBindings = struct { 23 | tlas: ?vk.AccelerationStructureKHR, 24 | instances: ?core.mem.BufferSlice(vk.AccelerationStructureInstanceKHR), 25 | world_to_instances: ?core.mem.BufferSlice(Mat4x3), 26 | meshes: ?core.mem.BufferSlice(engine.hrtsystem.MeshManager.Mesh.Device), 27 | geometries: ?core.mem.BufferSlice(engine.hrtsystem.ModelManager.Geometry.Device), 28 | models: ?core.mem.BufferSlice(engine.hrtsystem.ModelManager.Model.Device), 29 | materials: ?core.mem.BufferSlice(engine.hrtsystem.MaterialManager.Material.Device), 30 | instance_powers: core.mem.BufferSlice(F32x3), 31 | background_image: core.pipeline.CombinedImageSampler, 32 | output_image: core.pipeline.StorageImage, 33 | }; 34 | 35 | pub const StandardPushConstants = extern struct { 36 | instance_count: u32, 37 | camera: Camera.Camera, 38 | aspect_ratio: f32, 39 | sample_count: u32, 40 | global_volume: Material.Volume, 41 | background_to_world: Mat3, 42 | seed: u32, 43 | }; 44 | 45 | pub const Integrator = enum(u32) { 46 | direct_lighting, 47 | path_tracing, 48 | volume_path_tracing, 49 | }; 50 | 51 | pub const StandardPipeline = Pipeline(.{ 52 | .local_size = vk.Extent3D { .width = 8, .height = 8, .depth = 1 }, 53 | .shader_source = shaders.main, 54 | .SpecConstants = extern struct { 55 | integrator: Integrator = .path_tracing, 56 | direct_lighting_env_samples: u32 = 1, 57 | direct_lighting_mesh_samples: u32 = 1, 58 | direct_lighting_brdf_samples: u32 = 1, 59 | path_tracing_russian_roulette_depth: u32 = 3, 60 | path_tracing_env_samples_per_bounce: u32 = 1, 61 | path_tracing_mesh_samples_per_bounce: u32 = 1, 62 | volume_path_tracing_russian_roulette_depth: u32 = 3, 63 | volume_path_tracing_env_samples_per_bounce: u32 = 1, 64 | volume_path_tracing_mesh_samples_per_bounce: u32 = 1, 65 | }, 66 | .PushConstants = StandardPushConstants, 67 | .additional_descriptor_layout_count = 2, 68 | .PushSetBindings = StandardBindings, 69 | }); 70 | 71 | pub fn dispatch(pipeline: anytype, encoder: *Encoder, extent: vk.Extent2D) void { 72 | const width = std.math.divCeil(extent.width, 32) catch unreachable; 73 | const height = std.math.divCeil(extent.height, 32) catch unreachable; 74 | pipeline.recordDispatch(encoder.buffer, .{ .width = width, .height = height, .depth = 1 }); 75 | } -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/background/equirectangular_to_equal_area.hlsl: -------------------------------------------------------------------------------- 1 | #include "../../../shaders/utils/helpers.hlsl" 2 | #include "../../../shaders/utils/mappings.hlsl" 3 | 4 | [[vk::combinedImageSampler]] [[vk::binding(0, 0)]] Texture2D srcTexture; 5 | [[vk::combinedImageSampler]] [[vk::binding(0, 0)]] SamplerState srcTextureSampler; 6 | 7 | [[vk::binding(1, 0)]] RWTexture2D dstImage; 8 | 9 | [numthreads(8, 8, 1)] 10 | void main(uint3 dispatchXYZ: SV_DispatchThreadID) { 11 | const uint2 pixelIndex = dispatchXYZ.xy; 12 | const uint2 dstImageSize = textureDimensions(dstImage); 13 | 14 | if (any(pixelIndex >= dstImageSize)) return; 15 | 16 | float3 color = float3(0.0, 0.0, 0.0); 17 | const uint samples_per_dim = 3; 18 | for (uint i = 0; i < samples_per_dim; i++) { 19 | for (uint j = 0; j < samples_per_dim; j++) { 20 | const float2 subpixel = float2(1 + i, 1 + j) / float2(samples_per_dim + 1, samples_per_dim + 1); 21 | const float2 dstCoords = (float2(pixelIndex) + subpixel) / float2(dstImageSize); 22 | const float3 dir = squareToEqualAreaSphere(dstCoords); 23 | const float2 srcCoords = cartesianToSpherical(dir) / float2(2 * PI, PI); 24 | // not sure if there's a standard canonical environment map orientation, 25 | // but rotate this half a turn so that our default matches blender 26 | const float2 srcCoordsRotated = frac(srcCoords + float2(0.5, 0)); 27 | color += srcTexture.SampleLevel(srcTextureSampler, srcCoordsRotated, 0); // could also technically compute some sort of gradient and get area subtended by this pixel 28 | } 29 | } 30 | 31 | const uint total_samples = samples_per_dim * samples_per_dim; 32 | dstImage[pixelIndex] = float4(color / float(total_samples), 1); 33 | } -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/background/fold.hlsl: -------------------------------------------------------------------------------- 1 | #include "../../../shaders/utils/helpers.hlsl" 2 | 3 | [[vk::binding(0, 0)]] Texture2D srcMip; 4 | [[vk::binding(1, 0)]] RWTexture2D dstMip; 5 | 6 | [numthreads(8, 8, 1)] 7 | void main(uint3 dispatchXYZ: SV_DispatchThreadID) { 8 | const uint2 pixelIndex = dispatchXYZ.xy; 9 | const uint2 dstImageSize = textureDimensions(dstMip); 10 | 11 | if (any(pixelIndex >= dstImageSize)) return; 12 | 13 | dstMip[pixelIndex] = srcMip[2 * pixelIndex + uint2(0, 0)] 14 | + srcMip[2 * pixelIndex + uint2(1, 0)] 15 | + srcMip[2 * pixelIndex + uint2(0, 1)] 16 | + srcMip[2 * pixelIndex + uint2(1, 1)]; 17 | } -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/camera.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../../shaders/utils/mappings.hlsl" 4 | #include "../../shaders/utils/helpers.hlsl" 5 | #include "ray.hlsl" 6 | 7 | // in camera space, cameras are oriented: 8 | // * forward along +X 9 | // * right along +Y 10 | // * down along +Z 11 | 12 | enum class CameraModel : uint { 13 | ThinLens, 14 | Orthographic, 15 | }; 16 | 17 | struct ThinLens { 18 | float vfov; 19 | float aperture; 20 | float focusDistance; 21 | 22 | // uv is [-aspectRatio, aspectRatio], [-1, 1] 23 | Ray generateRay(const float2 rand, const float2 uv) { 24 | const float2 halfViewport = tan(vfov / 2); 25 | 26 | const float3 directionCameraSpaceUnorm = float3(1.0, uv * halfViewport); 27 | 28 | const float2 lens = aperture * squareToUniformDiskConcentric(rand) / 2.0; 29 | const float3 focus = focusDistance * directionCameraSpaceUnorm; 30 | 31 | Ray ray; 32 | ray.origin = float3(0.0, lens); 33 | ray.direction = normalize(focus - ray.origin); 34 | 35 | return ray; 36 | } 37 | }; 38 | 39 | struct Orthographic { 40 | float vscale; 41 | 42 | Ray generateRay(const float2 uv) { 43 | const float2 halfViewport = vscale; 44 | 45 | Ray ray; 46 | ray.origin = float3(0.0, uv * halfViewport); 47 | ray.direction = float3(1, 0, 0); 48 | 49 | return ray; 50 | } 51 | }; 52 | 53 | struct Camera { 54 | row_major float3x4 toWorld; 55 | CameraModel model; 56 | ThinLens thinLens; 57 | Orthographic orthographic; 58 | float aspect; 59 | 60 | Ray generateRay(const float2 uv, const float2 rand) { 61 | const float2 uvScaled = (uv * 2 - 1) * float2(aspect, 1); 62 | const Ray rayCameraSpace = thinLens.generateRay(rand, uvScaled); 63 | switch (model) { 64 | case CameraModel::ThinLens: { 65 | return thinLens.generateRay(rand, uvScaled).transformed(toWorld); 66 | } 67 | case CameraModel::Orthographic: { 68 | return orthographic.generateRay(uvScaled).transformed(toWorld); 69 | } 70 | } 71 | } 72 | }; 73 | 74 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/intersection.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../../shaders/utils/math.hlsl" 4 | #include "ray.hlsl" 5 | 6 | struct Intersection { 7 | uint instanceIndex; 8 | uint geometryIndex; 9 | uint primitiveIndex; 10 | float2 barycentrics; 11 | 12 | static Intersection create(uint instanceIndex, uint geometryIndex, uint primitiveIndex, float2 barycentrics) { 13 | Intersection its; 14 | its.instanceIndex = instanceIndex; 15 | its.geometryIndex = geometryIndex; 16 | its.primitiveIndex = primitiveIndex; 17 | its.barycentrics = barycentrics; 18 | return its; 19 | } 20 | 21 | static Intersection createMiss() { 22 | Intersection its; 23 | its.instanceIndex = MAX_UINT; 24 | return its; 25 | } 26 | 27 | static Intersection find(RaytracingAccelerationStructure accel, Ray ray) { 28 | return Intersection::find(accel, ray, 1.#INF); 29 | } 30 | 31 | static Intersection find(RaytracingAccelerationStructure accel, Ray ray, float tmax) { 32 | return Intersection::find(accel, ray, tmax, 0xFF); 33 | } 34 | 35 | static Intersection find(RaytracingAccelerationStructure accel, Ray ray, float tmax, uint mask) { 36 | RayQuery q; 37 | q.TraceRayInline(accel, 0, mask, ray.desc(0, tmax)); 38 | 39 | while (q.Proceed()) {}; 40 | 41 | if (q.CommittedStatus() != COMMITTED_TRIANGLE_HIT) { 42 | return Intersection::createMiss(); 43 | } 44 | 45 | // for some reason dxc won't let me put an else here :think: 46 | return Intersection::create( 47 | q.CommittedInstanceIndex(), 48 | q.CommittedGeometryIndex(), 49 | q.CommittedPrimitiveIndex(), 50 | q.CommittedTriangleBarycentrics() 51 | ); 52 | } 53 | 54 | bool hit() { 55 | return instanceIndex != MAX_UINT; 56 | } 57 | }; 58 | 59 | struct ShadowIntersection { 60 | bool inShadow; 61 | 62 | static bool hit(RaytracingAccelerationStructure accel, Ray ray, float tmax) { 63 | return ShadowIntersection::hit(accel, ray, tmax, 0xFF); 64 | } 65 | 66 | static bool hit(RaytracingAccelerationStructure accel, Ray ray, float tmax, uint mask) { 67 | RayQuery q; 68 | q.TraceRayInline(accel, 0, mask, ray.desc(0, tmax)); 69 | q.Proceed(); 70 | 71 | ShadowIntersection its; 72 | its.inShadow = q.CommittedStatus() == COMMITTED_TRIANGLE_HIT; 73 | return its.inShadow; 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/local_light/fold1.hlsl: -------------------------------------------------------------------------------- 1 | [[vk::binding(0, 0)]] RWStructuredBuffer levels; 2 | 3 | struct PushConsts { 4 | uint srcLevelOffset; 5 | uint dstLevelOffset; 6 | uint maxSrcIndex; 7 | }; 8 | [[vk::push_constant]] PushConsts pushConsts; 9 | 10 | [numthreads(32, 1, 1)] 11 | void main(uint3 dispatchXYZ: SV_DispatchThreadID) { 12 | const uint dstIndex = dispatchXYZ.x; 13 | const uint dstLevelSize = pushConsts.srcLevelOffset - pushConsts.dstLevelOffset; 14 | 15 | if (any(dstIndex >= dstLevelSize)) return; 16 | 17 | const uint srcBaseIndex = 2 * dstIndex; 18 | 19 | if (srcBaseIndex < pushConsts.maxSrcIndex) { 20 | levels[pushConsts.dstLevelOffset + dstIndex] = levels[pushConsts.srcLevelOffset + srcBaseIndex + 0] 21 | + levels[pushConsts.srcLevelOffset + srcBaseIndex + 1]; 22 | } else { 23 | levels[pushConsts.dstLevelOffset + dstIndex] = 0; 24 | } 25 | } -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/local_light/fold3.hlsl: -------------------------------------------------------------------------------- 1 | [[vk::binding(0, 0)]] RWStructuredBuffer levels; 2 | 3 | struct PushConsts { 4 | uint srcLevelOffset; 5 | uint dstLevelOffset; 6 | uint maxSrcIndex; 7 | }; 8 | [[vk::push_constant]] PushConsts pushConsts; 9 | 10 | [numthreads(32, 1, 1)] 11 | void main(uint3 dispatchXYZ: SV_DispatchThreadID) { 12 | const uint dstIndex = dispatchXYZ.x; 13 | const uint dstLevelSize = pushConsts.srcLevelOffset - pushConsts.dstLevelOffset; 14 | 15 | if (any(dstIndex >= dstLevelSize)) return; 16 | 17 | const uint srcBaseIndex = 2 * dstIndex; 18 | 19 | if (srcBaseIndex < pushConsts.maxSrcIndex) { 20 | levels[pushConsts.dstLevelOffset + dstIndex] = levels[pushConsts.srcLevelOffset + srcBaseIndex + 0] 21 | + levels[pushConsts.srcLevelOffset + srcBaseIndex + 1]; 22 | } else { 23 | levels[pushConsts.dstLevelOffset + dstIndex] = 0; 24 | } 25 | } -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/local_light/geometry_power.hlsl: -------------------------------------------------------------------------------- 1 | #include "../world.hlsl" 2 | 3 | [[vk::binding(0, 0)]] StructuredBuffer dGeometries; 4 | 5 | // dst 6 | [[vk::binding(1, 0)]] RWStructuredBuffer dstPower; 7 | 8 | // mesh info 9 | struct PushConsts { 10 | uint geometryCount; 11 | uint srcOffset; 12 | uint dstOffset; 13 | }; 14 | [[vk::push_constant]] PushConsts pushConsts; 15 | 16 | [numthreads(32, 1, 1)] 17 | void main(uint3 dispatchXYZ: SV_DispatchThreadID) { 18 | const uint srcIdx = dispatchXYZ.x; 19 | 20 | if (srcIdx >= pushConsts.geometryCount) { 21 | if (pushConsts.geometryCount != 1 && pushConsts.geometryCount % 2 == 1 && srcIdx == pushConsts.geometryCount) { 22 | // may be one element of padding, which should be zeroed 23 | dstPower[pushConsts.dstOffset + srcIdx] = 0; 24 | } 25 | return; 26 | } 27 | 28 | const Geometry geometry = dGeometries[pushConsts.srcOffset + srcIdx]; 29 | const float3x3 power = vk::RawBufferLoad(geometry.trianglePowersAddress); // TODO: transform 30 | 31 | dstPower[pushConsts.dstOffset + srcIdx] = power; 32 | } -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/local_light/instance_power.hlsl: -------------------------------------------------------------------------------- 1 | #include "../world.hlsl" 2 | 3 | [[vk::binding(0, 0)]] StructuredBuffer dInstances; 4 | [[vk::binding(1, 0)]] StructuredBuffer dWorldToInstance; 5 | [[vk::binding(2, 0)]] StructuredBuffer dModels; 6 | 7 | // dst 8 | [[vk::binding(3, 0)]] RWStructuredBuffer dstPower; 9 | 10 | struct PushConsts { 11 | uint instanceCount; 12 | uint dstOffset; 13 | }; 14 | [[vk::push_constant]] PushConsts pushConsts; 15 | 16 | [numthreads(32, 1, 1)] 17 | void main(uint3 dispatchXYZ: SV_DispatchThreadID) { 18 | const uint srcInstance = dispatchXYZ.x; 19 | 20 | if (srcInstance >= pushConsts.instanceCount) { 21 | if (pushConsts.instanceCount != 1 && pushConsts.instanceCount % 2 == 1 && srcInstance == pushConsts.instanceCount) { 22 | // may be one element of padding, which should be zeroed 23 | dstPower[pushConsts.dstOffset + srcInstance] = 0; 24 | } 25 | return; 26 | } 27 | 28 | const Instance instance = dInstances[srcInstance]; 29 | const Model model = dModels[instance.instanceCustomIndex]; 30 | 31 | const float3x3 toWorld = (float3x3)instance.transform; 32 | const float3x3 toLocal = (float3x3)dWorldToInstance[srcInstance]; 33 | const float3x3 cofactor = abs(transpose(toLocal) * determinant(toWorld)); 34 | 35 | const float3x3 modelPower = transpose(vk::RawBufferLoad(model.geometryPowersAddress)); 36 | dstPower[pushConsts.dstOffset + srcInstance] = float3( 37 | normL1(mul(cofactor, modelPower[0])), 38 | normL1(mul(cofactor, modelPower[1])), 39 | normL1(mul(cofactor, modelPower[2])) 40 | ); 41 | } -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/local_light/triangle_power.hlsl: -------------------------------------------------------------------------------- 1 | #include "../world.hlsl" 2 | 3 | [[vk::binding(0, 0)]] StructuredBuffer dMeshes; 4 | [[vk::binding(1, 0)]] StructuredBuffer dMaterials; 5 | 6 | // dst 7 | [[vk::binding(2, 0)]] RWStructuredBuffer dstPower; 8 | 9 | // mesh info 10 | struct PushConsts { 11 | uint meshIndex; 12 | uint materialIndex; 13 | uint triangleCount; 14 | uint dstOffset; 15 | }; 16 | [[vk::push_constant]] PushConsts pushConsts; 17 | 18 | [numthreads(32, 1, 1)] 19 | void main(uint3 dispatchXYZ: SV_DispatchThreadID) { 20 | const uint srcPrimitive = dispatchXYZ.x; 21 | 22 | if (srcPrimitive >= pushConsts.triangleCount) { 23 | if (pushConsts.triangleCount != 1 && pushConsts.triangleCount % 2 == 1 && srcPrimitive == pushConsts.triangleCount) { 24 | // may be one element of padding, which should be zeroed 25 | dstPower[pushConsts.dstOffset + srcPrimitive] = 0; 26 | } 27 | return; 28 | } 29 | 30 | const TriangleLocalSpace tri = dMeshes[pushConsts.meshIndex].triangleLocalSpace(srcPrimitive); 31 | const Material material = dMaterials[pushConsts.materialIndex]; 32 | 33 | float3 totalEmissive = 0; 34 | 35 | const uint samplesPerDimension = 8; 36 | for (uint i = 0; i < samplesPerDimension; i++) { 37 | for (uint j = 0; j < samplesPerDimension; j++) { 38 | const float2 barycentrics = squareToTriangle(float2(i, j) / float(samplesPerDimension)); 39 | const float2 texcoord = interpolate(float3(1.0 - barycentrics.x - barycentrics.y, barycentrics.x, barycentrics.y), tri.texcoords); 40 | totalEmissive += dTextures[NonUniformResourceIndex(material.emissive)].SampleLevel(dTextureSampler, texcoord, 0).rgb; 41 | } 42 | } 43 | 44 | const float3 averageEmissive = totalEmissive / float(samplesPerDimension * samplesPerDimension); 45 | const float3 projectedArea = tri.bivector() / 2.0; 46 | const float3x3 power = PI * float3x3( 47 | abs(projectedArea.x) * averageEmissive, 48 | abs(projectedArea.y) * averageEmissive, 49 | abs(projectedArea.z) * averageEmissive 50 | ); 51 | 52 | dstPower[pushConsts.dstOffset + srcPrimitive] = power; 53 | } -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/main.hlsl: -------------------------------------------------------------------------------- 1 | #include "intersection.hlsl" 2 | #include "camera.hlsl" 3 | #include "scene.hlsl" 4 | #include "integrator.hlsl" 5 | #include "medium.hlsl" 6 | 7 | // I use the `d` prefix to indicate a descriptor variable 8 | // because as a functional programmer impure functions scare me 9 | 10 | // GEOMETRY 11 | [[vk::binding(0, 0)]] RaytracingAccelerationStructure dTLAS; 12 | [[vk::binding(1, 0)]] StructuredBuffer dInstances; 13 | [[vk::binding(2, 0)]] StructuredBuffer dWorldToInstance; 14 | [[vk::binding(3, 0)]] StructuredBuffer dMeshes; 15 | [[vk::binding(4, 0)]] StructuredBuffer dGeometries; 16 | [[vk::binding(5, 0)]] StructuredBuffer dModels; 17 | [[vk::binding(6, 0)]] StructuredBuffer dMaterials; 18 | 19 | // EMISSIVE TRIANGLES 20 | [[vk::binding(7, 0)]] StructuredBuffer dInstancePower; 21 | 22 | // BACKGROUND 23 | [[vk::combinedImageSampler]] [[vk::binding(8, 0)]] Texture2D dBackgroundTexture; 24 | [[vk::combinedImageSampler]] [[vk::binding(8, 0)]] SamplerState dBackgroundSampler; 25 | 26 | // OUTPUT 27 | [[vk::binding(9, 0)]] RWTexture2D dOutputImage; 28 | 29 | // PUSH CONSTANTS 30 | struct PushConsts { 31 | uint instanceCount; 32 | Camera camera; 33 | uint sampleCount; 34 | ChromaticVolume globalVolume; 35 | float3x3 backgroundToWorld; 36 | uint seed; 37 | }; 38 | [[vk::push_constant]] PushConsts pushConsts; 39 | 40 | enum class IntegratorType : uint { 41 | DirectLight, 42 | PathTracing, 43 | VolumePathTracing, 44 | }; 45 | 46 | [[vk::constant_id(0)]] const uint dIntegratorType = 1; 47 | [[vk::constant_id(1)]] const uint dDirectLightEnvSamples = 1; // how many times the environment map should be sampled for light 48 | [[vk::constant_id(2)]] const uint dDirectLightMeshSamples = 1; // how many times emissive meshes should be sampled for light 49 | [[vk::constant_id(3)]] const uint dDirectLightBrdfSamples = 1; // how many times BRDF should be sampled for light 50 | [[vk::constant_id(4)]] const uint dPathTracingRussianRouletteDepth = 3; // at which bounce depth russian roulette should start 51 | [[vk::constant_id(5)]] const uint dPathTracingEnvSamplesPerBounce = 1; // how many times the environment map should be sampled per bounce for light 52 | [[vk::constant_id(6)]] const uint dPathTracingMeshSamplesPerBounce = 1; // how many times emissive meshes should be sampled per bounce for light 53 | [[vk::constant_id(7)]] const uint dVolumePathTracingRussianRouletteDepth = 3; // at which bounce depth russian roulette should start 54 | [[vk::constant_id(8)]] const uint dVolumePathTracingEnvSamplesPerBounce = 1; // how many times the environment map should be sampled per bounce for light 55 | [[vk::constant_id(9)]] const uint dVolumePathTracingMeshSamplesPerBounce = 1; // how many times emissive meshes should be sampled per bounce for light 56 | 57 | [numthreads(8, 8, 1)] 58 | void main(uint3 dispatchXYZ: SV_DispatchThreadID) { 59 | const uint2 imageCoords = dispatchXYZ.xy; 60 | const uint2 imageSize = textureDimensions(dOutputImage); 61 | 62 | if (any(imageCoords >= imageSize)) return; 63 | 64 | World world; 65 | world.instances = dInstances; 66 | world.worldToInstance = dWorldToInstance; 67 | world.meshes = dMeshes; 68 | world.models = dModels; 69 | world.geometries = dGeometries; 70 | world.materials = dMaterials; 71 | 72 | Scene scene; 73 | scene.tlas = dTLAS; 74 | scene.world = world; 75 | scene.envMap = EnvMap::create(pushConsts.backgroundToWorld, dBackgroundTexture, dBackgroundSampler); 76 | scene.instanceLights = InstanceLights::create(dInstancePower, pushConsts.instanceCount, world); 77 | scene.globalVolume = pushConsts.globalVolume; 78 | 79 | Rng rng = Rng::fromSeed(uint3(pushConsts.seed, imageCoords.x, imageCoords.y)); 80 | 81 | // set up initial ray 82 | const float2 jitter = float2(rng.getFloat(), rng.getFloat()); 83 | const float2 imageUV = (imageCoords + jitter) / imageSize; 84 | const Ray initialRay = pushConsts.camera.generateRay(imageUV, float2(rng.getFloat(), rng.getFloat())); 85 | 86 | // trace the ray 87 | WavelengthSample w = WavelengthSample::sampleVisible(rng.getFloat()); 88 | 89 | const IntegratorType integratorType = (IntegratorType)dIntegratorType; 90 | float radiance; 91 | switch (integratorType) { 92 | case IntegratorType::DirectLight: { 93 | const DirectLightIntegrator integrator = DirectLightIntegrator::create(dDirectLightEnvSamples, dDirectLightMeshSamples, dDirectLightBrdfSamples); 94 | radiance = integrator.incomingRadiance(scene, initialRay, w.λ, rng); 95 | break; 96 | } 97 | case IntegratorType::PathTracing: { 98 | const PathTracingIntegrator integrator = PathTracingIntegrator::create(dPathTracingRussianRouletteDepth, dPathTracingEnvSamplesPerBounce, dPathTracingMeshSamplesPerBounce); 99 | radiance = integrator.incomingRadiance(scene, initialRay, w.λ, rng); 100 | break; 101 | } 102 | case IntegratorType::VolumePathTracing: { 103 | const VolumePathTracingIntegrator integrator = VolumePathTracingIntegrator::create(dVolumePathTracingRussianRouletteDepth, dVolumePathTracingEnvSamplesPerBounce, dVolumePathTracingMeshSamplesPerBounce); 104 | radiance = integrator.incomingRadiance(scene, initialRay, w.λ, rng); 105 | break; 106 | } 107 | } 108 | 109 | // accumulate 110 | const float3 priorSampleAverage = pushConsts.sampleCount == 0 ? 0 : dOutputImage[imageCoords].xyz; 111 | const float3 newSample = Spectrum::toLinearSRGB(w.λ, radiance) / w.pdf; 112 | const float3 newAverage = accumulate(priorSampleAverage, newSample, pushConsts.sampleCount); 113 | dOutputImage[imageCoords] = float4(newAverage, 1); 114 | } 115 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/material.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | [[vk::binding(0, 1)]] Texture2D dTextures[]; 4 | [[vk::binding(1, 1)]] SamplerState dTextureSampler; 5 | 6 | #include "spectrum.hlsl" 7 | #include "volume.hlsl" 8 | #include "bsdf.hlsl" 9 | 10 | float3 decodeNormal(float2 rg) { 11 | rg = rg * 2 - 1; 12 | return float3(rg, sqrt(1.0 - saturate(dot(rg, rg)))); // saturate due to float/compression annoyingness 13 | } 14 | 15 | float3 tangentNormalToWorld(float3 normalTangentSpace, Frame tangentFrame) { 16 | return normalize(tangentFrame.frameToWorld(normalTangentSpace)).xyz; 17 | } 18 | 19 | Frame createTextureFrame(float3 normalWorldSpace, Frame tangentFrame) { 20 | Frame textureFrame = tangentFrame; 21 | textureFrame.n = normalWorldSpace; 22 | textureFrame.reorthogonalize(); 23 | 24 | return textureFrame; 25 | } 26 | 27 | enum class BSDFType : uint { 28 | Glass, 29 | Lambert, 30 | PerfectMirror, 31 | StandardPBR, 32 | }; 33 | 34 | struct Material { 35 | uint normal; 36 | uint emissive; 37 | 38 | // volume enclosed by the mesh of this material 39 | ChromaticVolume volume; 40 | 41 | // find appropriate thing to decode from address using `type` 42 | BSDFType type; 43 | uint64_t addr; 44 | 45 | Frame getTextureFrame(float2 texcoords, Frame tangentFrame) { 46 | const float2 rg = dTextures[NonUniformResourceIndex(normal)].SampleLevel(dTextureSampler, texcoords, 0).rg; 47 | const float3 normalTangentSpace = decodeNormal(rg); 48 | const float3 normalWorldSpace = tangentNormalToWorld(normalTangentSpace, tangentFrame); 49 | return createTextureFrame(normalWorldSpace, tangentFrame); 50 | } 51 | 52 | float getEmissive(float λ, float2 texcoords) { 53 | return Spectrum::sampleEmission(λ, dTextures[NonUniformResourceIndex(emissive)].SampleLevel(dTextureSampler, texcoords, 0).rgb); 54 | } 55 | }; 56 | 57 | struct PolymorphicBSDF : BSDF { 58 | BSDFType type; 59 | uint64_t addr; 60 | float2 texcoords; 61 | float λ; 62 | Frame shadingFrame; 63 | Frame triangleFrame; 64 | 65 | float intIOR; 66 | float extIOR; 67 | 68 | bool thin; 69 | 70 | static PolymorphicBSDF load(Material material, bool thin, float intIOR, float extIOR, float2 texcoords, Frame shadingFrame, Frame triangleFrame, float λ) { 71 | PolymorphicBSDF bsdf; 72 | bsdf.type = material.type; 73 | bsdf.addr = material.addr; 74 | bsdf.texcoords = texcoords; 75 | bsdf.λ = λ; 76 | bsdf.shadingFrame = shadingFrame; 77 | bsdf.triangleFrame = triangleFrame; 78 | bsdf.intIOR = thin ? material.volume.IOR.at(λ) : intIOR; 79 | bsdf.extIOR = extIOR; 80 | bsdf.thin = thin; 81 | return bsdf; 82 | } 83 | 84 | BSDFEvaluation evaluate(float3 w_i, float3 w_o) { 85 | BSDFEvaluation eval = BSDFEvaluation::empty(); 86 | 87 | // zero out cases where the shading normal and geometric normal disagree on the type of event 88 | const bool geometricTransmission = sign(dot(w_i, triangleFrame.n)) != sign(dot(w_o, triangleFrame.n)); 89 | const bool shadingTransmission = sign(dot(w_i, shadingFrame.n)) != sign(dot(w_o, shadingFrame.n)); 90 | if (geometricTransmission != shadingTransmission) return eval; 91 | 92 | const float3 w_i_frame = shadingFrame.worldToFrame(w_i); 93 | const float3 w_o_frame = shadingFrame.worldToFrame(w_o); 94 | 95 | switch (type) { 96 | case BSDFType::StandardPBR: { 97 | StandardPBR m = StandardPBR::load(addr, extIOR, texcoords, λ); 98 | eval = m.evaluate(w_i_frame, w_o_frame); 99 | break; 100 | } 101 | case BSDFType::Lambert: { 102 | Lambert m = Lambert::load(addr, texcoords, λ); 103 | eval = m.evaluate(w_i_frame, w_o_frame); 104 | break; 105 | } 106 | case BSDFType::PerfectMirror: { 107 | PerfectMirror m; 108 | eval = m.evaluate(w_i_frame, w_o_frame); 109 | break; 110 | } 111 | case BSDFType::Glass: { 112 | Glass m = Glass::load(intIOR, extIOR, thin); 113 | eval = m.evaluate(w_i_frame, w_o_frame); 114 | break; 115 | } 116 | } 117 | 118 | return eval; 119 | } 120 | 121 | BSDFSample sample(float3 w_o, float2 square) { 122 | const float3 w_o_frame = shadingFrame.worldToFrame(w_o); 123 | BSDFSample sample; 124 | switch (type) { 125 | case BSDFType::StandardPBR: { 126 | StandardPBR m = StandardPBR::load(addr, extIOR, texcoords, λ); 127 | sample = m.sample(w_o_frame, square); 128 | break; 129 | } 130 | case BSDFType::Lambert: { 131 | Lambert m = Lambert::load(addr, texcoords, λ); 132 | sample = m.sample(w_o_frame, square); 133 | break; 134 | } 135 | case BSDFType::PerfectMirror: { 136 | PerfectMirror m; 137 | sample = m.sample(w_o_frame, square); 138 | break; 139 | } 140 | case BSDFType::Glass: { 141 | Glass m = Glass::load(intIOR, extIOR, thin); 142 | sample = m.sample(w_o_frame, square); 143 | break; 144 | } 145 | } 146 | sample.dir = shadingFrame.frameToWorld(sample.dir); 147 | 148 | // zero out cases where the shading normal and geometric normal disagree on the type of event 149 | const bool geometricTransmission = sign(dot(sample.dir, triangleFrame.n)) != sign(dot(w_o, triangleFrame.n)); 150 | const bool shadingTransmission = sign(dot(sample.dir, shadingFrame.n)) != sign(dot(w_o, shadingFrame.n)); 151 | if (geometricTransmission != shadingTransmission) { 152 | sample.eval.attenuation = 0; 153 | sample.eval.pdf = 0; 154 | } 155 | 156 | return sample; 157 | } 158 | 159 | bool isDelta() { 160 | switch (type) { 161 | case BSDFType::StandardPBR: { 162 | return StandardPBR::isDelta(); 163 | } 164 | case BSDFType::Lambert: { 165 | return Lambert::isDelta(); 166 | } 167 | case BSDFType::PerfectMirror: { 168 | return PerfectMirror::isDelta(); 169 | } 170 | case BSDFType::Glass: { 171 | return Glass::isDelta(); 172 | } 173 | } 174 | } 175 | }; 176 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/medium.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "spectrum.hlsl" 4 | 5 | interface Medium { 6 | float transmittance(float distance); 7 | float pdf(float distance); 8 | float sample(float rand); 9 | }; 10 | 11 | struct Homogeneous : Medium { 12 | float σ_s; 13 | float σ_a; 14 | 15 | static Homogeneous create(float σ_s, float σ_a) { 16 | Homogeneous h; 17 | h.σ_s = σ_s; 18 | h.σ_a = σ_a; 19 | return h; 20 | } 21 | 22 | static Homogeneous none() { 23 | return Homogeneous::create(0, 0); 24 | } 25 | 26 | float σ_t() { 27 | return σ_s + σ_a; 28 | } 29 | 30 | float transmittance(float distance) { 31 | if (σ_t() == 0) { 32 | // if distance == INF and σ_t() == 0 we want 1 as a result, not nan 33 | return 1; 34 | } else { 35 | return exp(-σ_t() * distance); 36 | } 37 | } 38 | 39 | // P(t > distance), integral of pdf beyond distance 40 | float pMoreThanT(float distance) { 41 | return transmittance(distance); // happens to be the same as transmittance for homogeneous media 42 | } 43 | 44 | float pdf(float distance) { 45 | return exp(-σ_t() * distance) * σ_t(); 46 | } 47 | 48 | // TODO: doesn't sampling proportionally to σ_s (rather than σ_t) make more sense? 49 | // as the border case where σ_s == 0 should mean there is no scattering 50 | float sample(float rand) { 51 | return -log(1 - rand) / σ_t(); 52 | } 53 | }; 54 | 55 | struct ChromaticHomogeneous { 56 | float3 σ_s; 57 | float3 σ_a; 58 | 59 | Homogeneous at(const float λ) { 60 | return Homogeneous::create(Spectrum::sampleReflectance(λ, σ_s), Spectrum::sampleReflectance(λ, σ_a)); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/phase_function.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../../shaders/utils/mappings.hlsl" 4 | #include "bsdf.hlsl" 5 | 6 | struct Isotropic : BSDF { 7 | BSDFEvaluation evaluate(float3 w_i, float3 w_o) { 8 | BSDFEvaluation eval; 9 | eval.attenuation = 1 / (4 * PI); 10 | eval.pdf = 1 / (4 * PI); 11 | return eval; 12 | } 13 | 14 | BSDFSample sample(float3 w_o, float2 square) { 15 | const float3 w_i = squareToUniformSphere(square); 16 | BSDFSample sample; 17 | sample.dir = w_i; 18 | sample.eval = evaluate(w_i, w_o); 19 | sample.eval.attenuation = sample.eval.attenuation / sample.eval.pdf; 20 | return sample; 21 | } 22 | }; 23 | 24 | struct HenyeyGreenstein : BSDF { 25 | float g; 26 | 27 | static HenyeyGreenstein create(const float g) { 28 | HenyeyGreenstein phase; 29 | phase.g = g; 30 | return phase; 31 | } 32 | 33 | static float HG(const float g, const float cosTheta) { 34 | const float denom = 1 + g * g + 2 * g * cosTheta; 35 | return (1 - g * g) / (denom * sqrt(denom) * 4 * PI); 36 | } 37 | 38 | BSDFEvaluation evaluate(float3 w_i, float3 w_o) { 39 | BSDFEvaluation eval; 40 | eval.attenuation = HG(g, dot(w_i, w_o)); 41 | eval.pdf = HG(g, dot(w_i, w_o)); 42 | return eval; 43 | } 44 | 45 | BSDFSample sample(float3 w_o, float2 square) { 46 | // TODO: is there a sampling formulation here that doesn't need to special case zero? 47 | float cosTheta; 48 | if (abs(g) < 1e-3f) { 49 | cosTheta = 1 - 2 * square.x; 50 | } else { 51 | cosTheta = -1 / (2 * g) * (1 + g * g - (1 - g * g) * (1 - g * g) / (1 + g - 2 * g * square.x)); 52 | } 53 | const float sinTheta = sqrt(1 - cosTheta * cosTheta); 54 | const float phi = 2 * PI * square.y; 55 | const float3 w_i = Frame::create(w_o).frameToWorld(sphericalToCartesian(sinTheta, cosTheta, phi)); 56 | BSDFSample sample; 57 | sample.dir = w_i; 58 | sample.eval.pdf = HG(g, cosTheta); 59 | sample.eval.attenuation = 1; 60 | return sample; 61 | } 62 | }; -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/ray.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct Ray { 4 | float3 origin; 5 | float3 direction; 6 | 7 | RayDesc desc(float tmin, float tmax) { 8 | RayDesc desc; 9 | desc.Origin = origin; 10 | desc.Direction = direction; 11 | desc.TMin = tmin; 12 | desc.TMax = tmax; 13 | return desc; 14 | } 15 | 16 | Ray transformed(float3x4 mat) { 17 | Ray ray; 18 | ray.origin = mul(mat, float4(origin, 1.0)); 19 | ray.direction = normalize(mul(mat, float4(direction, 0.0))); 20 | return ray; 21 | } 22 | }; -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/reflection_frame.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../../shaders/utils/math.hlsl" 4 | 5 | struct Frame { 6 | float3 n; // normal 7 | float3 s; // tangent 8 | float3 t; // bitangent 9 | 10 | // takes in a normalized vector, returns a frame where x,y,z are bitangent, normal, and tangent respectively 11 | static Frame create(float3 n) { 12 | float3 t, s; 13 | coordinateSystem(n, t, s); 14 | return Frame::create(n, s, t); 15 | } 16 | 17 | static Frame create(float3 n, float3 s, float3 t) { 18 | Frame frame; 19 | frame.n = n; 20 | frame.s = s; 21 | frame.t = t; 22 | return frame; 23 | } 24 | 25 | Frame inSpace(float3x3 m) { 26 | float3 n2 = normalize(mul(m, n)); 27 | float3 s2 = normalize(mul(m, s)); 28 | float3 t2 = normalize(mul(m, t)); 29 | 30 | return Frame::create(n2, s2, t2); 31 | } 32 | 33 | void reorthogonalize() { 34 | // Gram-Schmidt 35 | s = normalize(s - n * dot(n, s)); 36 | t = normalize(cross(n, s)); 37 | } 38 | 39 | float3 worldToFrame(float3 v) { 40 | float3x3 toFrame = { s, t, n }; 41 | return mul(toFrame, v); 42 | } 43 | 44 | float3 frameToWorld(float3 v) { 45 | float3x3 toFrame = { s, t, n }; 46 | return mul(transpose(toFrame), v); 47 | } 48 | 49 | static float cosTheta(float3 v) { 50 | return v.z; 51 | } 52 | 53 | static float cos2Theta(float3 v) { 54 | return v.z * v.z; 55 | } 56 | 57 | static float sin2Theta(float3 v) { 58 | return max(0.0, 1.0 - cos2Theta(v)); 59 | } 60 | 61 | static float sinTheta(float3 v) { 62 | return sqrt(sin2Theta(v)); 63 | } 64 | 65 | static float tanTheta(float3 v) { 66 | return sinTheta(v) / cosTheta(v); 67 | } 68 | 69 | static float tan2Theta(float3 v) { 70 | return sin2Theta(v) / cos2Theta(v); 71 | } 72 | 73 | static float cosPhi(float3 v) { 74 | float sinTheta = Frame::sinTheta(v); 75 | return (sinTheta == 0.0) ? 1.0 : clamp(v.x / sinTheta, -1.0, 1.0); 76 | } 77 | 78 | static float sinPhi(float3 v) { 79 | float sinTheta = Frame::sinTheta(v); 80 | return (sinTheta == 0.0) ? 0.0 : clamp(v.y / sinTheta, -1.0, 1.0); 81 | } 82 | 83 | static bool sameHemisphere(float3 v1, float3 v2) { 84 | return v1.z * v2.z > 0.0; 85 | } 86 | }; 87 | 88 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/scene.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "light.hlsl" 4 | #include "world.hlsl" 5 | #include "volume.hlsl" 6 | 7 | struct Scene { 8 | RaytracingAccelerationStructure tlas; 9 | World world; 10 | EnvMap envMap; 11 | InstanceLights instanceLights; 12 | ChromaticVolume globalVolume; 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/spectrum.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | [[vk::binding(0, 2)]] SamplerState dSpectrumSampler; 4 | [[vk::binding(1, 2)]] Texture1D dSpectrumCIEX; 5 | [[vk::binding(2, 2)]] Texture1D dSpectrumCIEY; 6 | [[vk::binding(3, 2)]] Texture1D dSpectrumCIEZ; 7 | [[vk::binding(4, 2)]] Texture1D dSpectrumR; 8 | [[vk::binding(5, 2)]] Texture1D dSpectrumG; 9 | [[vk::binding(6, 2)]] Texture1D dSpectrumB; 10 | [[vk::binding(7, 2)]] Texture1D dSpectrumD65; 11 | 12 | #include "../../shaders/utils/random.hlsl" 13 | #include "../../shaders/utils/math.hlsl" 14 | 15 | static const float CIE1931YIntegral = 106.85691710117189; 16 | 17 | namespace Spectrum { 18 | // exclusive range 19 | float sampleTabulated(const float λ, const float start, const float end, Texture1D t) { 20 | return t.SampleLevel(dSpectrumSampler, (λ - start) / (end - start), 0); 21 | } 22 | 23 | float sampleReflectance(const float λ, const float3 reflectance) { 24 | const float samplesStart = 360; 25 | const float samplesEnd = 831; 26 | const float3 rgb = float3( 27 | sampleTabulated(λ, samplesStart, samplesEnd, dSpectrumR), 28 | sampleTabulated(λ, samplesStart, samplesEnd, dSpectrumG), 29 | sampleTabulated(λ, samplesStart, samplesEnd, dSpectrumB) 30 | ); 31 | return dot(rgb, reflectance); 32 | } 33 | 34 | // a somewhat roundabout way of doing this but I believe it's correct 35 | float sampleEmission(const float λ, const float3 emission) { 36 | const float sampledReflectance = sampleReflectance(λ, emission); 37 | const float sampledD65 = sampleTabulated(λ, 300, 831, dSpectrumD65); 38 | return sampledReflectance * sampledD65; 39 | } 40 | 41 | float3 toXYZ(const float λ, const float s) { 42 | const float samplesStart = 360; 43 | const float samplesEnd = 831; 44 | const float3 rgb = float3( 45 | sampleTabulated(λ, samplesStart, samplesEnd, dSpectrumCIEX), 46 | sampleTabulated(λ, samplesStart, samplesEnd, dSpectrumCIEY), 47 | sampleTabulated(λ, samplesStart, samplesEnd, dSpectrumCIEZ) 48 | ); 49 | return rgb * s / CIE1931YIntegral; 50 | } 51 | 52 | float3 toLinearSRGB(const float λ, const float s) { 53 | const float3 xyz = toXYZ(λ, s); 54 | const float3x3 XYZtoLinearSRGB = { 0.03276749869518854, -0.015543557073358668, -0.00504115364541362, -0.009800789737065342, 0.018969392573362078, 0.00042019608374440075, 0.0005626856849785213, -0.0020631808449212427, 0.010691028014591895 }; 55 | return mul(XYZtoLinearSRGB, xyz); 56 | } 57 | }; 58 | 59 | struct WavelengthSample { 60 | float λ; 61 | float pdf; 62 | 63 | static WavelengthSample sampleUniform(const float start, const float end, const float rand) { 64 | WavelengthSample s; 65 | s.λ = lerp(start, end, rand); 66 | s.pdf = 1 / (end - start); 67 | return s; 68 | } 69 | 70 | static WavelengthSample sampleVisible(const float rand) { 71 | WavelengthSample s; 72 | s.λ = 538 - 138.888889f * atanh(0.85691062f - 1.82750197f * rand); 73 | s.pdf = 0.0039398042f / pow(cosh(0.0072f * (s.λ - 538)), 2); 74 | return s; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/volume.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "medium.hlsl" 4 | #include "bsdf.hlsl" 5 | #include "phase_function.hlsl" 6 | 7 | struct Volume { 8 | Homogeneous medium; 9 | HenyeyGreenstein phase; 10 | float IOR; 11 | 12 | static Volume create(Homogeneous medium, HenyeyGreenstein phase, float IOR) { 13 | Volume v; 14 | v.medium = medium; 15 | v.phase = phase; 16 | v.IOR = IOR; 17 | return v; 18 | } 19 | 20 | static Volume transparent(float IOR) { 21 | return Volume::create(Homogeneous::none(), HenyeyGreenstein::create(0), IOR); 22 | } 23 | }; 24 | 25 | struct ChromaticVolume { 26 | ChromaticHomogeneous medium; 27 | HenyeyGreenstein phase; 28 | CauchyIOR IOR; 29 | 30 | Volume at(const float λ) { 31 | return Volume::create(medium.at(λ), phase, IOR.at(λ)); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/volume_tracker.hlsl: -------------------------------------------------------------------------------- 1 | #include "volume.hlsl" 2 | 3 | // probably does not satisfy the actual definition of an algebra as it's not closed 4 | // and there's not always an inverse 5 | namespace VolumeAlgebra { 6 | Volume add(Volume lhs, Volume rhs) { 7 | Volume o; 8 | o.medium.σ_s = lhs.medium.σ_s + rhs.medium.σ_s; 9 | o.medium.σ_a = lhs.medium.σ_a + rhs.medium.σ_a; 10 | o.phase.g = lhs.phase.g + rhs.phase.g; 11 | o.IOR = lhs.IOR * rhs.IOR; 12 | return o; 13 | } 14 | 15 | Volume sub(Volume lhs, Volume rhs) { 16 | Volume o; 17 | o.medium.σ_s = lhs.medium.σ_s - rhs.medium.σ_s; 18 | o.medium.σ_a = lhs.medium.σ_a - rhs.medium.σ_a; 19 | o.phase.g = lhs.phase.g - rhs.phase.g; 20 | o.IOR = lhs.IOR / rhs.IOR; 21 | return o; 22 | } 23 | 24 | Volume clampToValid(Volume v) { 25 | v.medium.σ_s = max(v.medium.σ_s, 0); 26 | v.medium.σ_a = max(v.medium.σ_a, 0); 27 | v.phase.g = clamp(v.phase.g, -0.9999999, 0.9999999); // TODO: handle delta phase functions 28 | return v; 29 | } 30 | }; 31 | 32 | struct VolumeTracker { 33 | static const uint VOLUME_PRIORITY_COUNT = 8; 34 | 35 | Volume current[VOLUME_PRIORITY_COUNT]; 36 | int depth[VOLUME_PRIORITY_COUNT - 1]; // last is always global, which is known to have depth == 0 37 | 38 | Volume other; 39 | bool inside; 40 | uint priority; 41 | 42 | // find volume stack containing the world-space position. expensive 43 | static VolumeTracker fromPosition(Scene scene, float3 position, float λ) { 44 | VolumeTracker t; 45 | for (uint i = 0; i < VOLUME_PRIORITY_COUNT - 1; i++) { 46 | t.depth[i] = 0; 47 | t.current[i + 1] = scene.globalVolume.at(λ); 48 | } 49 | t.current[0] = scene.globalVolume.at(λ); 50 | t.inside = true; // should be unused 51 | t.priority = 0; 52 | 53 | Ray ray = { position, float3(0, 1, 0) }; // direction arbitrary 54 | for (Intersection its = Intersection::find(scene.tlas, ray, 1.#INF, ~Instance::THIN_MASK); its.hit(); its = Intersection::find(scene.tlas, ray, 1.#INF, ~Instance::THIN_MASK)) { 55 | const SurfacePoint surface = scene.world.surfacePoint(its.instanceIndex, its.geometryIndex, its.primitiveIndex, its.barycentrics); 56 | const Volume volume = scene.world.material(its.instanceIndex, its.geometryIndex).volume.at(λ); 57 | const uint priority = scene.world.priority(its.instanceIndex); 58 | 59 | const bool entering = dot(ray.direction, surface.triangleFrame.n) < 0; 60 | if (entering) { 61 | // make sure we don't get something *slightly* above zero when we should've had zero 62 | // the more principled thing here is probably some ULP shenanigans 63 | t.current[priority] = VolumeAlgebra::sub(t.current[priority], VolumeAlgebra::add(volume, Volume::create(Homogeneous::create(volume.medium.σ_s * 0.0000001, volume.medium.σ_a * 0.0000001), HenyeyGreenstein::create(0), 1))); 64 | t.depth[priority - 1] -= 1; 65 | } else { 66 | t.current[priority] = VolumeAlgebra::add(t.current[priority], volume); 67 | t.depth[priority - 1] += 1; 68 | } 69 | ray.origin = surface.position + faceForward(surface.triangleFrame.n, ray.direction) * surface.spawnOffset; 70 | } 71 | 72 | t.other = t.current[t.activePriority()]; 73 | return t; 74 | } 75 | 76 | // surface should only be considered if this returns true 77 | bool newBoundary(bool newInside, uint newPriority, Volume newVolume) { 78 | inside = newInside; 79 | priority = newPriority; 80 | if (inside) { 81 | if (depth[priority - 1] > 1) { 82 | other = VolumeAlgebra::sub(current[priority], newVolume); 83 | } else { 84 | other = current[0]; 85 | for (uint i = priority - 1; i > 0; i--) { 86 | if (depth[i - 1] != 0) { 87 | other = current[i]; 88 | break; 89 | } 90 | } 91 | } 92 | } else { 93 | if (depth[priority - 1] > 0) { 94 | other = VolumeAlgebra::add(current[priority], newVolume); 95 | } else { 96 | other = newVolume; 97 | } 98 | } 99 | return activePriority() <= priority; 100 | } 101 | 102 | bool isIndexMatched() { 103 | return currentVolume().IOR == other.IOR; 104 | } 105 | 106 | Volume currentVolume() { 107 | // assume that negative numbers here are due to bad roundoff, and clamp 108 | return VolumeAlgebra::clampToValid(current[activePriority()]); 109 | } 110 | 111 | uint activePriority() { 112 | for (uint i = VOLUME_PRIORITY_COUNT - 1; i > 0; i--) { 113 | if (depth[i - 1] != 0) return i; 114 | } 115 | return 0; 116 | } 117 | 118 | Volume internal() { 119 | if (inside) { 120 | return currentVolume(); 121 | } else { 122 | return VolumeAlgebra::clampToValid(other); 123 | } 124 | } 125 | 126 | Volume external() { 127 | if (inside) { 128 | return VolumeAlgebra::clampToValid(other); 129 | } else { 130 | return currentVolume(); 131 | } 132 | } 133 | 134 | void cross() { 135 | current[priority] = other; 136 | depth[priority - 1] += inside ? -1 : 1; 137 | } 138 | }; -------------------------------------------------------------------------------- /src/lib/hrtsystem/shaders/world.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "reflection_frame.hlsl" 4 | #include "material.hlsl" 5 | 6 | struct Instance { // same required by vulkan on host side 7 | row_major float3x4 transform; 8 | uint instanceCustomIndex : 24; 9 | uint mask : 8; 10 | uint instanceShaderBindingTableRecordOffset : 24; 11 | uint flags : 8; 12 | uint64_t accelerationStructureReference; 13 | 14 | static const uint THIN_MASK = 0b10000000; 15 | 16 | bool thin() { 17 | return mask == THIN_MASK; 18 | } 19 | 20 | uint priority() { 21 | return firstbithigh(mask) + 1; 22 | } 23 | }; 24 | 25 | struct Geometry { 26 | uint meshIndex; 27 | uint materialIndex; 28 | uint64_t trianglePowersAddress; 29 | uint64_t trianglePowersSize; 30 | }; 31 | 32 | struct SurfacePoint { 33 | float3 position; 34 | float2 texcoord; 35 | 36 | Frame triangleFrame; // from triangle positions 37 | Frame frame; // from vertex attributes 38 | 39 | float spawnOffset; // minimum offset along normal that a ray will not intersect 40 | }; 41 | 42 | float3 loadPosition(uint64_t addr, uint index) { 43 | return vk::RawBufferLoad(addr + sizeof(float3) * index); 44 | } 45 | 46 | float2 loadTexcoord(uint64_t addr, uint index) { 47 | return vk::RawBufferLoad(addr + sizeof(float2) * index); 48 | } 49 | 50 | float3 loadNormal(uint64_t addr, uint index) { 51 | return vk::RawBufferLoad(addr + sizeof(float3) * index); 52 | } 53 | 54 | void getTangentBitangent(float3 positions[3], float2 texcoords[3], out float3 tangent, out float3 bitangent) { 55 | float2 deltaT10 = texcoords[1] - texcoords[0]; 56 | float2 deltaT20 = texcoords[2] - texcoords[0]; 57 | 58 | float3 deltaP10 = positions[1] - positions[0]; 59 | float3 deltaP20 = positions[2] - positions[0]; 60 | 61 | float det = deltaT10.x * deltaT20.y - deltaT10.y * deltaT20.x; 62 | if (det == 0.0) { 63 | coordinateSystem(normalize(cross(deltaP10, deltaP20)), tangent, bitangent); 64 | } else { 65 | tangent = normalize((deltaT20.y * deltaP10 - deltaT10.y * deltaP20) / det); 66 | bitangent = normalize((-deltaT20.x * deltaP10 + deltaT10.x * deltaP20) / det); 67 | } 68 | } 69 | 70 | template 71 | T interpolate(float3 barycentrics, T v[3]) { 72 | return barycentrics.x * v[0] + barycentrics.y * v[1] + barycentrics.z * v[2]; 73 | } 74 | 75 | struct TriangleLocalSpace { 76 | float3 positions[3]; 77 | float3 normals[3]; 78 | float2 texcoords[3]; 79 | 80 | // TODO: currently this does both barycentric evaluation and local to world 81 | // conversion. there's probably a way to decouple this into two functions 82 | SurfacePoint surfacePoint(const float2 attribs, const float3x4 toWorld, const float3x4 toLocal) { 83 | SurfacePoint surface; 84 | 85 | const float3 barycentrics = float3(1.0 - attribs.x - attribs.y, attribs.x, attribs.y); 86 | 87 | const float3 edge1 = positions[1] - positions[0]; 88 | const float3 edge2 = positions[2] - positions[0]; 89 | surface.position = positions[0] + ((attribs.x * edge1) + (attribs.y * edge2)); 90 | 91 | surface.texcoord = interpolate(barycentrics, texcoords); 92 | 93 | getTangentBitangent(positions, texcoords, surface.triangleFrame.s, surface.triangleFrame.t); 94 | surface.triangleFrame.n = normalize(cross(edge1, edge2)); 95 | surface.triangleFrame.reorthogonalize(); 96 | 97 | surface.frame = surface.triangleFrame; 98 | surface.frame.n = normalize(interpolate(barycentrics, normals)); 99 | surface.frame.reorthogonalize(); 100 | 101 | const float3 worldPosition = mul(toWorld, float4(surface.position, 1.0)); 102 | 103 | const float3x3 inverseTranspose = transpose((float3x3)toLocal); 104 | // https://developer.nvidia.com/blog/solving-self-intersection-artifacts-in-directx-raytracing/ 105 | { 106 | float3 wldNormal = mul(inverseTranspose, surface.triangleFrame.n); 107 | 108 | const float wldScale = rsqrt(dot(wldNormal, wldNormal)); 109 | wldNormal = mul(wldScale, wldNormal); 110 | 111 | // nvidia magic constants 112 | const float c0 = 5.9604644775390625E-8f; 113 | const float c1 = 1.788139769587360206060111522674560546875E-7f; 114 | const float c2 = 1.19209317972490680404007434844970703125E-7f; 115 | 116 | const float3 extent3 = abs(edge1) + abs(edge2) + abs(edge1 - edge2); 117 | const float extent = max(max(extent3.x, extent3.y), extent3.z); 118 | 119 | float3 objErr = c0 * abs(positions[0]) + mul(c1, extent); 120 | 121 | const float3 wldErr = c1 * mul(abs((float3x3)toWorld), abs(surface.position)) + mul(c2, abs(transpose(toWorld)[3])); 122 | 123 | objErr += c2 * mul(abs(toLocal), float4(abs(worldPosition), 1)); 124 | 125 | const float wldOffset = dot(wldErr, abs(wldNormal)); 126 | const float objOffset = dot(objErr, abs(surface.triangleFrame.n)); 127 | 128 | surface.spawnOffset = wldScale * objOffset + wldOffset; 129 | } 130 | 131 | // convert to world space 132 | { 133 | surface.position = worldPosition; 134 | 135 | surface.triangleFrame = surface.triangleFrame.inSpace(inverseTranspose); 136 | surface.frame = surface.frame.inSpace(inverseTranspose); 137 | } 138 | 139 | return surface; 140 | } 141 | 142 | float3 bivector() { 143 | return cross(positions[1] - positions[0], positions[2] - positions[0]); 144 | } 145 | 146 | float area(const float3x4 toLocal, const float3x4 toWorld) { 147 | const float3x3 inverseTranspose = transpose((float3x3)toLocal); 148 | const float3x3 cofactor = inverseTranspose * determinant((float3x3)toWorld); 149 | const float3 globalBivector = mul(cofactor, bivector()); 150 | return length(globalBivector) / 2.0; 151 | } 152 | }; 153 | 154 | struct Mesh { 155 | uint64_t positionAddress; 156 | uint64_t texcoordAddress; // may be zero, for no texcoords 157 | uint64_t normalAddress; // may be zero, for no vertex normals 158 | 159 | uint64_t indexAddress; // may be zero, for unindexed geometry 160 | 161 | uint64_t triangleCount; 162 | 163 | TriangleLocalSpace triangleLocalSpace(const uint primitiveIndex) { 164 | TriangleLocalSpace t; 165 | 166 | const uint3 ind = indexAddress != 0 ? vk::RawBufferLoad(indexAddress + sizeof(uint3) * primitiveIndex) : float3(primitiveIndex * 3 + 0, primitiveIndex * 3 + 1, primitiveIndex * 3 + 2); 167 | 168 | // positions always available 169 | t.positions[0] = loadPosition(positionAddress, ind.x); 170 | t.positions[1] = loadPosition(positionAddress, ind.y); 171 | t.positions[2] = loadPosition(positionAddress, ind.z); 172 | 173 | // texcoords optional 174 | if (texcoordAddress != 0) { 175 | t.texcoords[0] = loadTexcoord(texcoordAddress, ind.x); 176 | t.texcoords[1] = loadTexcoord(texcoordAddress, ind.y); 177 | t.texcoords[2] = loadTexcoord(texcoordAddress, ind.z); 178 | } else { 179 | // sane defaults for constant textures 180 | t.texcoords[0] = float2(0, 0); 181 | t.texcoords[1] = float2(1, 0); 182 | t.texcoords[2] = float2(1, 1); 183 | } 184 | 185 | // normals optional 186 | if (normalAddress != 0) { 187 | t.normals[0] = loadNormal(normalAddress, ind.x); 188 | t.normals[1] = loadNormal(normalAddress, ind.y); 189 | t.normals[2] = loadNormal(normalAddress, ind.z); 190 | } else { 191 | // use triangle normal 192 | const float3 normal = normalize(cross(t.positions[1] - t.positions[0], t.positions[2] - t.positions[0])); 193 | t.normals[0] = normal; 194 | t.normals[1] = normal; 195 | t.normals[2] = normal; 196 | } 197 | 198 | return t; 199 | } 200 | }; 201 | 202 | struct Model { 203 | uint geometryOffset; 204 | uint geometryCount; 205 | uint64_t geometryPowersAddress; 206 | uint64_t geometryPowersSize; 207 | }; 208 | 209 | struct World { 210 | StructuredBuffer instances; 211 | StructuredBuffer worldToInstance; 212 | 213 | StructuredBuffer models; 214 | StructuredBuffer meshes; 215 | StructuredBuffer geometries; 216 | 217 | StructuredBuffer materials; 218 | 219 | Mesh mesh(uint instanceIndex, uint geometryIndex) { 220 | const uint instanceID = instances[instanceIndex].instanceCustomIndex; 221 | const Geometry geometry = geometries[NonUniformResourceIndex(models[instanceID].geometryOffset + geometryIndex)]; 222 | return meshes[NonUniformResourceIndex(geometry.meshIndex)]; 223 | } 224 | 225 | Material material(uint instanceIndex, uint geometryIndex) { 226 | const uint instanceID = instances[instanceIndex].instanceCustomIndex; 227 | const Geometry geometry = geometries[NonUniformResourceIndex(models[instanceID].geometryOffset + geometryIndex)]; 228 | return materials[NonUniformResourceIndex(geometry.materialIndex)]; 229 | } 230 | 231 | bool thin(uint instanceIndex) { 232 | return instances[instanceIndex].thin(); 233 | } 234 | 235 | uint priority(uint instanceIndex) { 236 | return instances[instanceIndex].priority(); 237 | } 238 | 239 | float triangleArea(uint instanceIndex, uint geometryIndex, uint primitiveIndex) { 240 | return triangleLocalSpace(instanceIndex, geometryIndex, primitiveIndex).area(toLocal(instanceIndex), toWorld(instanceIndex)); 241 | } 242 | 243 | TriangleLocalSpace triangleLocalSpace(uint instanceIndex, uint geometryIndex, uint primitiveIndex) { 244 | return this.mesh(instanceIndex, geometryIndex).triangleLocalSpace(primitiveIndex); 245 | } 246 | 247 | float3x4 toWorld(uint instanceIndex) { 248 | return instances[NonUniformResourceIndex(instanceIndex)].transform; 249 | } 250 | 251 | float3x4 toLocal(uint instanceIndex) { 252 | return worldToInstance[NonUniformResourceIndex(instanceIndex)]; 253 | } 254 | 255 | SurfacePoint surfacePoint(uint instanceIndex, uint geometryIndex, uint primitiveIndex, float2 attribs) { 256 | return triangleLocalSpace(instanceIndex, geometryIndex, primitiveIndex).surfacePoint(attribs, toWorld(instanceIndex), toLocal(instanceIndex)); 257 | } 258 | }; 259 | -------------------------------------------------------------------------------- /src/lib/shaders/utils/helpers.hlsl: -------------------------------------------------------------------------------- 1 | template 2 | uint2 textureDimensions(Texture2D texture) { 3 | uint2 dimensions; 4 | texture.GetDimensions(dimensions.x, dimensions.y); 5 | return dimensions; 6 | } 7 | 8 | template 9 | uint2 textureDimensions(RWTexture2D texture) { 10 | uint2 dimensions; 11 | texture.GetDimensions(dimensions.x, dimensions.y); 12 | return dimensions; 13 | } 14 | 15 | template 16 | uint textureDimensions(Texture1D texture) { 17 | uint dimension; 18 | texture.GetDimensions(dimension); 19 | return dimension; 20 | } 21 | 22 | template 23 | uint textureDimensions(RWTexture1D texture) { 24 | uint dimension; 25 | texture.GetDimensions(dimension); 26 | return dimension; 27 | } 28 | 29 | template 30 | uint bufferDimensions(StructuredBuffer buffer) { 31 | uint size; 32 | uint stride; 33 | buffer.GetDimensions(size, stride); 34 | return size; 35 | } 36 | 37 | template 38 | uint bufferDimensions(RWStructuredBuffer buffer) { 39 | uint size; 40 | uint stride; 41 | buffer.GetDimensions(size, stride); 42 | return size; 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/shaders/utils/mappings.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "math.hlsl" 4 | 5 | // https://eheitzresearch.wordpress.com/749-2/ 6 | float2 squareToTriangle(float2 square) { 7 | if (square.x < square.y) { 8 | return float2(square.x / 2, square.y - (square.x / 2)); 9 | } else { 10 | return float2(square.x - (square.y / 2), square.y / 2); 11 | } 12 | } 13 | 14 | float2 squareToGaussian(float2 square) { 15 | const float u1 = 1.0 - square.x; 16 | const float u2 = square.y; 17 | const float r = sqrt(-2.0 * log(u1)); 18 | const float theta = 2 * PI * u2; 19 | return r * float2(cos(theta), sin(theta)); 20 | } 21 | 22 | float2 squareToUniformDiskConcentric(float2 square) { 23 | float2 uOffset = 2.0 * square - float2(1.0, 1.0); 24 | 25 | if (all(uOffset == float2(0.0, 0.0))) { 26 | return float2(0.0, 0.0); 27 | } 28 | 29 | float theta, r; 30 | 31 | if (abs(uOffset.x) > abs(uOffset.y)) { 32 | r = uOffset.x; 33 | theta = (PI / 4) * (uOffset.y / uOffset.x); 34 | } else { 35 | r = uOffset.y; 36 | theta = (PI / 2) - (PI / 4) * (uOffset.x / uOffset.y); 37 | } 38 | 39 | return r * float2(cos(theta), sin(theta)); 40 | } 41 | 42 | float3 squareToCosineHemisphere(float2 square) { 43 | float2 d = squareToUniformDiskConcentric(square); 44 | float z = sqrt(max(0.0, 1.0 - dot(d, d))); 45 | 46 | return float3(d.x, d.y, z); 47 | } 48 | 49 | float3 squareToUniformHemisphere(float2 square) { 50 | float z = square.x; 51 | float r = sqrt(max(0.0, 1.0 - z * z)); 52 | float phi = 2 * PI * square.y; 53 | return float3(r * cos(phi), r * sin(phi), z); 54 | } 55 | 56 | float3 squareToUniformSphere(float2 square) { 57 | float z = 1 - 2 * square.x; 58 | float r = sqrt(1 - z * z); 59 | float phi = 2 * PI * square.y; 60 | return float3(r * float2(cos(phi), sin(phi)), z); 61 | } 62 | 63 | float3 sphericalToCartesian(float sinTheta, float cosTheta, float phi) { 64 | return float3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta); 65 | } 66 | 67 | // (phi, theta) -- ([0, 2pi], [0, pi]) 68 | // assumes vector normalized 69 | float2 cartesianToSpherical(float3 v) { 70 | float p = atan2(v.y, v.x); 71 | float phi = (p < 0) ? (p + 2 * PI) : p; 72 | float theta = acos(v.z); 73 | return float2(phi, theta); 74 | } 75 | 76 | // from PBRTv4 3.8.3 "Equal-Area Mapping" 77 | float3 squareToEqualAreaSphere(float2 square) { 78 | const float2 uv = 2.0 * square - float2(1.0, 1.0); 79 | const float2 uvp = abs(uv); 80 | 81 | const float signedDistance = 1.0 - (uvp.x + uvp.y); 82 | const float d = abs(signedDistance); 83 | const float r = 1.0 - d; 84 | 85 | const float phi = (r == 0.0 ? 1.0 : (uvp.y - uvp.x) / r + 1.0) * PI / 4.0; 86 | const float3 signs = sign(float3(uv.x, uv.y, signedDistance)); 87 | 88 | return signs * float3( 89 | cos(phi) * r * sqrt(2.0 - r * r), 90 | sin(phi) * r * sqrt(2.0 - r * r), 91 | 1.0f - r * r 92 | ); 93 | } 94 | 95 | float2 squareToEqualAreaSphereInverse(float3 dir) { 96 | const float3 xyz = abs(dir); 97 | const float r = sqrt(1.0 - xyz.z); 98 | 99 | float phi = all(xyz.xy == float2(0.0, 0.0)) ? 0.0 : atan2(min(xyz.x, xyz.y), max(xyz.x, xyz.y)) * 2.0 / PI; 100 | if (xyz.x < xyz.y) phi = 1.0 - phi; 101 | 102 | float2 uv = float2(r - phi * r, phi * r); 103 | 104 | if (dir.z < 0) uv = float2(1.0, 1.0) - float2(uv.y, uv.x); 105 | 106 | uv *= sign(dir.xy); 107 | 108 | return (uv + float2(1.0, 1.0)) / 2.0; 109 | } 110 | 111 | // selects true with probability p (false otherwise), 112 | // remapping rand back into (0..1) 113 | bool coinFlipRemap(float p, inout float rand) { 114 | if (rand < p) { 115 | rand /= p; 116 | return true; 117 | } else { 118 | rand = (rand - p) / (1.0 - p); 119 | return false; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/lib/shaders/utils/math.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | static const float PI = 3.14159265; 4 | static const float EPSILON = 0.000000119; 5 | static const uint MAX_UINT = 0xFFFFFFFF; 6 | 7 | float3 vectorToColor(float3 v) { 8 | return (v + 1.0) / 2.0; 9 | } 10 | 11 | float atanh(float x) { 12 | return log((1 + x) / (1 - x)) / 2; 13 | } 14 | 15 | float luminance(float3 color) { 16 | return 0.2126 * color.r + 17 | 0.7152 * color.g + 18 | 0.0722 * color.b; 19 | } 20 | 21 | float3 faceForward(float3 n, float3 d) { 22 | return dot(n, d) > 0 ? n : -n; 23 | } 24 | 25 | float2 faceForward(float2 n, float2 d) { 26 | return dot(n, d) > 0 ? n : -n; 27 | } 28 | 29 | // https://www.nu42.com/2015/03/how-you-average-numbers.html 30 | float3 accumulate(const float3 priorAverage, const float3 newSample, const uint sampleCount) { 31 | return priorAverage + (newSample - priorAverage) / (sampleCount + 1); 32 | } 33 | 34 | void coordinateSystem(float3 v1, out float3 v2, out float3 v3) { 35 | if (abs(v1.x) > abs(v1.y)) { 36 | v2 = float3(-v1.z, 0.0, v1.x) / sqrt(v1.x * v1.x + v1.z * v1.z); 37 | } else { 38 | v2 = float3(0.0, v1.z, -v1.y) / sqrt(v1.y * v1.y + v1.z * v1.z); 39 | } 40 | 41 | v3 = cross(v2, v1); 42 | } 43 | 44 | uint log2Int(const uint x) { 45 | return firstbithigh(x); 46 | } 47 | 48 | uint log2IntCeil(const uint x) { 49 | if (x == 1) return 0; 50 | return log2Int(x - 1) + 1; 51 | } 52 | 53 | float normL1(const float3 v) { 54 | return abs(v.x) + abs(v.y) + abs(v.z); 55 | } 56 | 57 | float normL2(const float3 v) { 58 | return length(v); 59 | } 60 | 61 | float normLInf(const float3 v) { 62 | return max(max(abs(v.x), abs(v.y)), abs(v.z)); 63 | } 64 | 65 | // TODO: switch to this when figure it why it doesn't work 66 | // void coordinateSystem(float3 v1, out float3 v2, out float3 v3) { 67 | // float sign = v1.z >= 0.0f ? 1.0f : -1.0f; 68 | // float a = -1 / (sign + v1.z); 69 | // float b = v1.x * v1.y * a; 70 | // v2 = float3(1 + sign * sqrt(v1.x) * a, sign * b, -sign * v1.x); 71 | // v3 = float3(b, sign + sqrt(v1.y) * a, -v1.y); 72 | // } 73 | -------------------------------------------------------------------------------- /src/lib/shaders/utils/random.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "math.hlsl" 4 | 5 | // https://www.reedbeta.com/blog/hash-functions-for-gpu-rendering/ 6 | // https://jcgt.org/published/0009/03/02/ 7 | namespace Hash { 8 | uint lcg(uint a) { 9 | const uint multiplier = 747796405u; 10 | const uint increment = 2891336453u; 11 | return a * multiplier + increment; 12 | } 13 | 14 | // RXS-M-XS PCG permutation function 15 | uint rxs_m_xs(uint a) { 16 | const uint b = ((a >> ((a >> 28u) + 4u)) ^ a) * 277803737u; 17 | return (b >> 22u) ^ b; 18 | } 19 | 20 | uint pcg(uint a) { 21 | return rxs_m_xs(lcg(a)); 22 | } 23 | } 24 | 25 | struct Rng { 26 | uint state; 27 | 28 | static Rng fromSeed(uint3 seed) { 29 | Rng rng; 30 | rng.state = Hash::pcg(seed.x + Hash::pcg(seed.y + Hash::pcg(seed.z))); 31 | return rng; 32 | } 33 | 34 | void stepState() { 35 | state = Hash::lcg(state); 36 | } 37 | 38 | float getFloat() { 39 | stepState(); 40 | 41 | uint hashed_uint = Hash::rxs_m_xs(state); 42 | 43 | // convert to float [0-1) 44 | // https://pharr.org/matt/blog/2022/03/05/sampling-fp-unit-interval 45 | return float(hashed_uint >> 8) * 0x1p-24f; 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/lib/shaders/utils/reservoir.hlsl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "mappings.hlsl" 4 | 5 | template 6 | struct Reservoir { 7 | Candidate selected; 8 | float weightSum; 9 | 10 | static Reservoir empty() { 11 | Reservoir r; 12 | r.weightSum = 0.0; 13 | return r; 14 | } 15 | 16 | void update(const Candidate newCandidate, const float newWeight, inout float rand) { 17 | weightSum += newWeight; 18 | if (coinFlipRemap(newWeight / weightSum, rand)) { 19 | selected = newCandidate; 20 | } 21 | } 22 | 23 | // not valid to look at selected unless this returns true 24 | bool valid() { 25 | return weightSum != 0; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/lib/test_runner.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | const engine = @import("engine"); 5 | 6 | pub fn main() void { 7 | const enable_print = true; 8 | const print_all = true; 9 | 10 | var passed: u64 = 0; 11 | var skipped: u64 = 0; 12 | var failed: u64 = 0; 13 | const stderr = if (enable_print) std.io.getStdErr() else {}; 14 | for (builtin.test_functions) |test_fn| { 15 | if (enable_print and print_all) { 16 | stderr.writeAll(test_fn.name) catch {}; 17 | stderr.writeAll("... ") catch {}; 18 | } 19 | test_fn.func() catch |err| { 20 | if (enable_print and !print_all) { 21 | stderr.writeAll(test_fn.name) catch {}; 22 | stderr.writeAll("... ") catch {}; 23 | } 24 | if (err != error.SkipZigTest) { 25 | if (enable_print) stderr.writer().print("FAIL {}\n", .{ err }) catch {}; 26 | failed += 1; 27 | if (!enable_print) return err; 28 | continue; 29 | } 30 | if (enable_print) stderr.writeAll("SKIP\n") catch {}; 31 | skipped += 1; 32 | continue; 33 | }; 34 | if (enable_print and print_all) stderr.writeAll("PASS\n") catch {}; 35 | passed += 1; 36 | } 37 | if (enable_print) { 38 | stderr.writer().print("{} passed, {} skipped, {} failed\n", .{ passed, skipped, failed }) catch {}; 39 | if (failed != 0) std.process.exit(1); 40 | } 41 | } 42 | 43 | --------------------------------------------------------------------------------