Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discard transparent background pixels in the fragment shader instead of a separate CPU pre-processing step #369

Merged
merged 1 commit into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 0 additions & 47 deletions Core/NativeClient/Renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -820,54 +820,7 @@ function Renderer:CreateDummyTexture()
self.dummyTextureMaterial = dummyTextureMaterial
end

-- Should probably move this to the runtime for efficiency (needs benchmarking)
-- Also... should use string buffers everywhere, but currently the API still uses strings
local function discardTransparentPixels(rgbaImageBytes, width, height, discardRanges)
local OFFSET_RED = 0
local OFFSET_GREEN = 1
local OFFSET_BLUE = 2
local OFFSET_ALPHA = 3

local DISCARD_MIN_RED = discardRanges.red.from
local DISCARD_MAX_RED = discardRanges.red.to
local DISCARD_MIN_GREEN = discardRanges.green.from
local DISCARD_MAX_GREEN = discardRanges.green.to
local DISCARD_MIN_BLUE = discardRanges.blue.from
local DISCARD_MAX_BLUE = discardRanges.blue.to

local rgbaImageBuffer = buffer.new(width * height * 4):put(rgbaImageBytes)
local pixelArray, bufferSize = rgbaImageBuffer:ref()
assert(bufferSize == width * height * 4)

for pixelStartOffset = 0, width * height * 4, 4 do
local red = pixelArray[pixelStartOffset + OFFSET_RED]
local green = pixelArray[pixelStartOffset + OFFSET_GREEN]
local blue = pixelArray[pixelStartOffset + OFFSET_BLUE]

local isRedWithinDiscardedRange = (red >= DISCARD_MIN_RED and red <= DISCARD_MAX_RED)
local isGreenWithinDiscardedRange = (green >= DISCARD_MIN_GREEN and green <= DISCARD_MAX_GREEN)
local isBlueWithinDiscardedRange = (blue >= DISCARD_MIN_BLUE and blue <= DISCARD_MAX_BLUE)
local shouldDiscardPixel = isRedWithinDiscardedRange
and isGreenWithinDiscardedRange
and isBlueWithinDiscardedRange

if shouldDiscardPixel then
pixelArray[pixelStartOffset + OFFSET_ALPHA] = 0
end
end

return rgbaImageBuffer:tostring()
end

function Renderer:CreateTextureFromImage(rgbaImageBytes, width, height)
local inclusiveTransparentPixelRanges = {
red = { from = 254, to = 255 },
green = { from = 0, to = 3 },
blue = { from = 254, to = 255 },
}
-- This is currently NOT in-place and so incurs unnecessary copy overhead (optimize later)
rgbaImageBytes = discardTransparentPixels(rgbaImageBytes, width, height, inclusiveTransparentPixelRanges)

local texture = Texture(self.wgpuDevice, rgbaImageBytes, width, height)
Renderer:UploadTextureImage(texture)

Expand Down
11 changes: 10 additions & 1 deletion Core/NativeClient/WebGPU/Shaders/TerrainGeometryShader.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,23 @@ fn vs_main(in: VertexInput) -> VertexOutput {
return out;
}

