Skip to content

Commit

Permalink
Merge pull request #369 from RagnarokResearchLab/gpu-transparency-color
Browse files Browse the repository at this point in the history
Discard transparent background pixels in the fragment shader instead of a separate CPU pre-processing step
  • Loading branch information
rdw-software authored Feb 6, 2024
2 parents 8ff8e22 + 3020229 commit 9aa1583
Show file tree
Hide file tree
Showing 3 changed files with 10 additions and 143 deletions.
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

0 comments on commit 9aa1583

Please sign in to comment.