GTAO demo mod

This commit is contained in:
Luke Street
2026-07-10 00:40:18 -06:00
parent d9a978f21f
commit 75a249f3e1
20 changed files with 1851 additions and 12 deletions
+2 -1
View File
@@ -559,7 +559,8 @@ include(cmake/ModSDK.cmake)
if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
add_custom_target(dusklight_mods) # Aggregate target for all in-tree mods
add_subdirectory(tools/mod_template)
add_subdirectory(mods/template_mod)
add_subdirectory(mods/ao_mod)
endif ()
if (APPLE)
+5
View File
@@ -25,3 +25,8 @@ set(_game_include_dirs
add_library(dusklight_game_headers INTERFACE)
target_include_directories(dusklight_game_headers INTERFACE ${_game_include_dirs})
target_compile_definitions(dusklight_game_headers INTERFACE ${_game_compile_defs})
if (TARGET dawn::dawncpp_headers)
target_link_libraries(dusklight_game_headers INTERFACE dawn::dawncpp_headers)
elseif (TARGET dawn::webgpu_dawn)
target_link_libraries(dusklight_game_headers INTERFACE dawn::webgpu_dawn)
endif ()
+1 -1
View File
@@ -28,7 +28,7 @@ function, read and write data fields, and hook the vast majority of game functio
## Getting Started
Fork the [mod template](../tools/mod_template/), a self-contained CMake project that uses the Dusklight mod SDK.
Fork the [mod template](../mods/template_mod/), a self-contained CMake project that uses the Dusklight mod SDK.
```
my_mod/
+1 -1
+20
View File
@@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 3.25)
project(ao_mod CXX)
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
option(DUSK_MOD_USE_FULL_TREE "Use full build instead of the minimal mod SDK" OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
if (DUSK_MOD_USE_FULL_TREE)
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
else ()
add_subdirectory("${DUSK_DIR}/sdk" dusk-sdk EXCLUDE_FROM_ALL)
endif ()
endif ()
add_mod(ao_mod
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res
BUNDLE
)
+7
View File
@@ -0,0 +1,7 @@
{
"id": "dev.twilitrealm.ao_mod",
"name": "[Demo] Ambient Occlusion",
"version": "1.0.0",
"author": "Twilit Realm",
"description": "Ground-truth ambient occlusion (GTAO) computed from the scene depth buffer and composited over the game. Ported from Bevy Engine's SSAO and Intel XeGTAO."
}
+161
View File
@@ -0,0 +1,161 @@
// Fullscreen composite: multiplies the denoised ambient-occlusion visibility over the scene.
//
// Debug views:
// 1 = raw AO visibility as grayscale
// 2 = view-space normals reconstructed from depth (keep in sync with gtao.wgsl)
// 3 = the preprocessed depth input
// 4 = depth staircase detector
struct Uniforms {
projection: mat4x4f,
inverse_projection: mat4x4f,
size: vec2f, // AO texture size in pixels (may be half the render size)
inv_size: vec2f,
depth_scale: vec2f,
effect_radius: f32,
intensity: f32,
slice_count: f32,
samples_per_slice_side: f32,
debug_view: u32,
_pad: f32,
}
@group(0) @binding(0) var ambient_occlusion: texture_2d<f32>;
@group(0) @binding(1) var preprocessed_depth: texture_2d<f32>;
@group(0) @binding(2) var scene_depth_raw: texture_2d<f32>;
@group(0) @binding(3) var<uniform> uniforms: Uniforms;
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) uv: vec2f,
}
@vertex
fn vs_main(@builtin(vertex_index) index: u32) -> VertexOutput {
// Fullscreen triangle
var out: VertexOutput;
let uv = vec2f(f32((index << 1u) & 2u), f32(index & 2u));
out.position = vec4f(uv * vec2f(2.0, -2.0) + vec2f(-1.0, 1.0), 0.0, 1.0);
out.uv = uv;
return out;
}
// Manual bilinear sample (r32float is unfilterable without optional device features)
fn sample_visibility(uv: vec2f) -> f32 {
let coordinates = uv * uniforms.size - 0.5;
let base = floor(coordinates);
let fraction = coordinates - base;
let max_coordinates = vec2i(uniforms.size) - 1i;
let p00 = clamp(vec2i(base), vec2i(0i), max_coordinates);
let p11 = clamp(vec2i(base) + 1i, vec2i(0i), max_coordinates);
let v00 = textureLoad(ambient_occlusion, vec2i(p00.x, p00.y), 0i).r;
let v10 = textureLoad(ambient_occlusion, vec2i(p11.x, p00.y), 0i).r;
let v01 = textureLoad(ambient_occlusion, vec2i(p00.x, p11.y), 0i).r;
let v11 = textureLoad(ambient_occlusion, vec2i(p11.x, p11.y), 0i).r;
let top = mix(v00, v10, fraction.x);
let bottom = mix(v01, v11, fraction.x);
return mix(top, bottom, fraction.y);
}
fn load_depth(pixel_coordinates: vec2<i32>) -> f32 {
let coordinates = clamp(pixel_coordinates, vec2<i32>(0i), vec2<i32>(uniforms.size) - 1i);
return textureLoad(preprocessed_depth, coordinates, 0i).r;
}
fn reconstruct_view_space_position(depth: f32, uv: vec2f) -> vec3f {
let clip_xy = vec2f(uv.x * 2.0 - 1.0, 1.0 - 2.0 * uv.y);
let t = uniforms.inverse_projection * vec4f(clip_xy, depth, 1.0);
return t.xyz / t.w;
}
fn view_position_at(pixel_coordinates: vec2<i32>) -> vec3f {
let depth = load_depth(pixel_coordinates);
let uv = (vec2f(pixel_coordinates) + 0.5) * uniforms.inv_size;
return reconstruct_view_space_position(depth, uv);
}
fn reconstruct_normal(pixel_coordinates: vec2<i32>, pixel_position: vec3f, depth_center: f32) -> vec3f {
let depth_left1 = load_depth(pixel_coordinates + vec2<i32>(-1i, 0i));
let depth_left2 = load_depth(pixel_coordinates + vec2<i32>(-2i, 0i));
let depth_right1 = load_depth(pixel_coordinates + vec2<i32>(1i, 0i));
let depth_right2 = load_depth(pixel_coordinates + vec2<i32>(2i, 0i));
let depth_top1 = load_depth(pixel_coordinates + vec2<i32>(0i, -1i));
let depth_top2 = load_depth(pixel_coordinates + vec2<i32>(0i, -2i));
let depth_bottom1 = load_depth(pixel_coordinates + vec2<i32>(0i, 1i));
let depth_bottom2 = load_depth(pixel_coordinates + vec2<i32>(0i, 2i));
let use_left = abs(2.0 * depth_left1 - depth_left2 - depth_center) <
abs(2.0 * depth_right1 - depth_right2 - depth_center);
let use_top = abs(2.0 * depth_top1 - depth_top2 - depth_center) <
abs(2.0 * depth_bottom1 - depth_bottom2 - depth_center);
var ddx: vec3f;
if use_left {
ddx = pixel_position - view_position_at(pixel_coordinates + vec2<i32>(-1i, 0i));
} else {
ddx = view_position_at(pixel_coordinates + vec2<i32>(1i, 0i)) - pixel_position;
}
var ddy: vec3f;
if use_top {
ddy = pixel_position - view_position_at(pixel_coordinates + vec2<i32>(0i, -1i));
} else {
ddy = view_position_at(pixel_coordinates + vec2<i32>(0i, 1i)) - pixel_position;
}
var normal = normalize(cross(ddy, ddx));
if dot(normal, pixel_position) > 0.0 {
normal = -normal;
}
return normal;
}
// Raw-snapshot variant of load_depth for the staircase view
fn load_raw_depth(pixel_coordinates: vec2<i32>) -> f32 {
let size = vec2<i32>(textureDimensions(scene_depth_raw));
let coordinates = clamp(pixel_coordinates, vec2<i32>(0i), size - 1i);
return textureLoad(scene_depth_raw, coordinates, 0i).r;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
if uniforms.debug_view == 2u {
// Reconstructed view-space normals, [-1,1] -> RGB
let pixel = vec2<i32>(in.uv * uniforms.size);
let depth = load_depth(pixel);
let uv = (vec2f(pixel) + 0.5) * uniforms.inv_size;
let position = reconstruct_view_space_position(depth, uv);
let normal = reconstruct_normal(pixel, position, depth);
return vec4f(normal * 0.5 + 0.5, 1.0);
}
if uniforms.debug_view == 3u {
// Preprocessed depth as an exponential distance gradient (white = near, black = far)
let pixel = vec2<i32>(in.uv * uniforms.size);
let position = view_position_at(pixel);
let value = exp(-max(-position.z, 0.0) * 0.0003);
return vec4f(value, value, value, 1.0);
}
if uniforms.debug_view == 4u {
// Staircase detector on the raw snapshot depth
let size = vec2f(textureDimensions(scene_depth_raw));
let pixel = vec2<i32>(in.uv * size);
let d_center = load_raw_depth(pixel);
let d_left = load_raw_depth(pixel + vec2<i32>(-1i, 0i));
let d_right = load_raw_depth(pixel + vec2<i32>(1i, 0i));
let d_top = load_raw_depth(pixel + vec2<i32>(0i, -1i));
let d_bottom = load_raw_depth(pixel + vec2<i32>(0i, 1i));
let gradient_x = abs(d_right - d_left) * 0.5;
let curvature_x = abs(d_right - 2.0 * d_center + d_left);
let gradient_y = abs(d_bottom - d_top) * 0.5;
let curvature_y = abs(d_bottom - 2.0 * d_center + d_top);
let ratio_x = curvature_x / max(gradient_x, 1e-12);
let ratio_y = curvature_y / max(gradient_y, 1e-12);
return vec4f(saturate(ratio_x), saturate(ratio_y), 0.0, 1.0);
}
let visibility = sample_visibility(in.uv);
if uniforms.debug_view == 1u {
return vec4f(visibility, visibility, visibility, 1.0);
}
let value = mix(1.0, visibility, uniforms.intensity);
return vec4f(value, value, value, 1.0);
}
+108
View File
@@ -0,0 +1,108 @@
// 3x3 bilaterial filter (edge-preserving blur)
// https://people.csail.mit.edu/sparis/bf_course/course_notes.pdf
//
// Note: Does not use the Gaussian kernel part of a typical bilateral blur
// From the paper: "use the information gathered on a neighborhood of 4 x 4 using a bilateral filter for
// reconstruction, using _uniform_ convolution weights"
//
// Note: The paper does a 4x4 (not quite centered) filter, offset by +/- 1 pixel every other frame
// XeGTAO does a 3x3 filter, on two pixels at a time per compute thread, applied twice
// We do a 3x3 filter, on 1 pixel per compute thread, applied once
//
// Ported from Bevy Engine, crates/bevy_pbr/src/ssao/spatial_denoise.wgsl (v0.13.2), licensed
// MIT OR Apache-2.0 (see res/licenses/), itself derived from Intel XeGTAO (MIT).
//
// PORT: the textureGather calls are rewritten as explicit per-neighbor textureLoads (r32float
// and r32uint are unfilterable); Bevy view uniforms -> the mod's uniform block; r16float -> r32float.
struct Uniforms {
projection: mat4x4f,
inverse_projection: mat4x4f,
size: vec2f,
inv_size: vec2f,
depth_scale: vec2f,
effect_radius: f32,
intensity: f32,
slice_count: f32,
samples_per_slice_side: f32,
debug_view: u32,
_pad: f32,
}
@group(0) @binding(0) var ambient_occlusion_noisy: texture_2d<f32>;
@group(0) @binding(1) var depth_differences: texture_2d<u32>;
@group(0) @binding(2) var ambient_occlusion: texture_storage_2d<r32float, write>;
@group(0) @binding(3) var<uniform> uniforms: Uniforms;
fn clamp_coordinates(pixel_coordinates: vec2<i32>) -> vec2<i32> {
return clamp(pixel_coordinates, vec2<i32>(0i), vec2<i32>(uniforms.size) - 1i);
}
// Each pixel's packed edge info is (left, right, top, bottom) weights, packed by the GTAO pass.
fn load_edges(pixel_coordinates: vec2<i32>) -> vec4<f32> {
return unpack4x8unorm(textureLoad(depth_differences, clamp_coordinates(pixel_coordinates), 0i).r);
}
fn load_visibility(pixel_coordinates: vec2<i32>) -> f32 {
return textureLoad(ambient_occlusion_noisy, clamp_coordinates(pixel_coordinates), 0i).r;
}
@compute
@workgroup_size(8, 8, 1)
fn spatial_denoise(@builtin(global_invocation_id) global_id: vec3<u32>) {
let pixel_coordinates = vec2<i32>(global_id.xy);
let left_edges = load_edges(pixel_coordinates + vec2<i32>(-1i, 0i));
let right_edges = load_edges(pixel_coordinates + vec2<i32>(1i, 0i));
let top_edges = load_edges(pixel_coordinates + vec2<i32>(0i, -1i));
let bottom_edges = load_edges(pixel_coordinates + vec2<i32>(0i, 1i));
var center_edges = load_edges(pixel_coordinates);
// Cross-check each edge against the neighbor's opposing edge weight.
center_edges *= vec4<f32>(left_edges.y, right_edges.x, top_edges.w, bottom_edges.z);
let center_weight = 1.2;
let left_weight = center_edges.x;
let right_weight = center_edges.y;
let top_weight = center_edges.z;
let bottom_weight = center_edges.w;
let top_left_weight = 0.425 * (top_weight * top_edges.x + left_weight * left_edges.z);
let top_right_weight = 0.425 * (top_weight * top_edges.y + right_weight * right_edges.z);
let bottom_left_weight = 0.425 * (bottom_weight * bottom_edges.x + left_weight * left_edges.w);
let bottom_right_weight = 0.425 * (bottom_weight * bottom_edges.y + right_weight * right_edges.w);
let center_visibility = load_visibility(pixel_coordinates);
let left_visibility = load_visibility(pixel_coordinates + vec2<i32>(-1i, 0i));
let right_visibility = load_visibility(pixel_coordinates + vec2<i32>(1i, 0i));
let top_visibility = load_visibility(pixel_coordinates + vec2<i32>(0i, -1i));
let bottom_visibility = load_visibility(pixel_coordinates + vec2<i32>(0i, 1i));
let top_left_visibility = load_visibility(pixel_coordinates + vec2<i32>(-1i, -1i));
let top_right_visibility = load_visibility(pixel_coordinates + vec2<i32>(1i, -1i));
let bottom_left_visibility = load_visibility(pixel_coordinates + vec2<i32>(-1i, 1i));
let bottom_right_visibility = load_visibility(pixel_coordinates + vec2<i32>(1i, 1i));
// PORT: Bevy sums the center sample unweighted while still counting center_weight in the
// denominator; XeGTAO's original weights the value too, which is what we do here.
var sum = center_visibility * center_weight;
sum += left_visibility * left_weight;
sum += right_visibility * right_weight;
sum += top_visibility * top_weight;
sum += bottom_visibility * bottom_weight;
sum += top_left_visibility * top_left_weight;
sum += top_right_visibility * top_right_weight;
sum += bottom_left_visibility * bottom_left_weight;
sum += bottom_right_visibility * bottom_right_weight;
var sum_weight = center_weight;
sum_weight += left_weight;
sum_weight += right_weight;
sum_weight += top_weight;
sum_weight += bottom_weight;
sum_weight += top_left_weight;
sum_weight += top_right_weight;
sum_weight += bottom_left_weight;
sum_weight += bottom_right_weight;
let denoised_visibility = sum / sum_weight;
textureStore(ambient_occlusion, pixel_coordinates, vec4<f32>(denoised_visibility, 0.0, 0.0, 0.0));
}
+247
View File
@@ -0,0 +1,247 @@
// Ground Truth-based Ambient Occlusion (GTAO)
// Paper: https://www.activision.com/cdn/research/Practical_Real_Time_Strategies_for_Accurate_Indirect_Occlusion_NEW%20VERSION_COLOR.pdf
// Presentation: https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf
//
// Ported from Bevy Engine, crates/bevy_pbr/src/ssao/gtao.wgsl (v0.13.2), licensed
// MIT OR Apache-2.0 (see res/licenses/), itself heavily based on XeGTAO v1.30 from Intel (MIT):
// https://github.com/GameTechDev/XeGTAO/blob/0d177ce06bfa642f64d8af4de1197ad1bcb862d4/Source/Rendering/Shaders/XeGTAO.hlsli
//
// PORT:
// - Bevy view/globals bindings -> the mod's own uniform block (matrices from Dusklight's
// CameraService, WebGPU clip convention, reversed-Z - the same convention Bevy uses).
// - Prepass normals -> normals reconstructed from depth (atyuwen's accurate 5-tap method,
// https://atyuwen.github.io/posts/normal-reconstruction/).
// - Sampler-based reads -> textureLoad (r32float is unfilterable without optional features);
// the mip level for the XeGTAO bandwidth optimization is selected explicitly per load.
// - effect_radius and slice/sample counts come from uniforms instead of constants/shader defs
// (game world units are ~100x larger than Bevy's meters, and quality is a live setting).
// - No TEMPORAL_JITTER: the noise index is pinned (no TAA; the spatial denoiser is the only
// filter, a configuration XeGTAO supports).
// - Storage format r16float -> r32float (core WebGPU storage format).
struct Uniforms {
projection: mat4x4f,
inverse_projection: mat4x4f,
size: vec2f,
inv_size: vec2f,
depth_scale: vec2f,
effect_radius: f32,
intensity: f32,
slice_count: f32,
samples_per_slice_side: f32,
debug_view: u32,
_pad: f32,
}
@group(0) @binding(0) var preprocessed_depth: texture_2d<f32>;
@group(0) @binding(1) var hilbert_index_lut: texture_2d<u32>;
@group(0) @binding(2) var ambient_occlusion: texture_storage_2d<r32float, write>;
@group(0) @binding(3) var depth_differences: texture_storage_2d<r32uint, write>;
@group(0) @binding(4) var<uniform> uniforms: Uniforms;
const PI: f32 = 3.141592653589793;
const HALF_PI: f32 = 1.5707963267948966;
fn fast_sqrt(x: f32) -> f32 {
return bitcast<f32>(0x1fbd1df5 + (bitcast<i32>(x) >> 1u));
}
fn fast_acos(in_x: f32) -> f32 {
let x = abs(in_x);
var res = -0.156583 * x + HALF_PI;
res *= fast_sqrt(1.0 - x);
return select(PI - res, res, in_x >= 0.0);
}
fn load_noise(pixel_coordinates: vec2<i32>) -> vec2<f32> {
let index = textureLoad(hilbert_index_lut, pixel_coordinates % 64, 0).r;
// R2 sequence - http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences
return fract(0.5 + f32(index) * vec2<f32>(0.75487766624669276005, 0.5698402909980532659114));
}
fn load_depth(pixel_coordinates: vec2<i32>, mip_level: i32) -> f32 {
let mip_size = max(vec2<i32>(uniforms.size) >> vec2<u32>(u32(mip_level)), vec2<i32>(1i));
let coordinates = clamp(pixel_coordinates, vec2<i32>(0i), mip_size - 1i);
return textureLoad(preprocessed_depth, coordinates, mip_level).r;
}
// Calculate differences in depth between neighbor pixels (later used by the spatial denoiser pass to preserve object edges)
fn calculate_neighboring_depth_differences(pixel_coordinates: vec2<i32>) -> f32 {
// Sample the pixel's depth and 4 depths around it
// PORT: explicit loads instead of two textureGathers.
let depth_center = load_depth(pixel_coordinates, 0i);
let depth_left = load_depth(pixel_coordinates + vec2<i32>(-1i, 0i), 0i);
let depth_top = load_depth(pixel_coordinates + vec2<i32>(0i, -1i), 0i);
let depth_bottom = load_depth(pixel_coordinates + vec2<i32>(0i, 1i), 0i);
let depth_right = load_depth(pixel_coordinates + vec2<i32>(1i, 0i), 0i);
// Calculate the depth differences (large differences represent object edges)
var edge_info = vec4<f32>(depth_left, depth_right, depth_top, depth_bottom) - depth_center;
let slope_left_right = (edge_info.y - edge_info.x) * 0.5;
let slope_top_bottom = (edge_info.w - edge_info.z) * 0.5;
let edge_info_slope_adjusted = edge_info + vec4<f32>(slope_left_right, -slope_left_right, slope_top_bottom, -slope_top_bottom);
edge_info = min(abs(edge_info), abs(edge_info_slope_adjusted));
let bias = 0.25; // Using the bias and then saturating nudges the values a bit
let scale = depth_center * 0.011; // Weight the edges by their distance from the camera
edge_info = saturate((1.0 + bias) - edge_info / scale); // Apply the bias and scale, and invert edge_info so that small values become large, and vice versa
// Pack the edge info into the texture
let edge_info_packed = vec4<u32>(pack4x8unorm(edge_info), 0u, 0u, 0u);
textureStore(depth_differences, pixel_coordinates, edge_info_packed);
return depth_center;
}
fn reconstruct_view_space_position(depth: f32, uv: vec2<f32>) -> vec3<f32> {
let clip_xy = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - 2.0 * uv.y);
let t = uniforms.inverse_projection * vec4<f32>(clip_xy, depth, 1.0);
let view_xyz = t.xyz / t.w;
return view_xyz;
}
fn view_position_at(pixel_coordinates: vec2<i32>) -> vec3<f32> {
let depth = load_depth(pixel_coordinates, 0i);
let uv = (vec2<f32>(pixel_coordinates) + 0.5) * uniforms.inv_size;
return reconstruct_view_space_position(depth, uv);
}
// PORT: replaces Bevy's load_normal_view_space (which reads a prepass normal texture we do
// not have). Accurate view-space normal reconstruction from depth, atyuwen's 5-tap method:
// for each axis, extrapolate the center depth from the two taps on each side and derive the
// tangent from whichever side predicts it better. This keeps normals stable across depth
// discontinuities where naive derivatives smear.
fn reconstruct_normal(pixel_coordinates: vec2<i32>, pixel_position: vec3<f32>, depth_center: f32) -> vec3<f32> {
let depth_left1 = load_depth(pixel_coordinates + vec2<i32>(-1i, 0i), 0i);
let depth_left2 = load_depth(pixel_coordinates + vec2<i32>(-2i, 0i), 0i);
let depth_right1 = load_depth(pixel_coordinates + vec2<i32>(1i, 0i), 0i);
let depth_right2 = load_depth(pixel_coordinates + vec2<i32>(2i, 0i), 0i);
let depth_top1 = load_depth(pixel_coordinates + vec2<i32>(0i, -1i), 0i);
let depth_top2 = load_depth(pixel_coordinates + vec2<i32>(0i, -2i), 0i);
let depth_bottom1 = load_depth(pixel_coordinates + vec2<i32>(0i, 1i), 0i);
let depth_bottom2 = load_depth(pixel_coordinates + vec2<i32>(0i, 2i), 0i);
let use_left = abs(2.0 * depth_left1 - depth_left2 - depth_center) <
abs(2.0 * depth_right1 - depth_right2 - depth_center);
let use_top = abs(2.0 * depth_top1 - depth_top2 - depth_center) <
abs(2.0 * depth_bottom1 - depth_bottom2 - depth_center);
var ddx: vec3<f32>;
if use_left {
ddx = pixel_position - view_position_at(pixel_coordinates + vec2<i32>(-1i, 0i));
} else {
ddx = view_position_at(pixel_coordinates + vec2<i32>(1i, 0i)) - pixel_position;
}
var ddy: vec3<f32>;
if use_top {
ddy = pixel_position - view_position_at(pixel_coordinates + vec2<i32>(0i, -1i));
} else {
ddy = view_position_at(pixel_coordinates + vec2<i32>(0i, 1i)) - pixel_position;
}
var normal = normalize(cross(ddy, ddx));
// Guard the orientation: the normal must face the camera.
if dot(normal, pixel_position) > 0.0 {
normal = -normal;
}
return normal;
}
fn load_and_reconstruct_view_space_position(uv: vec2<f32>, sample_mip_level: f32) -> vec3<f32> {
// PORT: point-sample the selected mip explicitly instead of textureSampleLevel.
let mip_level = i32(sample_mip_level + 0.5);
let mip_size = max(vec2<i32>(uniforms.size) >> vec2<u32>(u32(mip_level)), vec2<i32>(1i));
let depth = load_depth(vec2<i32>(uv * vec2<f32>(mip_size)), mip_level);
return reconstruct_view_space_position(depth, uv);
}
@compute
@workgroup_size(8, 8, 1)
fn gtao(@builtin(global_invocation_id) global_id: vec3<u32>) {
let slice_count = uniforms.slice_count;
let samples_per_slice_side = uniforms.samples_per_slice_side;
let effect_radius = uniforms.effect_radius;
let falloff_range = 0.615 * effect_radius;
let falloff_from = effect_radius * (1.0 - 0.615);
let falloff_mul = -1.0 / falloff_range;
let falloff_add = falloff_from / falloff_range + 1.0;
let pixel_coordinates = vec2<i32>(global_id.xy);
let uv = (vec2<f32>(pixel_coordinates) + 0.5) * uniforms.inv_size;
var pixel_depth = calculate_neighboring_depth_differences(pixel_coordinates);
let raw_depth = pixel_depth;
pixel_depth += 0.00001; // Avoid depth precision issues
let pixel_position = reconstruct_view_space_position(pixel_depth, uv);
// PORT: the reconstruction differences the center position against neighbor positions
// built from unbiased depths, so its center must use the raw depth too: at this game's
// depth scale (far plane 200000 -> depth ~5e-3) Bevy's +0.00001 bias is comparable to a
// one-pixel depth step, and a biased center corrupts both tangents.
let pixel_normal = reconstruct_normal(
pixel_coordinates, reconstruct_view_space_position(raw_depth, uv), raw_depth);
let view_vec = normalize(-pixel_position);
let noise = load_noise(pixel_coordinates);
let sample_scale = (-0.5 * effect_radius * uniforms.projection[0][0]) / pixel_position.z;
var visibility = 0.0;
for (var slice_t = 0.0; slice_t < slice_count; slice_t += 1.0) {
let slice = slice_t + noise.x;
let phi = (PI / slice_count) * slice;
let omega = vec2<f32>(cos(phi), sin(phi));
let direction = vec3<f32>(omega.xy, 0.0);
let orthographic_direction = direction - (dot(direction, view_vec) * view_vec);
let axis = cross(direction, view_vec);
let projected_normal = pixel_normal - axis * dot(pixel_normal, axis);
let projected_normal_length = length(projected_normal);
let sign_norm = sign(dot(orthographic_direction, projected_normal));
let cos_norm = saturate(dot(projected_normal, view_vec) / projected_normal_length);
let n = sign_norm * fast_acos(cos_norm);
let min_cos_horizon_1 = cos(n + HALF_PI);
let min_cos_horizon_2 = cos(n - HALF_PI);
var cos_horizon_1 = min_cos_horizon_1;
var cos_horizon_2 = min_cos_horizon_2;
let sample_mul = vec2<f32>(omega.x, -omega.y) * sample_scale;
for (var sample_t = 0.0; sample_t < samples_per_slice_side; sample_t += 1.0) {
var sample_noise = (slice_t + sample_t * samples_per_slice_side) * 0.6180339887498948482;
sample_noise = fract(noise.y + sample_noise);
var s = (sample_t + sample_noise) / samples_per_slice_side;
s *= s; // https://github.com/GameTechDev/XeGTAO#sample-distribution
let sample = s * sample_mul;
// * uniforms.size gets us from [0, 1] to [0, viewport_size], which is needed for this to get the correct mip levels
let sample_mip_level = clamp(log2(length(sample * uniforms.size)) - 3.3, 0.0, 4.0); // https://github.com/GameTechDev/XeGTAO#memory-bandwidth-bottleneck
let sample_position_1 = load_and_reconstruct_view_space_position(uv + sample, sample_mip_level);
let sample_position_2 = load_and_reconstruct_view_space_position(uv - sample, sample_mip_level);
let sample_difference_1 = sample_position_1 - pixel_position;
let sample_difference_2 = sample_position_2 - pixel_position;
let sample_distance_1 = length(sample_difference_1);
let sample_distance_2 = length(sample_difference_2);
var sample_cos_horizon_1 = dot(sample_difference_1 / sample_distance_1, view_vec);
var sample_cos_horizon_2 = dot(sample_difference_2 / sample_distance_2, view_vec);
let weight_1 = saturate(sample_distance_1 * falloff_mul + falloff_add);
let weight_2 = saturate(sample_distance_2 * falloff_mul + falloff_add);
sample_cos_horizon_1 = mix(min_cos_horizon_1, sample_cos_horizon_1, weight_1);
sample_cos_horizon_2 = mix(min_cos_horizon_2, sample_cos_horizon_2, weight_2);
cos_horizon_1 = max(cos_horizon_1, sample_cos_horizon_1);
cos_horizon_2 = max(cos_horizon_2, sample_cos_horizon_2);
}
let horizon_1 = fast_acos(cos_horizon_1);
let horizon_2 = -fast_acos(cos_horizon_2);
let v1 = (cos_norm + 2.0 * horizon_1 * sin(n) - cos(2.0 * horizon_1 - n)) / 4.0;
let v2 = (cos_norm + 2.0 * horizon_2 * sin(n) - cos(2.0 * horizon_2 - n)) / 4.0;
visibility += projected_normal_length * (v1 + v2);
}
visibility /= slice_count;
visibility = clamp(visibility, 0.03, 1.0);
textureStore(ambient_occlusion, pixel_coordinates, vec4<f32>(visibility, 0.0, 0.0, 0.0));
}
@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+19
View File
@@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (C) 2016-2021, Intel Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+138
View File
@@ -0,0 +1,138 @@
// Inputs a depth texture and outputs a MIP-chain of depths.
//
// Because SSAO's performance is bound by texture reads, this increases
// performance over using the full resolution depth for every sample.
//
// Reference: https://research.nvidia.com/sites/default/files/pubs/2012-06_Scalable-Ambient-Obscurance/McGuire12SAO.pdf, section 2.2
//
// Ported from Bevy Engine, crates/bevy_pbr/src/ssao/preprocess_depth.wgsl (v0.13.2),
// licensed MIT OR Apache-2.0 (see res/licenses/), itself derived from Intel XeGTAO (MIT).
//
// PORT: sampler-based gathers replaced with textureLoad (r32float is not filterable without
// optional device features), Bevy view uniforms replaced with the mod's own uniform block,
// storage format r16float -> r32float (core WebGPU storage format). MIP 4 moved into its own
// entry point (core WebGPU limit is 4 storage textures per stage).
struct Uniforms {
projection: mat4x4f,
inverse_projection: mat4x4f,
size: vec2f, // AO chain size in pixels (MIP 0 of the preprocessed depth)
inv_size: vec2f,
depth_scale: vec2f, // input depth snapshot pixels per chain pixel (1 or 2)
effect_radius: f32, // view-space units
intensity: f32,
slice_count: f32,
samples_per_slice_side: f32,
debug_view: u32,
_pad: f32,
}
@group(0) @binding(0) var input_depth: texture_2d<f32>;
@group(0) @binding(1) var preprocessed_depth_mip0: texture_storage_2d<r32float, write>;
@group(0) @binding(2) var preprocessed_depth_mip1: texture_storage_2d<r32float, write>;
@group(0) @binding(3) var preprocessed_depth_mip2: texture_storage_2d<r32float, write>;
@group(0) @binding(4) var preprocessed_depth_mip3: texture_storage_2d<r32float, write>;
@group(0) @binding(5) var<uniform> uniforms: Uniforms;
// downsample_mip4 entry point only (disjoint subresources of the same texture).
@group(0) @binding(6) var preprocessed_depth_mip3_in: texture_2d<f32>;
@group(0) @binding(7) var preprocessed_depth_mip4: texture_storage_2d<r32float, write>;
// PORT: replaces the textureGather of the input depth with explicit loads (also handles the
// half-resolution case, where one chain texel covers depth_scale snapshot texels).
fn load_input_depth(pixel_coordinates: vec2<i32>) -> f32 {
let input_size = vec2<i32>(uniforms.size * uniforms.depth_scale);
let coordinates = clamp(vec2<i32>(vec2<f32>(pixel_coordinates) * uniforms.depth_scale),
vec2<i32>(0i), input_size - 1i);
return textureLoad(input_depth, coordinates, 0i).r;
}
// Using 4 depths from the previous MIP, compute a weighted average for the depth of the current MIP
fn weighted_average(depth0: f32, depth1: f32, depth2: f32, depth3: f32) -> f32 {
let depth_range_scale_factor = 0.75;
let effect_radius = depth_range_scale_factor * 0.5 * 1.457;
let falloff_range = 0.615 * effect_radius;
let falloff_from = effect_radius * (1.0 - 0.615);
let falloff_mul = -1.0 / falloff_range;
let falloff_add = falloff_from / falloff_range + 1.0;
let min_depth = min(min(depth0, depth1), min(depth2, depth3));
let weight0 = saturate((depth0 - min_depth) * falloff_mul + falloff_add);
let weight1 = saturate((depth1 - min_depth) * falloff_mul + falloff_add);
let weight2 = saturate((depth2 - min_depth) * falloff_mul + falloff_add);
let weight3 = saturate((depth3 - min_depth) * falloff_mul + falloff_add);
let weight_total = weight0 + weight1 + weight2 + weight3;
return ((weight0 * depth0) + (weight1 * depth1) + (weight2 * depth2) + (weight3 * depth3)) / weight_total;
}
// Used to share the depths from the previous MIP level between all invocations in a workgroup
var<workgroup> previous_mip_depth: array<array<f32, 8>, 8>;
@compute
@workgroup_size(8, 8, 1)
fn preprocess_depth(@builtin(global_invocation_id) global_id: vec3<u32>, @builtin(local_invocation_id) local_id: vec3<u32>) {
let base_coordinates = vec2<i32>(global_id.xy);
// MIP 0 - Copy 4 texels from the input depth (per invocation, 8x8 invocations per workgroup)
let pixel_coordinates0 = base_coordinates * 2i;
let pixel_coordinates1 = pixel_coordinates0 + vec2<i32>(1i, 0i);
let pixel_coordinates2 = pixel_coordinates0 + vec2<i32>(0i, 1i);
let pixel_coordinates3 = pixel_coordinates0 + vec2<i32>(1i, 1i);
let depth0 = load_input_depth(pixel_coordinates0);
let depth1 = load_input_depth(pixel_coordinates1);
let depth2 = load_input_depth(pixel_coordinates2);
let depth3 = load_input_depth(pixel_coordinates3);
textureStore(preprocessed_depth_mip0, pixel_coordinates0, vec4<f32>(depth0, 0.0, 0.0, 0.0));
textureStore(preprocessed_depth_mip0, pixel_coordinates1, vec4<f32>(depth1, 0.0, 0.0, 0.0));
textureStore(preprocessed_depth_mip0, pixel_coordinates2, vec4<f32>(depth2, 0.0, 0.0, 0.0));
textureStore(preprocessed_depth_mip0, pixel_coordinates3, vec4<f32>(depth3, 0.0, 0.0, 0.0));
// MIP 1 - Weighted average of MIP 0's depth values (per invocation, 8x8 invocations per workgroup)
let depth_mip1 = weighted_average(depth0, depth1, depth2, depth3);
textureStore(preprocessed_depth_mip1, base_coordinates, vec4<f32>(depth_mip1, 0.0, 0.0, 0.0));
previous_mip_depth[local_id.x][local_id.y] = depth_mip1;
workgroupBarrier();
// MIP 2 - Weighted average of MIP 1's depth values (per invocation, 4x4 invocations per workgroup)
if all(local_id.xy % vec2<u32>(2u) == vec2<u32>(0u)) {
let mip2_depth0 = previous_mip_depth[local_id.x + 0u][local_id.y + 0u];
let mip2_depth1 = previous_mip_depth[local_id.x + 1u][local_id.y + 0u];
let mip2_depth2 = previous_mip_depth[local_id.x + 0u][local_id.y + 1u];
let mip2_depth3 = previous_mip_depth[local_id.x + 1u][local_id.y + 1u];
let depth_mip2 = weighted_average(mip2_depth0, mip2_depth1, mip2_depth2, mip2_depth3);
textureStore(preprocessed_depth_mip2, base_coordinates / 2i, vec4<f32>(depth_mip2, 0.0, 0.0, 0.0));
previous_mip_depth[local_id.x][local_id.y] = depth_mip2;
}
workgroupBarrier();
// MIP 3 - Weighted average of MIP 2's depth values (per invocation, 2x2 invocations per workgroup)
if all(local_id.xy % vec2<u32>(4u) == vec2<u32>(0u)) {
let mip3_depth0 = previous_mip_depth[local_id.x + 0u][local_id.y + 0u];
let mip3_depth1 = previous_mip_depth[local_id.x + 2u][local_id.y + 0u];
let mip3_depth2 = previous_mip_depth[local_id.x + 0u][local_id.y + 2u];
let mip3_depth3 = previous_mip_depth[local_id.x + 2u][local_id.y + 2u];
let depth_mip3 = weighted_average(mip3_depth0, mip3_depth1, mip3_depth2, mip3_depth3);
textureStore(preprocessed_depth_mip3, base_coordinates / 4i, vec4<f32>(depth_mip3, 0.0, 0.0, 0.0));
previous_mip_depth[local_id.x][local_id.y] = depth_mip3;
}
}
// MIP 4: weighted average of MIP 3's depth values, as a second (tiny) dispatch.
@compute
@workgroup_size(8, 8, 1)
fn downsample_mip4(@builtin(global_invocation_id) global_id: vec3<u32>) {
let base_coordinates = vec2<i32>(global_id.xy);
let mip3_size = max(vec2<i32>(textureDimensions(preprocessed_depth_mip3_in)), vec2<i32>(1i));
let coordinates0 = clamp(base_coordinates * 2i, vec2<i32>(0i), mip3_size - 1i);
let coordinates1 = clamp(base_coordinates * 2i + vec2<i32>(1i, 0i), vec2<i32>(0i), mip3_size - 1i);
let coordinates2 = clamp(base_coordinates * 2i + vec2<i32>(0i, 1i), vec2<i32>(0i), mip3_size - 1i);
let coordinates3 = clamp(base_coordinates * 2i + vec2<i32>(1i, 1i), vec2<i32>(0i), mip3_size - 1i);
let depth0 = textureLoad(preprocessed_depth_mip3_in, coordinates0, 0i).r;
let depth1 = textureLoad(preprocessed_depth_mip3_in, coordinates1, 0i).r;
let depth2 = textureLoad(preprocessed_depth_mip3_in, coordinates2, 0i).r;
let depth3 = textureLoad(preprocessed_depth_mip3_in, coordinates3, 0i).r;
let depth_mip4 = weighted_average(depth0, depth1, depth2, depth3);
textureStore(preprocessed_depth_mip4, base_coordinates, vec4<f32>(depth_mip4, 0.0, 0.0, 0.0));
}
+931
View File
@@ -0,0 +1,931 @@
// Ambient occlusion (GTAO) example mod.
//
// Showcases the gfx service's compute tasks and the camera service: after opaque scene draws,
// before translucent/fog overlays, the scene depth is resolved and a three-dispatch compute
// chain (depth MIP prefilter, GTAO, spatial denoise) produces a visibility texture that a
// fullscreen draw multiplies over the world.
//
// The WGSL in res/ is ported from Bevy Engine's SSAO implementation (MIT OR Apache-2.0),
// itself based on Intel XeGTAO (MIT); see res/licenses/ and the `PORT:` notes in the shaders.
#include "mods/service.hpp"
#include "mods/svc/camera.h"
#include "mods/svc/config.h"
#include "mods/svc/gfx.h"
#include "mods/svc/log.h"
#include "mods/svc/resource.h"
#include "mods/svc/ui.h"
#include <algorithm>
#include <atomic>
#include <cstring>
#include <initializer_list>
#include <type_traits>
#include <utility>
#include <vector>
#include <webgpu/webgpu.h>
DEFINE_MOD();
IMPORT_SERVICE(LogService, svc_log);
IMPORT_SERVICE(ConfigService, svc_config);
IMPORT_SERVICE(ResourceService, svc_resource);
IMPORT_SERVICE(UiService, svc_ui);
IMPORT_SERVICE(GfxService, svc_gfx);
IMPORT_SERVICE(CameraService, svc_camera);
namespace {
ConfigVarHandle g_cvarEnabled = 0;
ConfigVarHandle g_cvarQuality = 0;
ConfigVarHandle g_cvarRadius = 0;
ConfigVarHandle g_cvarIntensity = 0;
ConfigVarHandle g_cvarHalfRes = 0;
ConfigVarHandle g_cvarDebugView = 0;
GfxComputeTypeHandle g_computeType = 0;
GfxDrawTypeHandle g_drawType = 0;
GfxStageHookHandle g_afterOpaqueHook = 0;
UiWindowHandle g_controlsWindow = 0;
ResourceBuffer g_preprocessSource = RESOURCE_BUFFER_INIT;
ResourceBuffer g_gtaoSource = RESOURCE_BUFFER_INIT;
ResourceBuffer g_denoiseSource = RESOURCE_BUFFER_INIT;
ResourceBuffer g_compositeSource = RESOURCE_BUFFER_INIT;
GfxDeviceInfo g_deviceInfo = GFX_DEVICE_INFO_INIT;
WGPUComputePipeline g_preprocessPipeline = nullptr;
WGPUComputePipeline g_mip4Pipeline = nullptr;
WGPUComputePipeline g_gtaoPipeline = nullptr;
WGPUComputePipeline g_denoisePipeline = nullptr;
WGPUBindGroupLayout g_preprocessLayout = nullptr;
WGPUBindGroupLayout g_mip4Layout = nullptr;
WGPUBindGroupLayout g_gtaoLayout = nullptr;
WGPUBindGroupLayout g_denoiseLayout = nullptr;
WGPURenderPipeline g_compositePipeline = nullptr;
WGPURenderPipeline g_compositeDebugPipeline = nullptr;
WGPUBindGroupLayout g_compositeLayout = nullptr;
WGPUBindGroupLayout g_compositeDebugLayout = nullptr;
WGPUTexture g_hilbertLut = nullptr;
WGPUTextureView g_hilbertLutView = nullptr;
// AO chain targets, recreated when the render size (or halfRes) changes. Old sets are retired
// for a few frames instead of released immediately: payloads embedding their views may still
// be in flight on the render worker.
struct AoTargets {
uint32_t width = 0;
uint32_t height = 0;
WGPUTexture preprocessedDepth = nullptr;
WGPUTextureView preprocessedDepthMips[5] = {};
WGPUTextureView preprocessedDepthAll = nullptr;
WGPUTexture aoNoisy = nullptr;
WGPUTextureView aoNoisyView = nullptr;
WGPUTexture depthDifferences = nullptr;
WGPUTextureView depthDifferencesView = nullptr;
WGPUTexture aoFinal = nullptr;
WGPUTextureView aoFinalView = nullptr;
};
AoTargets g_targets;
struct RetiredTargets {
AoTargets targets;
int framesLeft = 0;
};
std::vector<RetiredTargets> g_retiredTargets;
bool g_warnedNoDepth = false;
bool g_loggedChain = false;
std::atomic g_chainExecuted{false};
// Mirror of the WGSL Uniforms struct (keep in sync with res/*.wgsl).
struct AoUniforms {
float projection[16];
float inverse_projection[16];
float size[2];
float inv_size[2];
float depth_scale[2];
float effect_radius;
float intensity;
float slice_count;
float samples_per_slice_side;
uint32_t debug_view;
float _pad;
};
static_assert(sizeof(AoUniforms) % 16 == 0);
struct ComputePayload {
WGPUTextureView depth; // frame-pooled scene depth snapshot
WGPUTextureView preprocessedDepthMips[5];
WGPUTextureView preprocessedDepthAll;
WGPUTextureView aoNoisy;
WGPUTextureView depthDifferences;
WGPUTextureView aoFinal;
uint32_t uniform_offset;
uint32_t uniform_size;
uint32_t width;
uint32_t height;
};
static_assert(sizeof(ComputePayload) <= GFX_INLINE_DRAW_PAYLOAD_SIZE);
static_assert(std::is_trivially_copyable_v<ComputePayload>);
struct CompositePayload {
WGPUTextureView aoFinal;
WGPUTextureView preprocessedDepth; // debug views reconstruct normals/depth from it
WGPUTextureView sceneDepth; // raw snapshot, for the bypass debug views
uint32_t uniform_offset;
uint32_t uniform_size;
uint32_t debug_view;
};
static_assert(sizeof(CompositePayload) <= GFX_INLINE_DRAW_PAYLOAD_SIZE);
static_assert(std::is_trivially_copyable_v<CompositePayload>);
int64_t get_int_option(ConfigVarHandle handle, int64_t fallback) {
int64_t value = fallback;
if (handle == 0 || svc_config->get_int(mod_ctx, handle, &value) != MOD_OK) {
return fallback;
}
return value;
}
bool get_bool_option(ConfigVarHandle handle, bool fallback) {
bool value = fallback;
if (handle == 0 || svc_config->get_bool(mod_ctx, handle, &value) != MOD_OK) {
return fallback;
}
return value;
}
// XeGTAO/Bevy quality presets: slices x (samples per slice side * 2).
void quality_counts(int64_t quality, float& sliceCount, float& samplesPerSliceSide) {
switch (std::clamp<int64_t>(quality, 0, 3)) {
case 0:
sliceCount = 1.0f;
samplesPerSliceSide = 2.0f;
break;
case 1:
sliceCount = 2.0f;
samplesPerSliceSide = 2.0f;
break;
default:
case 2:
sliceCount = 3.0f;
samplesPerSliceSide = 3.0f;
break;
case 3:
sliceCount = 9.0f;
samplesPerSliceSide = 3.0f;
break;
}
}
WGPUShaderModule create_shader_module(const char* label, const ResourceBuffer& source) {
WGPUShaderSourceWGSL wgsl = WGPU_SHADER_SOURCE_WGSL_INIT;
wgsl.code = {static_cast<const char*>(source.data), source.size};
WGPUShaderModuleDescriptor moduleDesc = WGPU_SHADER_MODULE_DESCRIPTOR_INIT;
moduleDesc.nextInChain = &wgsl.chain;
moduleDesc.label = {label, WGPU_STRLEN};
return wgpuDeviceCreateShaderModule(g_deviceInfo.device, &moduleDesc);
}
bool build_compute_pipeline(const char* label, const ResourceBuffer& source, const char* entry,
WGPUComputePipeline& outPipeline, WGPUBindGroupLayout& outLayout) {
WGPUShaderModule module = create_shader_module(label, source);
if (module == nullptr) {
return false;
}
WGPUComputePipelineDescriptor pipelineDesc = WGPU_COMPUTE_PIPELINE_DESCRIPTOR_INIT;
pipelineDesc.label = {label, WGPU_STRLEN};
pipelineDesc.compute.module = module;
pipelineDesc.compute.entryPoint = {entry, WGPU_STRLEN};
outPipeline = wgpuDeviceCreateComputePipeline(g_deviceInfo.device, &pipelineDesc);
wgpuShaderModuleRelease(module);
if (outPipeline == nullptr) {
return false;
}
outLayout = wgpuComputePipelineGetBindGroupLayout(outPipeline, 0);
return outLayout != nullptr;
}
bool build_composite_pipeline(
bool blend, WGPURenderPipeline& outPipeline, WGPUBindGroupLayout& outLayout) {
WGPUShaderModule module = create_shader_module("AO composite", g_compositeSource);
if (module == nullptr) {
return false;
}
// Multiply blend
WGPUBlendState blendState{
.color =
{
.operation = WGPUBlendOperation_Add,
.srcFactor = WGPUBlendFactor_Dst,
.dstFactor = WGPUBlendFactor_Zero,
},
.alpha =
{
.operation = WGPUBlendOperation_Add,
.srcFactor = WGPUBlendFactor_Zero,
.dstFactor = WGPUBlendFactor_One,
},
};
WGPUColorTargetState colorTarget = WGPU_COLOR_TARGET_STATE_INIT;
colorTarget.format = g_deviceInfo.color_format;
if (blend) {
colorTarget.blend = &blendState;
}
WGPUFragmentState fragment = WGPU_FRAGMENT_STATE_INIT;
fragment.module = module;
fragment.entryPoint = {"fs_main", WGPU_STRLEN};
fragment.targetCount = 1;
fragment.targets = &colorTarget;
// Depth state must match the EFB pass despite never touching depth.
WGPUDepthStencilState depthStencil = WGPU_DEPTH_STENCIL_STATE_INIT;
depthStencil.format = g_deviceInfo.depth_format;
depthStencil.depthWriteEnabled = WGPUOptionalBool_False;
depthStencil.depthCompare = WGPUCompareFunction_Always;
WGPURenderPipelineDescriptor pipelineDesc = WGPU_RENDER_PIPELINE_DESCRIPTOR_INIT;
pipelineDesc.label = {blend ? "AO composite" : "AO composite (debug)", WGPU_STRLEN};
pipelineDesc.vertex.module = module;
pipelineDesc.vertex.entryPoint = {"vs_main", WGPU_STRLEN};
pipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
pipelineDesc.depthStencil = &depthStencil;
pipelineDesc.multisample.count = g_deviceInfo.sample_count;
pipelineDesc.fragment = &fragment;
outPipeline = wgpuDeviceCreateRenderPipeline(g_deviceInfo.device, &pipelineDesc);
wgpuShaderModuleRelease(module);
if (outPipeline == nullptr) {
return false;
}
outLayout = wgpuRenderPipelineGetBindGroupLayout(outPipeline, 0);
return outLayout != nullptr;
}
// Hilbert curve index LUT for the R2 noise sequence, generated once at init.
// Ported from Bevy's generate_hilbert_index_lut (https://www.shadertoy.com/view/3tB3z3).
uint16_t hilbert_index(uint16_t x, uint16_t y) {
uint16_t index = 0;
for (uint16_t level = 32; level > 0; level /= 2) {
const uint16_t regionX = (x & level) > 0 ? 1 : 0;
const uint16_t regionY = (y & level) > 0 ? 1 : 0;
index += level * level * ((3 * regionX) ^ regionY);
if (regionY == 0) {
if (regionX == 1) {
x = 63 - x;
y = 63 - y;
}
std::swap(x, y);
}
}
return index;
}
bool build_hilbert_lut() {
WGPUTextureDescriptor texDesc = WGPU_TEXTURE_DESCRIPTOR_INIT;
texDesc.label = {"AO hilbert LUT", WGPU_STRLEN};
texDesc.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst;
texDesc.size = {64, 64, 1};
texDesc.format = WGPUTextureFormat_R16Uint;
g_hilbertLut = wgpuDeviceCreateTexture(g_deviceInfo.device, &texDesc);
if (g_hilbertLut == nullptr) {
return false;
}
g_hilbertLutView = wgpuTextureCreateView(g_hilbertLut, nullptr);
if (g_hilbertLutView == nullptr) {
return false;
}
uint16_t lut[64 * 64];
for (uint16_t y = 0; y < 64; ++y) {
for (uint16_t x = 0; x < 64; ++x) {
lut[y * 64 + x] = hilbert_index(x, y);
}
}
WGPUTexelCopyTextureInfo dst = WGPU_TEXEL_COPY_TEXTURE_INFO_INIT;
dst.texture = g_hilbertLut;
WGPUTexelCopyBufferLayout layout{.offset = 0, .bytesPerRow = 64 * 2, .rowsPerImage = 64};
WGPUExtent3D extent{64, 64, 1};
wgpuQueueWriteTexture(g_deviceInfo.queue, &dst, lut, sizeof(lut), &layout, &extent);
return true;
}
void release_targets(AoTargets& targets) {
for (auto*& view : targets.preprocessedDepthMips) {
if (view != nullptr) {
wgpuTextureViewRelease(view);
view = nullptr;
}
}
const auto releaseView = [](WGPUTextureView& view) {
if (view != nullptr) {
wgpuTextureViewRelease(view);
view = nullptr;
}
};
const auto releaseTexture = [](WGPUTexture& texture) {
if (texture != nullptr) {
wgpuTextureRelease(texture);
texture = nullptr;
}
};
releaseView(targets.preprocessedDepthAll);
releaseView(targets.aoNoisyView);
releaseView(targets.depthDifferencesView);
releaseView(targets.aoFinalView);
releaseTexture(targets.preprocessedDepth);
releaseTexture(targets.aoNoisy);
releaseTexture(targets.depthDifferences);
releaseTexture(targets.aoFinal);
targets.width = targets.height = 0;
}
void tick_retired_targets() {
for (auto it = g_retiredTargets.begin(); it != g_retiredTargets.end();) {
if (--it->framesLeft <= 0) {
release_targets(it->targets);
it = g_retiredTargets.erase(it);
} else {
++it;
}
}
}
bool ensure_targets(uint32_t width, uint32_t height) {
if (g_targets.width == width && g_targets.height == height) {
return true;
}
if (g_targets.width != 0) {
g_retiredTargets.push_back(RetiredTargets{std::exchange(g_targets, AoTargets{}), 4});
}
const auto createStorageTexture = [&](const char* label, WGPUTextureFormat format,
uint32_t mipCount, WGPUTexture& outTexture) {
WGPUTextureDescriptor texDesc = WGPU_TEXTURE_DESCRIPTOR_INIT;
texDesc.label = {label, WGPU_STRLEN};
texDesc.usage = WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding;
texDesc.size = {width, height, 1};
texDesc.format = format;
texDesc.mipLevelCount = mipCount;
outTexture = wgpuDeviceCreateTexture(g_deviceInfo.device, &texDesc);
return outTexture != nullptr;
};
bool ok = createStorageTexture("AO preprocessed depth", WGPUTextureFormat_R32Float, 5,
g_targets.preprocessedDepth) &&
createStorageTexture("AO noisy", WGPUTextureFormat_R32Float, 1, g_targets.aoNoisy) &&
createStorageTexture("AO depth differences", WGPUTextureFormat_R32Uint, 1,
g_targets.depthDifferences) &&
createStorageTexture("AO final", WGPUTextureFormat_R32Float, 1, g_targets.aoFinal);
if (ok) {
for (uint32_t mip = 0; mip < 5 && ok; ++mip) {
WGPUTextureViewDescriptor viewDesc = WGPU_TEXTURE_VIEW_DESCRIPTOR_INIT;
viewDesc.baseMipLevel = mip;
viewDesc.mipLevelCount = 1;
g_targets.preprocessedDepthMips[mip] =
wgpuTextureCreateView(g_targets.preprocessedDepth, &viewDesc);
ok = g_targets.preprocessedDepthMips[mip] != nullptr;
}
}
if (ok) {
g_targets.preprocessedDepthAll =
wgpuTextureCreateView(g_targets.preprocessedDepth, nullptr);
g_targets.aoNoisyView = wgpuTextureCreateView(g_targets.aoNoisy, nullptr);
g_targets.depthDifferencesView = wgpuTextureCreateView(g_targets.depthDifferences, nullptr);
g_targets.aoFinalView = wgpuTextureCreateView(g_targets.aoFinal, nullptr);
ok = g_targets.preprocessedDepthAll != nullptr && g_targets.aoNoisyView != nullptr &&
g_targets.depthDifferencesView != nullptr && g_targets.aoFinalView != nullptr;
}
if (!ok) {
release_targets(g_targets);
return false;
}
g_targets.width = width;
g_targets.height = height;
return true;
}
constexpr uint32_t div_ceil(uint32_t numerator, uint32_t denominator) {
return (numerator + denominator - 1) / denominator;
}
// Render worker thread: the AO chain as one compute pass with three dispatches.
void on_compute(
ModContext*, const GfxComputeContext* ctx, const void* payload, size_t payloadSize, void*) {
if (payloadSize != sizeof(ComputePayload)) {
return;
}
ComputePayload data;
std::memcpy(&data, payload, sizeof(data));
if (data.depth == nullptr || g_preprocessPipeline == nullptr) {
return;
}
const auto makeBindGroup = [&](WGPUBindGroupLayout layout,
std::initializer_list<WGPUBindGroupEntry> entries) {
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
bindGroupDesc.layout = layout;
bindGroupDesc.entryCount = entries.size();
bindGroupDesc.entries = entries.begin();
return wgpuDeviceCreateBindGroup(ctx->device, &bindGroupDesc);
};
const auto textureEntry = [](uint32_t binding, WGPUTextureView view) {
WGPUBindGroupEntry entry = WGPU_BIND_GROUP_ENTRY_INIT;
entry.binding = binding;
entry.textureView = view;
return entry;
};
const auto uniformEntry = [&](uint32_t binding) {
WGPUBindGroupEntry entry = WGPU_BIND_GROUP_ENTRY_INIT;
entry.binding = binding;
entry.buffer = ctx->uniform_buffer;
entry.offset = data.uniform_offset;
entry.size = data.uniform_size;
return entry;
};
WGPUBindGroup preprocessGroup = makeBindGroup(g_preprocessLayout,
{textureEntry(0, data.depth), textureEntry(1, data.preprocessedDepthMips[0]),
textureEntry(2, data.preprocessedDepthMips[1]),
textureEntry(3, data.preprocessedDepthMips[2]),
textureEntry(4, data.preprocessedDepthMips[3]), uniformEntry(5)});
WGPUBindGroup mip4Group =
makeBindGroup(g_mip4Layout, {textureEntry(6, data.preprocessedDepthMips[3]),
textureEntry(7, data.preprocessedDepthMips[4])});
WGPUBindGroup gtaoGroup = makeBindGroup(
g_gtaoLayout, {textureEntry(0, data.preprocessedDepthAll),
textureEntry(1, g_hilbertLutView), textureEntry(2, data.aoNoisy),
textureEntry(3, data.depthDifferences), uniformEntry(4)});
WGPUBindGroup denoiseGroup = makeBindGroup(
g_denoiseLayout, {textureEntry(0, data.aoNoisy), textureEntry(1, data.depthDifferences),
textureEntry(2, data.aoFinal), uniformEntry(3)});
if (preprocessGroup == nullptr || mip4Group == nullptr || gtaoGroup == nullptr ||
denoiseGroup == nullptr)
{
const auto release = [](WGPUBindGroup group) {
if (group != nullptr) {
wgpuBindGroupRelease(group);
}
};
release(preprocessGroup);
release(mip4Group);
release(gtaoGroup);
release(denoiseGroup);
return;
}
WGPUComputePassDescriptor passDesc = WGPU_COMPUTE_PASS_DESCRIPTOR_INIT;
passDesc.label = {"AO chain", WGPU_STRLEN};
WGPUComputePassEncoder pass = wgpuCommandEncoderBeginComputePass(ctx->encoder, &passDesc);
// Each preprocess workgroup covers 16x16 MIP-0 texels (8x8 invocations, 2x2 texels each).
wgpuComputePassEncoderSetPipeline(pass, g_preprocessPipeline);
wgpuComputePassEncoderSetBindGroup(pass, 0, preprocessGroup, 0, nullptr);
wgpuComputePassEncoderDispatchWorkgroups(
pass, div_ceil(data.width, 16), div_ceil(data.height, 16), 1);
wgpuComputePassEncoderSetPipeline(pass, g_mip4Pipeline);
wgpuComputePassEncoderSetBindGroup(pass, 0, mip4Group, 0, nullptr);
wgpuComputePassEncoderDispatchWorkgroups(pass, div_ceil(std::max(data.width >> 4, 1u), 8),
div_ceil(std::max(data.height >> 4, 1u), 8), 1);
wgpuComputePassEncoderSetPipeline(pass, g_gtaoPipeline);
wgpuComputePassEncoderSetBindGroup(pass, 0, gtaoGroup, 0, nullptr);
wgpuComputePassEncoderDispatchWorkgroups(
pass, div_ceil(data.width, 8), div_ceil(data.height, 8), 1);
wgpuComputePassEncoderSetPipeline(pass, g_denoisePipeline);
wgpuComputePassEncoderSetBindGroup(pass, 0, denoiseGroup, 0, nullptr);
wgpuComputePassEncoderDispatchWorkgroups(
pass, div_ceil(data.width, 8), div_ceil(data.height, 8), 1);
wgpuComputePassEncoderEnd(pass);
wgpuComputePassEncoderRelease(pass);
wgpuBindGroupRelease(preprocessGroup);
wgpuBindGroupRelease(mip4Group);
wgpuBindGroupRelease(gtaoGroup);
wgpuBindGroupRelease(denoiseGroup);
g_chainExecuted.store(true, std::memory_order_release);
}
// Render worker thread: composite the AO over the scene (or show it, in debug view).
void on_draw(
ModContext*, const GfxDrawContext* ctx, const void* payload, size_t payloadSize, void*) {
if (payloadSize != sizeof(CompositePayload)) {
return;
}
CompositePayload data;
std::memcpy(&data, payload, sizeof(data));
WGPURenderPipeline pipeline =
data.debug_view != 0 ? g_compositeDebugPipeline : g_compositePipeline;
WGPUBindGroupLayout layout = data.debug_view != 0 ? g_compositeDebugLayout : g_compositeLayout;
if (data.aoFinal == nullptr || data.preprocessedDepth == nullptr ||
data.sceneDepth == nullptr || pipeline == nullptr)
{
return;
}
WGPUBindGroupEntry entries[4] = {WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT,
WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT};
entries[0].binding = 0;
entries[0].textureView = data.aoFinal;
entries[1].binding = 1;
entries[1].textureView = data.preprocessedDepth;
entries[2].binding = 2;
entries[2].textureView = data.sceneDepth;
entries[3].binding = 3;
entries[3].buffer = ctx->uniform_buffer;
entries[3].offset = data.uniform_offset;
entries[3].size = data.uniform_size;
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
bindGroupDesc.layout = layout;
bindGroupDesc.entryCount = 4;
bindGroupDesc.entries = entries;
WGPUBindGroup bindGroup = wgpuDeviceCreateBindGroup(ctx->device, &bindGroupDesc);
if (bindGroup == nullptr) {
return;
}
wgpuRenderPassEncoderSetPipeline(ctx->pass, pipeline);
wgpuRenderPassEncoderSetBindGroup(ctx->pass, 0, bindGroup, 0, nullptr);
wgpuRenderPassEncoderDraw(ctx->pass, 3, 1, 0, 0);
wgpuBindGroupRelease(bindGroup);
}
// Game thread, after opaque scene draws and before translucent/fog overlay lists.
void on_scene_after_opaque(ModContext*, const GfxStageContext* stageCtx, void*) {
tick_retired_targets();
if (!get_bool_option(g_cvarEnabled, true)) {
return;
}
if (stageCtx == nullptr || stageCtx->struct_size < sizeof(GfxStageContext) ||
stageCtx->game_view == nullptr)
{
return;
}
CameraInfo camera = CAMERA_INFO_INIT;
if (svc_camera->get_camera(mod_ctx, stageCtx->game_view, &camera) != MOD_OK) {
return;
}
GfxResolveDesc resolveDesc = GFX_RESOLVE_DESC_INIT;
resolveDesc.color = false;
resolveDesc.depth = true;
GfxResolvedTargets resolved = GFX_RESOLVED_TARGETS_INIT;
if (svc_gfx->resolve_pass(mod_ctx, &resolveDesc, &resolved) != MOD_OK ||
resolved.depth == nullptr)
{
if (!g_warnedNoDepth) {
g_warnedNoDepth = true;
svc_log->warn(mod_ctx, "depth snapshots unavailable; AO disabled");
}
return;
}
const bool halfRes = get_bool_option(g_cvarHalfRes, false);
const uint32_t divisor = halfRes ? 2 : 1;
const uint32_t width = resolved.width / divisor;
const uint32_t height = resolved.height / divisor;
if (width < 32 || height < 32 || !ensure_targets(width, height)) {
return;
}
AoUniforms uniforms{};
std::memcpy(uniforms.projection, camera.proj_from_view, sizeof(uniforms.projection));
std::memcpy(
uniforms.inverse_projection, camera.view_from_proj, sizeof(uniforms.inverse_projection));
uniforms.size[0] = static_cast<float>(width);
uniforms.size[1] = static_cast<float>(height);
uniforms.inv_size[0] = 1.0f / uniforms.size[0];
uniforms.inv_size[1] = 1.0f / uniforms.size[1];
uniforms.depth_scale[0] = static_cast<float>(resolved.width) / uniforms.size[0];
uniforms.depth_scale[1] = static_cast<float>(resolved.height) / uniforms.size[1];
uniforms.effect_radius =
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarRadius, 70), 10, 500));
uniforms.intensity =
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarIntensity, 100), 0, 100)) /
100.0f;
quality_counts(
get_int_option(g_cvarQuality, 2), uniforms.slice_count, uniforms.samples_per_slice_side);
const uint32_t debugMode =
static_cast<uint32_t>(std::clamp<int64_t>(get_int_option(g_cvarDebugView, 0), 0, 4));
uniforms.debug_view = debugMode;
GfxRange uniformRange{0, 0};
if (svc_gfx->push_uniform(mod_ctx, &uniforms, sizeof(uniforms), &uniformRange) != MOD_OK) {
return;
}
ComputePayload computePayload{};
computePayload.depth = resolved.depth;
for (int mip = 0; mip < 5; ++mip) {
computePayload.preprocessedDepthMips[mip] = g_targets.preprocessedDepthMips[mip];
}
computePayload.preprocessedDepthAll = g_targets.preprocessedDepthAll;
computePayload.aoNoisy = g_targets.aoNoisyView;
computePayload.depthDifferences = g_targets.depthDifferencesView;
computePayload.aoFinal = g_targets.aoFinalView;
computePayload.uniform_offset = uniformRange.offset;
computePayload.uniform_size = uniformRange.size;
computePayload.width = width;
computePayload.height = height;
if (svc_gfx->push_compute(mod_ctx, g_computeType, &computePayload, sizeof(computePayload)) !=
MOD_OK)
{
return;
}
const CompositePayload drawPayload{g_targets.aoFinalView, g_targets.preprocessedDepthAll,
resolved.depth, uniformRange.offset, uniformRange.size, debugMode};
svc_gfx->push_draw(mod_ctx, g_drawType, &drawPayload, sizeof(drawPayload));
}
void add_control(UiElementHandle pane, const UiControlDesc& desc) {
svc_ui->pane_add_control(mod_ctx, pane, &desc, nullptr);
}
void add_toggle(UiElementHandle pane, const char* label, ConfigVarHandle cvar, const char* help) {
UiControlDesc control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_TOGGLE;
control.label = label;
control.help_rml = help;
control.binding = UI_BINDING_CONFIG_VAR;
control.config_var = cvar;
add_control(pane, control);
}
ModResult build_controls_tab(
ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) {
(void)right;
svc_ui->pane_add_section(mod_ctx, left, "Ambient Occlusion");
add_toggle(left, "Enabled", g_cvarEnabled, "Enables the GTAO pass.");
static const char* kQualityOptions[] = {"Low", "Medium", "High", "Ultra"};
UiControlDesc control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_SELECT;
control.label = "Quality";
control.help_rml = "Horizon slices and samples per pixel (XeGTAO presets: 4/8/18/54 spp).";
control.binding = UI_BINDING_CONFIG_VAR;
control.config_var = g_cvarQuality;
control.options = kQualityOptions;
control.option_count = 4;
add_control(left, control);
control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_NUMBER;
control.label = "Radius";
control.help_rml = "Occlusion sampling radius in world units.";
control.binding = UI_BINDING_CONFIG_VAR;
control.config_var = g_cvarRadius;
control.min = 10;
control.max = 500;
control.step = 10;
add_control(left, control);
control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_NUMBER;
control.label = "Intensity";
control.help_rml = "How strongly occlusion darkens the scene.";
control.binding = UI_BINDING_CONFIG_VAR;
control.config_var = g_cvarIntensity;
control.min = 0;
control.max = 100;
control.step = 5;
control.suffix = "%";
add_control(left, control);
add_toggle(left, "Half Resolution", g_cvarHalfRes,
"Computes AO at half resolution and upscales; faster, slightly softer.");
static const char* kDebugOptions[] = {"Off", "AO", "Normals", "Depth", "Staircase"};
control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_SELECT;
control.label = "Debug View";
control.help_rml = "AO: raw visibility as grayscale.<br/>Normals: the view-space "
"normals the GTAO pass consumes.<br/>Depth: the preprocessed depth "
"as a distance gradient.<br/>Staircase: detects quantized depth - smooth "
"depth is near-black with thin triangle edges, quantized depth lights "
"up across surfaces.";
control.binding = UI_BINDING_CONFIG_VAR;
control.config_var = g_cvarDebugView;
control.options = kDebugOptions;
control.option_count = 5;
add_control(left, control);
return MOD_OK;
}
void on_controls_window_closed(ModContext*, UiWindowHandle, void*) {
g_controlsWindow = 0;
}
void on_open_controls(ModContext*, void*) {
if (g_controlsWindow != 0) {
return;
}
UiTabDesc tabs[1] = {UI_TAB_DESC_INIT};
tabs[0].title = "Controls";
tabs[0].build = build_controls_tab;
UiWindowDesc desc = UI_WINDOW_DESC_INIT;
desc.tabs = tabs;
desc.tab_count = 1;
desc.on_closed = on_controls_window_closed;
if (svc_ui->window_push(mod_ctx, &desc, &g_controlsWindow) != MOD_OK) {
svc_log->error(mod_ctx, "failed to open AO controls window");
}
}
ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) {
UiControlDesc control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_TOGGLE;
control.label = "Enabled";
control.binding = UI_BINDING_CONFIG_VAR;
control.config_var = g_cvarEnabled;
add_control(panel, control);
control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_BUTTON;
control.label = "Open Controls";
control.on_pressed = on_open_controls;
add_control(panel, control);
return MOD_OK;
}
ModResult register_bool_option(
const char* name, bool defaultValue, ConfigVarHandle& outHandle, ModError* error) {
ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT;
cvarDesc.name = name;
cvarDesc.type = CONFIG_VAR_BOOL;
cvarDesc.default_bool = defaultValue;
if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) {
return dusk::mods::set_error(error, MOD_ERROR, "failed to register AO option");
}
return MOD_OK;
}
ModResult register_int_option(
const char* name, int64_t defaultValue, ConfigVarHandle& outHandle, ModError* error) {
ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT;
cvarDesc.name = name;
cvarDesc.type = CONFIG_VAR_INT;
cvarDesc.default_int = defaultValue;
if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) {
return dusk::mods::set_error(error, MOD_ERROR, "failed to register AO option");
}
return MOD_OK;
}
} // namespace
extern "C" {
MOD_EXPORT ModResult mod_initialize(ModError* error) {
ModResult result = svc_resource->load(mod_ctx, "preprocess_depth.wgsl", &g_preprocessSource);
if (result == MOD_OK) {
result = svc_resource->load(mod_ctx, "gtao.wgsl", &g_gtaoSource);
}
if (result == MOD_OK) {
result = svc_resource->load(mod_ctx, "denoise.wgsl", &g_denoiseSource);
}
if (result == MOD_OK) {
result = svc_resource->load(mod_ctx, "composite.wgsl", &g_compositeSource);
}
if (result != MOD_OK) {
return dusk::mods::set_error(error, result, "failed to load AO shaders");
}
result = register_bool_option("effectEnabled", false, g_cvarEnabled, error);
if (result != MOD_OK) {
return result;
}
result = register_int_option("quality", 2, g_cvarQuality, error);
if (result != MOD_OK) {
return result;
}
result = register_int_option("radius", 70, g_cvarRadius, error);
if (result != MOD_OK) {
return result;
}
result = register_int_option("intensity", 100, g_cvarIntensity, error);
if (result != MOD_OK) {
return result;
}
result = register_bool_option("halfRes", false, g_cvarHalfRes, error);
if (result != MOD_OK) {
return result;
}
result = register_int_option("debugMode", 0, g_cvarDebugView, error);
if (result != MOD_OK) {
return result;
}
if (svc_gfx->get_device_info(mod_ctx, &g_deviceInfo) != MOD_OK) {
return dusk::mods::set_error(error, MOD_ERROR, "failed to query device info");
}
if (!build_compute_pipeline("AO preprocess depth", g_preprocessSource, "preprocess_depth",
g_preprocessPipeline, g_preprocessLayout) ||
!build_compute_pipeline("AO downsample mip4", g_preprocessSource, "downsample_mip4",
g_mip4Pipeline, g_mip4Layout) ||
!build_compute_pipeline("AO gtao", g_gtaoSource, "gtao", g_gtaoPipeline, g_gtaoLayout) ||
!build_compute_pipeline(
"AO denoise", g_denoiseSource, "spatial_denoise", g_denoisePipeline, g_denoiseLayout))
{
return dusk::mods::set_error(error, MOD_ERROR, "failed to create AO compute pipelines");
}
if (!build_composite_pipeline(true, g_compositePipeline, g_compositeLayout) ||
!build_composite_pipeline(false, g_compositeDebugPipeline, g_compositeDebugLayout))
{
return dusk::mods::set_error(error, MOD_ERROR, "failed to create AO composite pipeline");
}
if (!build_hilbert_lut()) {
return dusk::mods::set_error(error, MOD_ERROR, "failed to create AO noise LUT");
}
GfxComputeTypeDesc computeDesc = GFX_COMPUTE_TYPE_DESC_INIT;
computeDesc.label = "AO chain";
computeDesc.callback = on_compute;
if (svc_gfx->register_compute_type(mod_ctx, &computeDesc, &g_computeType) != MOD_OK) {
return dusk::mods::set_error(error, MOD_ERROR, "failed to register compute type");
}
GfxDrawTypeDesc drawDesc = GFX_DRAW_TYPE_DESC_INIT;
drawDesc.label = "AO composite";
drawDesc.draw = on_draw;
if (svc_gfx->register_draw_type(mod_ctx, &drawDesc, &g_drawType) != MOD_OK) {
return dusk::mods::set_error(error, MOD_ERROR, "failed to register draw type");
}
GfxStageHookDesc stageDesc = GFX_STAGE_HOOK_DESC_INIT;
stageDesc.callback = on_scene_after_opaque;
if (svc_gfx->register_stage_hook(
mod_ctx, GFX_STAGE_SCENE_AFTER_OPAQUE, &stageDesc, &g_afterOpaqueHook) != MOD_OK)
{
return dusk::mods::set_error(error, MOD_ERROR, "failed to register stage hook");
}
UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT;
panelDesc.build = build_panel;
svc_ui->register_mods_panel(mod_ctx, &panelDesc);
svc_log->info(mod_ctx, "ao_mod ready");
return MOD_OK;
}
MOD_EXPORT ModResult mod_update(ModError*) {
if (!g_loggedChain && g_chainExecuted.load(std::memory_order_acquire)) {
g_loggedChain = true;
svc_log->info(mod_ctx, "AO chain executed OK");
}
return MOD_OK;
}
MOD_EXPORT ModResult mod_shutdown(ModError*) {
svc_resource->free(mod_ctx, &g_preprocessSource);
svc_resource->free(mod_ctx, &g_gtaoSource);
svc_resource->free(mod_ctx, &g_denoiseSource);
svc_resource->free(mod_ctx, &g_compositeSource);
release_targets(g_targets);
for (auto& retired : g_retiredTargets) {
release_targets(retired.targets);
}
g_retiredTargets.clear();
const auto releasePipeline = [](WGPUComputePipeline& pipeline) {
if (pipeline != nullptr) {
wgpuComputePipelineRelease(pipeline);
pipeline = nullptr;
}
};
const auto releaseLayout = [](WGPUBindGroupLayout& layout) {
if (layout != nullptr) {
wgpuBindGroupLayoutRelease(layout);
layout = nullptr;
}
};
releasePipeline(g_preprocessPipeline);
releasePipeline(g_mip4Pipeline);
releasePipeline(g_gtaoPipeline);
releasePipeline(g_denoisePipeline);
releaseLayout(g_preprocessLayout);
releaseLayout(g_mip4Layout);
releaseLayout(g_gtaoLayout);
releaseLayout(g_denoiseLayout);
if (g_compositePipeline != nullptr) {
wgpuRenderPipelineRelease(g_compositePipeline);
g_compositePipeline = nullptr;
}
if (g_compositeDebugPipeline != nullptr) {
wgpuRenderPipelineRelease(g_compositeDebugPipeline);
g_compositeDebugPipeline = nullptr;
}
releaseLayout(g_compositeLayout);
releaseLayout(g_compositeDebugLayout);
if (g_hilbertLutView != nullptr) {
wgpuTextureViewRelease(g_hilbertLutView);
g_hilbertLutView = nullptr;
}
if (g_hilbertLut != nullptr) {
wgpuTextureRelease(g_hilbertLut);
g_hilbertLut = nullptr;
}
g_cvarEnabled = g_cvarQuality = g_cvarRadius = g_cvarIntensity = 0;
g_cvarHalfRes = g_cvarDebugView = 0;
g_computeType = g_drawType = 0;
g_afterOpaqueHook = 0;
g_controlsWindow = 0;
return MOD_OK;
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"id": "com.example.mod",
"name": "Template Mod",
"version": "1.0.0",
"author": "You",
"description": "An example Dusklight mod"
}
+7 -2
View File
@@ -1,7 +1,7 @@
# Dusklight Mod SDK entry point
#
# Provides game/service headers, compile definitions and version.h without
# configuring the full game tree.
# Provides game/service headers, compile definitions, version.h and WebGPU
# headers without configuring the full game tree.
#
# Usage (from a mod project):
# add_subdirectory(<dusk>/sdk dusk-sdk EXCLUDE_FROM_ALL)
@@ -30,6 +30,11 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/DetectVersion.cmake")
detect_version()
configure_version_header()
# Provides dawn::webgpu_dawn and dawn::dawncpp_headers for public gfx service headers.
include("${CMAKE_CURRENT_SOURCE_DIR}/../extern/aurora/cmake/AuroraDependencyVersions.cmake")
set(AURORA_DAWN_PROVIDER "package" CACHE STRING "How to provide Dawn for the mod SDK")
include("${CMAKE_CURRENT_SOURCE_DIR}/../extern/aurora/cmake/AuroraDawnProvider.cmake")
# Game ABI headers & compile definitions
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/GameABIConfig.cmake")
-7
View File
@@ -1,7 +0,0 @@
{
"id": "com.example.mod",
"name": "Template Mod",
"version": "1.0.0",
"author": "You",
"description": "An example Dusklight mod"
}