// Magenta background pixels should be discarded (but pre-processing on the CPU is expensive)
fn isTransparentBackgroundPixel(diffuseTextureColor : vec4f) -> bool {
return (diffuseTextureColor.r >= 254/255 && diffuseTextureColor.g <= 3/255 && diffuseTextureColor.b >= 254/255);
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
let textureCoords = in.diffuseTextureCoords;
let diffuseTextureColor = textureSample(diffuseTexture, diffuseTextureSampler, textureCoords);
var diffuseTextureColor = textureSample(diffuseTexture, diffuseTextureSampler, textureCoords);
let normal = normalize(in.surfaceNormal);
let sunlightColor = uPerSceneData.directionalLightColor.rgb;
let ambientColor = uPerSceneData.ambientLight.rgb;

if (isTransparentBackgroundPixel(diffuseTextureColor)) {
diffuseTextureColor.a = 0.0;
}

let lightmapTexCoords = in.lightmapTextureCoords;
var lightmapTextureColor = textureSample(lightmapTexture, lightmapTextureSampler, lightmapTexCoords);

Expand Down
95 changes: 0 additions & 95 deletions Tests/NativeClient/Renderer.spec.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
local etrace = require("etrace")
local ffi = require("ffi")
local miniz = require("miniz")

local Plane = require("Core.NativeClient.DebugDraw.Plane")
local Mesh = require("Core.NativeClient.WebGPU.Mesh")
Expand Down Expand Up @@ -57,100 +56,6 @@ describe("Renderer", function()
assertEquals(tonumber(events[1].payload.dataLayout.bytesPerRow), width * 4)
assertEquals(tonumber(events[1].payload.dataLayout.rowsPerImage), height)
end)

it("should discard transparent background pixels in the final image", function()
local IMAGE_WIDTH, IMAGE_HEIGHT = 256, 256
local transparentColors = {
"\254\000\254\255",
"\254\000\255\255",
"\254\001\254\255",
"\254\001\255\255",
"\254\002\254\255",
"\254\002\255\255",
"\254\003\254\255",
"\254\003\255\255",
"\255\000\254\255",
"\255\000\255\255",
"\255\001\254\255",
"\255\001\255\255",
"\255\002\254\255",
"\255\002\255\255",
"\255\003\254\255",
"\255\003\255\255",
}

local function createImageBytes(pixel)
return string.rep(pixel, IMAGE_WIDTH * IMAGE_HEIGHT)
end

local transparentImages = {}
for index = 1, #transparentColors, 1 do
table.insert(transparentImages, createImageBytes(transparentColors[index]))
end

-- Only discard alpha to save on unnecessary writes
local expectedPixelValues = {
"\254\000\254\000",
"\254\000\255\000",
"\254\001\254\000",
"\254\001\255\000",
"\254\002\254\000",
"\254\002\255\000",
"\254\003\254\000",
"\254\003\255\000",
"\255\000\254\000",
"\255\000\255\000",
"\255\001\254\000",
"\255\001\255\000",
"\255\002\254\000",
"\255\002\255\000",
"\255\003\254\000",
"\255\003\255\000",
}

local expectedImages = {}
for index = 1, #expectedPixelValues, 1 do
table.insert(expectedImages, createImageBytes(expectedPixelValues[index]))
end

local function assertRendererDiscardsTransparentPixelsOnUpload(rgbaImageBytes, expectedResult)
etrace.clear()

Renderer:CreateTextureFromImage(rgbaImageBytes, IMAGE_WIDTH, IMAGE_HEIGHT)
local events = etrace.filter("GPU_TEXTURE_WRITE")
local payload = events[1].payload

assertEquals(events[1].name, "GPU_TEXTURE_WRITE")
assertEquals(payload.dataSize, IMAGE_WIDTH * IMAGE_HEIGHT * 4)
assertEquals(payload.writeSize.width, IMAGE_WIDTH)
assertEquals(payload.writeSize.height, IMAGE_HEIGHT)
assertEquals(payload.writeSize.depthOrArrayLayers, 1)

assertEquals(#payload.data, #rgbaImageBytes) -- More readable errors in case of failure
assertEquals(miniz.crc32(payload.data), miniz.crc32(expectedResult))

assertEquals(tonumber(events[1].payload.dataLayout.offset), 0)
assertEquals(tonumber(events[1].payload.dataLayout.bytesPerRow), IMAGE_WIDTH * 4)
assertEquals(tonumber(events[1].payload.dataLayout.rowsPerImage), IMAGE_HEIGHT)
end

assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[1], expectedImages[1])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[2], expectedImages[2])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[3], expectedImages[3])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[4], expectedImages[4])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[5], expectedImages[5])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[6], expectedImages[6])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[7], expectedImages[7])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[8], expectedImages[8])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[9], expectedImages[9])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[10], expectedImages[10])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[11], expectedImages[11])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[12], expectedImages[12])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[13], expectedImages[13])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[14], expectedImages[14])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[15], expectedImages[15])
assertRendererDiscardsTransparentPixelsOnUpload(transparentImages[16], expectedImages[16])
end)
end)

describe("UploadMeshGeometry", function()
Expand Down
Loading