Skip to content

Latest commit

 

History

History
315 lines (286 loc) · 10.3 KB

README.md

File metadata and controls

315 lines (286 loc) · 10.3 KB

Unity-WebGPU-PBR-Maps-Generator

Helps generate PBR maps from a base/diffuse map depending on the workflow chosen. This project demonstrates the power of WebGPU by implementing a Physically Based Rendering (PBR) map generator that runs efficiently in web browsers. It showcases the use of compute shaders for GPU-accelerated texture processing, providing a significant performance boost over traditional CPU-based methods. The output of course is not very fancy, as we don't have access to the real height information of the texture, and we're just making a guess based on the color information, which could be a hit or miss. But it's a good starting point to prototype some PBR textures quickly!

You can try out the project here- https://makra.wtf/Unity-WebGPU-PBR-Maps-Generator/.
The page might take a while to load for the first time but once it's cached, it should load quickly on subsequent visits.

pbr_intro.mp4

Features

  • Generate various PBR maps from a single base texture:
    • Height Map
    • Normal Map
    • Ambient Occlusion (AO) Map
    • Roughness Map
    • Metallic Map
    • Specular Map
    • Glossiness Map
  • Choose between Metallic-Roughness and Specular-Glossiness workflows.
  • GPU acceleration using compute shaders using Unity's new WebGPU backend.
  • CPU fallback for devices without GPU that supports 64 threads per block.
  • Real-time preview of generated maps along with the option to download them.
Base Height Normal AO Metallic Roughness Specular Glossiness

PBR maps generated from a single base texture using the tool

WebGPU advantage

Here's a comparison of CPU vs GPU methods for generating a normal map:

CPU Method

private static Texture2D CPUConvertToNormalMap(Texture2D heightMap)
{
    Texture2D normalMap = new Texture2D(heightMap.width, heightMap.height, TextureFormat.RGBA32, false);
    for (int y = 0; y < heightMap.height; y++)
    {
        for (int x = 0; x < heightMap.width; x++)
        {
            float left = GetPixelHeight(heightMap, x - 1, y);
            float right = GetPixelHeight(heightMap, x + 1, y);
            float top = GetPixelHeight(heightMap, x, y - 1);
            float bottom = GetPixelHeight(heightMap, x, y + 1);
            
            Vector3 normal = new Vector3(left - right, bottom - top, 1).normalized;
            normal = normal * 0.5f + Vector3.one * 0.5f;
            
            normalMap.SetPixel(x, y, new Color(normal.x, normal.y, normal.z, 1));
        }
    }
    normalMap.Apply();
    return normalMap;
}

GPU Method

#pragma kernel CSMain

Texture2D<float> HeightMap;
RWTexture2D<float4> NormalMap;
uint2 TextureSize;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    if (id.x >= TextureSize.x || id.y >= TextureSize.y)
        return;

    float left = HeightMap[uint2(max(id.x - 1, 0), id.y)];
    float right = HeightMap[uint2(min(id.x + 1, TextureSize.x - 1), id.y)];
    float top = HeightMap[uint2(id.x, max(id.y - 1, 0))];
    float bottom = HeightMap[uint2(id.x, min(id.y + 1, TextureSize.y - 1))];
    
    float3 normal = normalize(float3(left - right, bottom - top, 1));
    normal = normal * 0.5 + 0.5;
    
    NormalMap[id.xy] = float4(normal, 1);
}

Using compute shaders we create 8x8 threads per block, which is the maximum supported by the WebGPU backend. This allows us to process 64 pixels in parallel, providing a significant performance boost over the CPU method.

These are the results of the performance comparison using a system with an Intel Core i9-13900HX CPU and an NVIDIA GeForce RTX 4070 GPU and 32 GB of RAM:

pbr_compare.mp4
Map Type Resolution CPU Time (ms) GPU Time (ms) Speedup Factor
Height 512x512 350 20 17.5x
1024x1024 1500 50 30x
2048x2048 6200 150 41.3x
Normal 512x512 500 30 16.7x
1024x1024 2000 80 25x
2048x2048 8000 250 32x
AO 512x512 450 20 22.5x
1024x1024 1800 60 30x
2048x2048 7200 180 40x
Roughness 512x512 550 30 18.3x
1024x1024 2200 70 31.4x
2048x2048 8800 220 40x
Metallic 512x512 400 20 20x
1024x1024 1600 50 32x
2048x2048 6400 160 40x
Specular 512x512 420 20 21x
1024x1024 1700 60 28.3x
2048x2048 6800 180 37.8x
Glossiness 512x512 600 40 15x
1024x1024 2400 90 26.7x
2048x2048 9600 280 34.3x
plt

As we can see from the table and graph, the GPU method consistently outperforms the CPU method, with the performance gap widening as the texture resolution increases. This demonstrates the scalability and efficiency of compute shaders for texture processing tasks.

WebGPU Limitations & Solutions

One key limitation of WebGPU is the lack of support for synchronous GPU readback. To address this, we use AsyncGPUReadback instead of the traditional ReadPixels method:

private void ReadbackTexture(Texture texture, Action<Texture2D> callback)
{
    AsyncGPUReadback.Request(texture, 0, readback =>
    {
        if (readback.hasError)
        {
            Debug.LogError("GPU readback error detected.");
            return;
        }
        Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false);
        texture2D.LoadRawTextureData(readback.GetData<byte>());
        texture2D.Apply();
        callback(texture2D);
    });
}

This asynchronous approach ensures compatibility with WebGPU while maintaining efficient GPU-to-CPU data transfer. You can see an example implementation here.

Installation

  • Clone the repository or download the .unitypackage from here.
  • Open/Import the project in Unity 2023.3 or later.
  • If you're using the .unitypackage, make sure to create a project using the Built-In Render Pipeline and import TextMeshPro.
  • Make sure to enable the WebGPU backend to take advantage of GPU acceleration. Instructions here.

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Currently working on-

  • Better algorithms for generating the maps that give better results.
  • Incorporating deep learning models to estimate the height information from the color information instead of just doing a greyscale conversion.

License

MIT