Skip to content

Commit

Permalink
Client: Fix alpha compositing for translucent water surfaces
Browse files Browse the repository at this point in the history
After some investigation, it's become clear that the reference client seems to incorrectly mix sRGB and linear-space colors. While this is a bit of a guess, it would explain why the contrast correction was needed in the first place; it makes up for the underexposure introduced by the lack of gamma-decompression steps after loading sRGB-encoded textures. Indeed, screen blending is used for this exact purpose in digital photo editing (apparently).

In order to reproduce the resulting outputs, neither fully-sRGB nor fully-linearized computations will yield accurate blends while the underlying texture view is in sRGB format. WebGPU automatically converts the fragment colors - first to sRGB and then back to linear as part of the blend stage. Unfortunately, the color space used is actually neither of the two by this point, and so the results can't possibly match expectations.

Originally, sRGB was selected because it's the preferred surface format. Using a different one grants full control of the color space used and eliminates the undesirable conversion, but may incur a performance hit. However, conversions between RGBA and sRGB should be hardware-accelerated on any reasonably modern device. Switching to RGBA also simplifies the shader logic as the added linearization step is no longer needed. So overall it's probably a net gain?
  • Loading branch information
rdw-software committed Feb 28, 2024
1 parent 3ffe75f commit ca4dea2
Show file tree
Hide file tree
Showing 8 changed files with 22 additions and 26 deletions.
7 changes: 3 additions & 4 deletions Core/NativeClient/Renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,7 @@ local Renderer = {

function Renderer:InitializeWithGLFW(nativeWindowHandle)
Renderer:CreateGraphicsContext(nativeWindowHandle)

-- Need to compute the preferred texture format first
self.backingSurface:UpdateConfiguration()
Renderer:CompileMaterials(self.backingSurface.preferredTextureFormat)
Renderer:CompileMaterials(self.backingSurface.textureFormat)

Renderer:CreateUniformBuffers()

Expand Down Expand Up @@ -134,6 +131,8 @@ function Renderer:CreateGraphicsContext(nativeWindowHandle)

printf("Creating depth buffer with texture dimensions %d x %d", viewportWidth, viewportHeight)
self.depthStencilTexture = DepthStencilTexture(device, viewportWidth, viewportHeight)

self.backingSurface:UpdateConfiguration()
end

function Renderer:CompileMaterials(outputTextureFormat)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ function GroundMeshDrawingPipeline:Construct(wgpuDeviceHandle, textureFormatID)
dstFactor = ffi.C.WGPUBlendFactor_OneMinusSrcAlpha,
operation = ffi.C.WGPUBlendOperation_Add,
},
alpha = {
srcFactor = ffi.C.WGPUBlendFactor_One,
dstFactor = ffi.C.WGPUBlendFactor_OneMinusSrcAlpha,
operation = ffi.C.WGPUBlendOperation_Add,
},
}),
writeMask = ffi.C.WGPUColorWriteMask_All,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ function WaterPlaneDrawingPipeline:Construct(wgpuDeviceHandle, textureFormatID)
dstFactor = ffi.C.WGPUBlendFactor_OneMinusSrcAlpha,
operation = ffi.C.WGPUBlendOperation_Add,
},
alpha = {
srcFactor = ffi.C.WGPUBlendFactor_One,
dstFactor = ffi.C.WGPUBlendFactor_OneMinusSrcAlpha,
operation = ffi.C.WGPUBlendOperation_Add,
},
}),
writeMask = ffi.C.WGPUColorWriteMask_All,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ local Device = require("Core.NativeClient.WebGPU.Device")
local format = string.format

local ScreenshotCaptureTexture = {
OUTPUT_TEXTURE_FORMAT = ffi.C.WGPUTextureFormat_RGBA8UnormSrgb,
OUTPUT_TEXTURE_FORMAT = ffi.C.WGPUTextureFormat_RGBA8Unorm,
}

function ScreenshotCaptureTexture:Construct(wgpuDevice, width, height)
Expand Down
7 changes: 1 addition & 6 deletions Core/NativeClient/WebGPU/Shaders/TerrainGeometryShader.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,5 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {

// Should be a no-op if fog is disabled, since the fogFactor would be zero
let foggedColor = mix(fragmentColor.rgb, uPerSceneData.fogColor.rgb, in.fogFactor);

// Gamma-correction:
// WebGPU assumes that the colors output by the fragment shader are given in linear space
// When setting the surface format to BGRA8UnormSrgb it performs a linear to sRGB conversion
let gammaCorrectedColor = pow(foggedColor.rgb, vec3f(2.2));
return vec4f(gammaCorrectedColor, diffuseTextureColor.a + DEBUG_ALPHA_OFFSET);
return vec4f(foggedColor.rgb, diffuseTextureColor.a + DEBUG_ALPHA_OFFSET);
}
6 changes: 1 addition & 5 deletions Core/NativeClient/WebGPU/Shaders/WaterSurfaceShader.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,5 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
let materialColor = vec4f(uMaterialInstanceData.diffuseRed, uMaterialInstanceData.diffuseGreen, uMaterialInstanceData.diffuseBlue, uMaterialInstanceData.materialOpacity);
let finalColor = in.color * diffuseTextureColor.rgb * materialColor.rgb;

