Qt Quick 3D - User Passes Example

Demonstrates creating custom render passes in Qt Quick 3D.

A 3D scene rendered using custom user passes

}

The User Passes example demonstrates how to create custom render passes in Qt Quick 3D. It shows a simple 3D scene rendered using multiple user-defined render passes to achieve a simplined deferred lighting technique.

Disabling internal render passes

By default, Qt Quick 3D uses a set of internal render passes to render the 3D scene. Sometimes you may want to disable these internal passes and implement your own rendering pipeline using user-defined render passes.

To disable the internal render passes, set the renderOverrides property of the View3D to View3D.DisableInternalPasses.

 View3D {
     id: view3D
     anchors.fill: parent
     renderOverrides: View3D.DisableInternalPasses
     environment: SceneEnvironment {
         lightProbe: Texture {
             textureData: ProceduralSkyTextureData {
             }
         }
         backgroundMode: SceneEnvironment.SkyBox
     }

If you disable the internal render passes, you will need to provide the result of the main color pass for the View3D to be able to display anything on the screen.

Geometry Buffer Pass

In this example, the first custom render pass is a geometry buffer (G-buffer) pass that renders the scene geometry into multiple render targets, storing different material properties in each target. The provided example is a subset of the full material properties provided by Qt Quick 3D materials, focusing on the properties needed for a basic deferred lighting implementation.

Our RenderPass is defined in GBufferPass.qml:

 RenderPass {
     id: gbufferPass
     clearColor: Qt.rgba(0.0, 0.0, 0.0, 0.0)

     property alias layerMask: filter.layerMask
     required property RenderPassTexture depthTexture

     RenderPassTexture {
         id: gbuffer0
         format: RenderPassTexture.RGBA16F
         // rgb: baseColor (linear), a: metalness
     }

     RenderPassTexture {
         id: gbuffer1
         format: RenderPassTexture.RGBA16F
         // rgb: normal, a: roughness
     }

     RenderPassTexture {
         id: gbuffer2
         format: RenderPassTexture.RGBA16F
         // rgb: emissive, a: ao/spare
     }

     commands: [
         ColorAttachment { target: gbuffer0; name: "GBUFFER0" },
         ColorAttachment { target: gbuffer1; name: "GBUFFER1" },
         ColorAttachment { target: gbuffer2; name: "GBUFFER2" },
         DepthTextureAttachment { target: gbufferPass.depthTexture },
         RenderablesFilter {
             id: filter
             renderableTypes: RenderablesFilter.Opaque
         }
     ]

     materialMode: RenderPass.AugmentMaterial
     augmentShader: "gbuffer_augment.glsl"
 }

It defines 3 color attachments and 1 depth attachment. The pass requires 3 textures which are defined as RenderPassTexture objects inside the RenderPass. These 3 RenderPassTextures are used as the targets for the color attachments of the pass, and the depth attachment uses a depth texture provided from outside the pass.

The RenderPass itself is set to AugmentMaterial mode, which means that it will augment the materials of the rendered objects with additional shader code. The augment shader is provided in the gbuffer_augment.glsl file, which outputs the required material properties to the multiple render targets.

 void MAIN_FRAGMENT_AUGMENT()
 {
     vec3 baseColor   = BASE_COLOR.rgb;
     float metalness  = METALNESS;
     float roughness  = ROUGHNESS;
     vec3 worldNormal = normalize(WORLD_NORMAL);

     // GBuffer 0: albedo + metalness
     GBUFFER0 = vec4(baseColor, metalness);

     // GBuffer 1: normal (encoded to 0..1) + roughness
     GBUFFER1 = vec4(worldNormal * 0.5 + 0.5, roughness);

     // GBuffer 2: world position
     GBUFFER2 = vec4(qt_varWorldPos, 1.0);
 }

Here you see how the base color, metalness, worldNormal, roughness and world position are stored into the 3 color attachments of the G-buffer.

To actually use the G-buffer pass in the rendering pipeline, we need to create an instance of it in Main.qml, and provide the required depth texture:

 RenderPassTexture {
     id: mainDepthStencilTexture
     format: RenderPassTexture.Depth24Stencil8
 }
 GBufferPass {
     id: gbufferPass
     layerMask: ContentLayer.Layer0 | ContentLayer.Layer1
     depthTexture: mainDepthStencilTexture
 }

 RenderOutputProvider {
     id: gbuffer0Provider
     textureSource: RenderOutputProvider.UserPassTexture
     renderPass: gbufferPass
     attachmentSelector: RenderOutputProvider.Attachment0
 }

 RenderOutputProvider {
     id: gbuffer1Provider
     textureSource: RenderOutputProvider.UserPassTexture
     renderPass: gbufferPass
     attachmentSelector: RenderOutputProvider.Attachment1
 }

 RenderOutputProvider {
     id: gbuffer2Provider
     textureSource: RenderOutputProvider.UserPassTexture
     renderPass: gbufferPass
     attachmentSelector: RenderOutputProvider.Attachment2
 }

Three instances of RenderPassTexture are created to provide references to the rendered G-buffer textures, which will be used in the subsequent lighting pass.

The layerMask property of the G-buffer pass is set to only render objects that are on ContentLayer.Layer0 and ContentLayer.Layer1. This allows us to control which objects are rendered in the G-buffer pass by setting their layers property accordingly.

Deferred Lighting Pass

The second custom render pass is a deferred lighting pass that uses the data stored in the G-buffer to compute lighting for the scene. This pass renders a full-screen quad that samples the G-buffer textures and applies lighting calculations in the fragment shader.

The deferred lighting pass is defined in Main.qml as follows:

 Model {
     id: deferredLightingQuad
     layers: ContentLayer.Layer13
     geometry: PlaneGeometry {
         // geometry doesn't matter, just need 4 verts
         plane: PlaneGeometry.XY
     }
     materials: [
         CustomMaterial {
             id: lightingPassMaterial
             property TextureInput gbuffer0: TextureInput {
                 enabled: true
                 texture: Texture {
                     textureProvider: gbuffer0Provider
                 }
             }
             property TextureInput gbuffer1: TextureInput {
                 enabled: true
                 texture: Texture {
                     textureProvider: gbuffer1Provider
                 }
             }
             property TextureInput gbuffer2: TextureInput {
                 enabled: true
                 texture: Texture {
                     textureProvider: gbuffer2Provider
                 }
             }
             shadingMode: CustomMaterial.Unshaded
             fragmentShader: "lighting.frag"
             vertexShader: "lighting.vert"
         }
     ]
 }

 RenderPass {
     id: deferredLightingPass

     readonly property Texture gbuffer0: Texture { textureProvider: gbuffer0Provider }
     readonly property Texture gbuffer1: Texture { textureProvider: gbuffer1Provider }
     readonly property Texture gbuffer2: Texture { textureProvider: gbuffer2Provider }

     materialMode: RenderPass.OriginalMaterial

     commands: [
         ColorAttachment { target: mainColorTexture },
         DepthStencilAttachment {},
         RenderablesFilter { layerMask: ContentLayer.Layer13 }
     ]

 }

The deferredLightingQuad Model uses a CustomMaterial with vertex and fragment shaders defined in lighting.vert and lighting.frag files. The CustomMaterial has three TextureInput properties that are used to pass the G-buffer textures to the shaders.

The deferredLightingPass RenderPass renders the full-screen quad to the main color texture of the View3D. It uses a RenderablesFilter to only render objects on ContentLayer.Layer13, which is where the deferredLightingQuad Model is placed.

Rendering to the screen

Finally, to display the result of our custom render passes on the screen, we need to ensure that the View3D's main color texture is updated with the result of the deferred lighting pass.

 SimpleQuadRenderer {
     texture: Texture {
         textureProvider: mainColorTextureProvider
     }
 }

 RenderPassTexture {
     id: mainColorTexture
     format: RenderPassTexture.RGBA16F
 }

 RenderOutputProvider {
     id: mainColorTextureProvider
     textureSource: RenderOutputProvider.UserPassTexture
     renderPass: deferredLightingPass
     attachmentSelector: RenderOutputProvider.Attachment0
 }

Here the SimpleQuadRenderer is used to render the main color texture of the View3D using the texture provided by the deferredLightingPass.

This is a highly simplified example of using custom user passes in Qt Quick 3D.

Example project @ code.qt.io