// Gamma-correction:
// WebGPU assumes that the colors output by the fragment shader are given in linear space
// When setting the surface format to BGRA8UnormSrgb it performs a linear to sRGB conversion
let gammaCorrectedColor = pow(finalColor.rgb, vec3f(2.2));
return vec4f(gammaCorrectedColor, diffuseTextureColor.a * materialColor.a + DEBUG_ALPHA_OFFSET);
return vec4f(finalColor.rgb, diffuseTextureColor.a * materialColor.a + DEBUG_ALPHA_OFFSET );
}
14 changes: 5 additions & 9 deletions Core/NativeClient/WebGPU/Surface.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ local Surface = {
BACKING_SURFACE_OUTDATED = "The backing surface is outdated and can't be used (window resized or moved?)",
BACKING_SURFACE_TIMEOUT = "The backing surface couldn't be accessed in time (CPU or GPU too busy?)",
},
textureFormat = ffi.C.WGPUTextureFormat_BGRA8Unorm,
}

function Surface:Construct(wgpuInstance, wgpuAdapter, wgpuDevice, glfwWindow)
Expand All @@ -33,23 +34,16 @@ function Surface:Construct(wgpuInstance, wgpuAdapter, wgpuDevice, glfwWindow)
end

function Surface:UpdateConfiguration()
local preferredTextureFormat = webgpu.bindings.wgpu_surface_get_preferred_format(self.wgpuSurface, self.wgpuAdapter)
self.preferredTextureFormat = preferredTextureFormat -- Required to create the render pipeline
assert(
preferredTextureFormat == ffi.C.WGPUTextureFormat_BGRA8UnormSrgb,
"Only sRGB texture formats are currently supported"
)

local textureViewDescriptor = self.wgpuTextureViewDescriptor
textureViewDescriptor.dimension = ffi.C.WGPUTextureViewDimension_2D
textureViewDescriptor.format = preferredTextureFormat
textureViewDescriptor.format = self.textureFormat
textureViewDescriptor.mipLevelCount = 1
textureViewDescriptor.arrayLayerCount = 1
textureViewDescriptor.aspect = ffi.C.WGPUTextureAspect_All

local surfaceConfiguration = self.wgpuSurfaceConfiguration
surfaceConfiguration.device = self.wgpuDevice
surfaceConfiguration.format = preferredTextureFormat
surfaceConfiguration.format = self.textureFormat
surfaceConfiguration.usage = ffi.C.WGPUTextureUsage_RenderAttachment

-- The underlying framebuffer may be different if DPI scaling is applied, but let's ignore that for now
Expand All @@ -62,12 +56,14 @@ function Surface:UpdateConfiguration()

webgpu.bindings.wgpu_surface_configure(self.wgpuSurface, surfaceConfiguration)

local preferredTextureFormat = webgpu.bindings.wgpu_surface_get_preferred_format(self.wgpuSurface, self.wgpuAdapter)
printf(
"Surface configuration changed: Frame buffer size is now %dx%d (preferred texture format: %d)",
viewportWidth,
viewportHeight,
tonumber(preferredTextureFormat)
)
self.preferredTextureFormat = preferredTextureFormat
end

function Surface:AcquireTextureView()
Expand Down
2 changes: 1 addition & 1 deletion Core/NativeClient/WebGPU/Texture.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ local new = ffi.new
local math_floor = math.floor

local Texture = {
DEFAULT_TEXTURE_FORMAT = ffi.C.WGPUTextureFormat_RGBA8Unorm, -- TBD: WGPUTextureFormat_BGRA8UnormSrgb ?
DEFAULT_TEXTURE_FORMAT = ffi.C.WGPUTextureFormat_RGBA8Unorm,
MAX_TEXTURE_DIMENSION = 4096,
ERROR_DIMENSIONS_NOT_POWER_OF_TWO = "Texture dimensions should always be a power of two",
ERROR_DIMENSIONS_EXCEEDING_LIMIT = "Texture dimensions must not exceed the configured GPU limit",
Expand Down

0 comments on commit ca4dea2

Please sign in to comment.