W3cubDocs

/Qt

CustomMaterial QML Type

Base component for creating custom materials used to shade models. More...

Import Statement: import QtQuick3D
Inherits:

Material

Properties

Detailed Description

The custom material allows using custom shader code for a material, enabling programmability on graphics shader level. A vertex, fragment, or both shaders can be provided. The vertexShader and fragmentShader properties are URLs, referencing files containing shader snippets, and work very similarly to ShaderEffect or Image.source. Only the file and qrc schemes are supported with custom materials. It is also possible to omit the file scheme, allowing to specify a relative path in a convenient way. Such a path is resolved relative to the component's (the .qml file's) location.

For a getting started guide to custom materials, see the page Programmable Materials, Effects, Geometry, and Texture data.

Introduction

Consider the following versions of the same scene. On the left, the cylinder is using a built-in, non-programmable material. Such materials are configurable through a wide range of properties, but there is no further control given over the shaders that are generated under the hood. On the right, the same cylinder is now associated with a CustomMaterial referencing application-provided vertex and fragment shader snippets. This allows inserting custom, application-specific logic into the vertex shader to transform the geometry, and to determine certain color properties in a custom manner in the fragment shader. As this is a shaded custom material, the cylinder still participates in the scene lighting normally.

View3D {
    anchors.fill: parent
    PerspectiveCamera {
        id: camera
        position: Qt.vector3d(0, 0, 600)
    }
    camera: camera
    DirectionalLight {
        position: Qt.vector3d(-500, 500, -100)
        color: Qt.rgba(0.2, 0.2, 0.2, 1.0)
        ambientColor: Qt.rgba(0.1, 0.1, 0.1, 1.0)
    }
    Model {
        source: "#Cylinder"
        eulerRotation: Qt.vector3d(30, 30, 0)
        scale: Qt.vector3d(1.5, 1.5, 1.5)
        materials: [
            DefaultMaterial {
                diffuseColor: Qt.rgba(0, 1, 0, 1)
            }
        ]
    }
}
View3D {
    anchors.fill: parent
    PerspectiveCamera {
        id: camera
        position: Qt.vector3d(0, 0, 600)
    }
    camera: camera
    DirectionalLight {
        position: Qt.vector3d(-500, 500, -100)
        color: Qt.rgba(0.2, 0.2, 0.2, 1.0)
        ambientColor: Qt.rgba(0.1, 0.1, 0.1, 1.0)
    }
    Model {
        source: "#Cylinder"
        eulerRotation: Qt.vector3d(30, 30, 0)
        scale: Qt.vector3d(1.5, 1.5, 1.5)
        materials: [
            CustomMaterial {
                vertexShader: "material.vert"
                fragmentShader: "material.frag"
                property real uTime
                property real uAmplitude: 50
                NumberAnimation on uTime { from: 0; to: 100; duration: 10000; loops: -1 }
            }
        ]
    }
}

Let's assume that the shader snippets in material.vert and material.frag are the following:

void MAIN()
{
    VERTEX.x += sin(uTime + VERTEX.y) * uAmplitude;
}
void MAIN()
{
    BASE_COLOR = vec4(0.0, 1.0, 0.0, 1.0);
}

Notice how uTime and uAmplitude are properties of the CustomMaterial element. They can change values and get animated normally, the values will be exposed to the shaders automatically without any further action from the developer.

The result is a cylinder that animates its vertices:

Two flavors of custom materials

There are two main types of custom materials. This is specified by the shadingMode property. In unshaded custom materials the fragment shader outputs a single vec4 color, ignoring lights, light probes, shadowing in the scene. In shaded materials the shader is expected to implement certain functions and work with built-in variables to take lighting and shadow contribution into account.

The default choice is typically a shaded material, this is reflected in the default value of the shadingMode property. This fits materials that needs to transform vertices or other incoming data from the geometry, or determine values like BASE_COLOR or EMISSIVE_COLOR in a custom manner, perhaps by sampling SCREEN_TEXTURE or DEPTH_TEXTURE, while still reciving light and shadow contributions from the scene. Additionally, such materials can also override and reimplement the equations used to calculate the contributions from directional, point, and other lights. The application-provided shader snippets are heavily amended by the Qt Quick 3D engine under the hood, in order to provide the features, such as lighting, the standard materials have.

Unshaded materials are useful when the object's appearance is determined completely by the custom shader code. The shaders for such materials receive minimal additions by the engine, and therefore it is completely up to the shader to determine the final fragment color. This gives more freedom, but also limits possiblities to integrate with other elements of the scene, such as lights.

Note: Shader code is always provided using Vulkan-style GLSL, regardless of the graphics API used by Qt at run time.

Note: The vertex and fragment shader code provided by the material are not full, complete GLSL shaders on their own. Rather, they provide a set of functions, which are then amended with further shader code by the engine.

Exposing data to the shaders

The dynamic properties of the CustomMaterial can be changed and animated using QML and Qt Quick facilities, and the values are exposed to the shaders automatically. This in practice is very similar ShaderEffect. The following list shows how properties are mapped:

  • bool, int, real -> bool, int, float
  • QColor, color -> vec4, and the color gets converted to linear, assuming sRGB space for the color value specified in QML. The built-in Qt colors, such as "green" are in sRGB color space as well, and the same conversion is performed for all color properties of DefaultMaterial and PrincipledMaterial, so this behavior of CustomMaterial matches those. Unlike Qt Quick, for Qt Quick 3D linearizing is essential as there will typically be tonemapping performed on the 3D scene.
  • QRect, QRectF, rect -> vec4
  • QPoint, QPointF, point, QSize, QSizeF, size -> vec2
  • QVector2D, vector2d -> vec2
  • QVector3D, vector3d -> vec3
  • QVector4D, vector4d -> vec4
  • QMatrix4x4, matrix4x4 -> mat4
  • QQuaternion, quaternion -> vec4, scalar value is w
  • TextureInput -> sampler2D - Textures referencing image files and Qt Quick item layers are both supported. Setting the enabled property to false leads to exposing a dummy texture to the shader, meaning the shaders are still functional but will sample a texture with opaque black image content. Pay attention to the fact that properties for samplers must always reference a TextureInput object, not a Texture directly. When it comes to the Texture properties, the source, tiling, and filtering related ones are the only ones that are taken into account implicitly with custom materials, as the rest (such as, UV transformations) is up to the custom shaders to implement as they see fit.

Note: When a uniform referenced in the shader code does not have a corresponding property, it will cause a shader compilation error when processing the material at run time. There are some exceptions to this, such as, sampler uniforms, that get a dummy texture bound when no corresponding QML property is present, but as a general rule, all uniforms and samplers must have a corresponding property declared in the CustomMaterial object.

Unshaded custom materials

The following is an example of an unshaded custom material.

CustomMaterial {
    // These properties are automatically exposed to the shaders
    property real time: 0.0
    property real amplitude: 5.0
    property real alpha: 1.0
    property TextureInput tex: TextureInput {
        enabled: true
        texture: Texture { source: "image.png" }
    }

    shadingMode: CustomMaterial.Unshaded
    sourceBlend: alpha < 1.0 ? CustomMaterial.SrcAlpha : CustomMaterial.NoBlend
    destinationBlend: alpha < 1.0 ? CustomMaterial.OneMinusSrcAlpha : CustomMaterial.NoBlend
    cullMode: CustomMaterial.BackFaceCulling

    vertexShader: "customshader.vert"
    fragmentShader: "customshader.frag"
}

With the above example, the unshaded vertex and fragment shaders snippets could look like the following. Note how the shaders do not, and must not, declare uniforms or vertex inputs as that is taken care of by Qt when assembling the final shader code.

VARYING vec3 pos;
VARYING vec2 texcoord;

void MAIN()
{
    pos = VERTEX;
    pos.x += sin(time * 4.0 + pos.y) * amplitude;
    texcoord = UV0;
    POSITION = MODELVIEWPROJECTION_MATRIX * vec4(pos, 1.0);
}
VARYING vec3 pos;
VARYING vec2 texcoord;

void MAIN()
{
    vec4 c = texture(tex, texcoord);
    FRAGCOLOR = vec4(pos.x * 0.02, pos.y * 0.02, pos.z * 0.02, alpha) * c;
}

The following special, uppercase keywords are available:

  • MAIN -> the name of the entry point in the vertex or fragment shader snippet must always be MAIN. Providing this function is mandatory in shader snippets for unshaded custom materials.
  • VARYING -> declares an output from the vertex shader or an input to the fragment shader
  • POSITION -> vec4, the output from the vertex shader
  • FRAGCOLOR -> vec4, the output from the fragment shader. Available only for unshaded custom materials.
  • VERTEX -> vec3, the vertex position in the vertex shader.
  • NORMAL -> vec3, the vertex normal in the vertex shader. When the mesh for the associated model does not provide normals, the value is vec3(0.0).
  • UV0 -> vec2, the first set of texture coordinates in the vertex shader. When the mesh for the associated model does not provide texture coordinates, the value is vec2(0.0).
  • UV1 -> vec2, the second set of texture coordinates in the vertex shader. When the mesh for the associated model does not provide a second set of texture coordinates, the value is vec2(0.0).
  • COLOR -> vec4, the vertex color in the vertex shader. When the mesh for the associated model does not provide per-vertex colors, the value is vec4(1.0).
  • TANGENT -> vec3, tangent in the vertex shader. When the mesh for the associated model does not provide tangent data, the value is vec3(0.0).
  • BINORMAL -> vec3, binormal in the vertex shader. When the mesh for the associated model does not provide binormal data, the value is vec3(0.0).
  • JOINTS -> ivec4, joint indexes in the vertex shader. When the mesh for the associated model does not provide joint indexes data, the value is ivec4(0).
  • WEIGHTS -> vec4, joint weights in the vertex shader. When the mesh for the associated model does not provide joint weights data, the value is vec4(0.0).
  • MORPH_POSITIONn -> vec3, the nth morph target position in the vertex shader. n's range is from 0 to 7. The associated model should provide proper data. For safety, the user can check defined(QT_MORPH_IN_POSITIONn) before use it.
  • MORPH_NORMALn -> vec3, the nth morph target normal in the vertex shader. n's range is from 0 to 4. The associated model should provide proper data. For safety, the user can check defined(QT_MORPH_IN_NORMALn) before use it.
  • MORPH_TANGENTn -> vec3, the nth morph target tangent in the vertex shader. n's range is from 0 to 1. The associated model should provide proper data. For safety, the user can check defined(QT_MORPH_IN_TANGENTn) before use it.
  • MORPH_BINORMALn -> vec3, the nth morph target binormal in the vertex shader. n's range is from 0 to 1. The associated model should provide proper data. For safety, the user can check defined(QT_MORPH_IN_BINORMALn) before use it.
  • MODELVIEWPROJECTION_MATRIX -> mat4, the model-view-projection matrix. Projection matrices always follow OpenGL conventions, with a baked-in transformation for the Y axis direction and clip depth, depending on the graphics API used at run time.
  • VIEWPROJECTION_MATRIX -> mat4, the view-projection matrix
  • PROJECTION_MATRIX -> mat4, the projection matrix
  • INVERSE_PROJECTION_MATRIX -> mat4, the inverse projection matrix
  • VIEW_MATRIX -> mat4, the view (camera) matrix
  • MODEL_MATRIX -> mat4, the model (world) matrix
  • NORMAL_MATRIX -> mat3, the normal matrix (the transpose of the inverse of the top-left 3x3 part of the model matrix)
  • BONE_TRANSFORMS -> mat4[], the array of the model's bone matrixes
  • BONE_NORMAL_TRANSFORMS -> mat3[], the array of the model's bone normal matrixes (the transpose of the inverse of the top-left 3x3 part of the each bone matrixes)
  • MORPH_WEIGHTS -> float[], the array of the morph weights. The associated model should provide proper data. For safety, QT_MORPH_MAX_COUNT is defined to the size of this array.
  • CAMERA_POSITION -> vec3, the camera position in world space
  • CAMERA_DIRECTION -> vec3, the camera direction vector
  • CAMERA_PROPERTIES -> vec2, the near and far clip values for the camera
  • POINT_SIZE -> float, writable in the vertex shader only. When rendering geometry with a topology of points, the custom vertex shader must set this to either 1.0 or another value, both in shaded and unshaded custom materials. See PrincipledMaterial::pointSize for further notes on support for sizes other than 1.

Shaded custom materials

A shaded material augments the shader code that would be generated by a PrincipledMaterial. Unlike unshaded materials, that provide almost all logic for the vertex and fragment shader main functions on their own, preventing adding generated code for lighting, shadowing, global illumination, etc., shaded materials let shader generation happen normally, as if the CustomMaterial was a PrincipledMaterial. The vertex and fragment shader snippets are expected to provide optional functions that are then invoked at certain points, giving them the possibility to customize the colors and other values that are then used for calculating lighting and the final fragment color.

Rather than implementing just a MAIN function, the fragment shader for a shaded custom material can implement multiple functions. All functions, including MAIN, are optional to implement in shaded custom materials. An empty shader snippet, or, even, not specifying the vertexShader or fragmentShader properties at all can be perfectly valid too.

Vertex shader snippets in a shaded custom material

The following functions can be implemented in a vertex shader snippet:

  • void MAIN() When present, this function is called in order to set the value of POSITION, the vec4 output from the vertex shader, and, optionally, to modify the values of VERTEX, COLOR, NORMAL, UV0, UV1, TANGENT, BINORMAL, JOINTS, and WEIGHTS. Unlike in unshaded materials, writing to these makes sense because the modified values are then taken into account in the rest of the generated shader code (whereas for unshaded materials there is no additional shader code generated). For example, if the custom vertex shader displaces the vertices or the normals, it will want to store the modified values to VERTEX or NORMAL, to achieve correct lighting calculations afterwards. Additionally, the function can write to variables defined with VARYING in order to pass interpolated data to the fragment shader. When this function or a redefinition of POSITION is not present, POSITION is calculated based on VERTEX and MODELVIEWPROJECTION_MATRIX, just like a PrincipledMaterial would do.

    Example, with relying both on QML properties exposed as uniforms, and also passing data to the fragment shader:

    VARYING vec3 vNormal;
    VARYING vec3 vViewVec;
    
    void MAIN()
    {
        VERTEX.x += sin(uTime * 4.0 + VERTEX.y) * uAmplitude;
        vNormal = normalize(NORMAL_MATRIX * NORMAL);
        vViewVec = CAMERA_POSITION - (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
        POSITION = MODELVIEWPROJECTION_MATRIX * vec4(VERTEX, 1.0);
    }

    Note: In the above example, assigning a value to POSITION is optional as the usage in this case is identical to the default behavior.

Fragment shader snippets in a shaded custom material

The following functions can be implemented in a fragment shader snippet:

  • void MAIN() When present, this function is called to set the values of the special writable variables BASE_COLOR, METALNESS, ROUGHNESS, SPECULAR_AMOUNT, NORMAL, and FRESNEL_POWER.

    One common use case is to set the value of BASE_COLOR based on sampling a texture, be it a base color map, SCREEN_TEXTURE, or some other kind of source. This can be relevant and convenient especially when no custom light processor functions are implemented. Setting BASE_COLOR.a to something other than the default 1.0 allows affecting the final alpha value of the fragment. (note that this will often require also enabling alpha blending in sourceBlend and destinationBlend)

    Another scenario is when there is no custom SPECULAR_LIGHT function provided, or when there is a light probe set in the SceneEnvironment. The metalness, roughness, and other values that affect the specular contribution calculation can be set in MAIN to their desired custom values.

    The function can write to the following special variables. The values written to these will typically be either hardcoded or be calculated based on QML properties mapped to uniforms. The semantics are identical to PrincipledMaterial.

    • vec4 BASE_COLOR - The base color and material alpha value. Corresponds to the built-in materials' color property. When light processor functions are not implemented, it can be convenient to set a custom base color in MAIN because that is then taken into account in the default lighting calculations. The default value is vec4(1.0), meaning white with an alpha of 1.0. The alpha value effects the final alpha of the fragment. The final alpha value is the object (model) opacity multiplied by the base color alpha. When specifying the value directly in shader code, not relying on uniform values exposed from color properties in QML, be aware that it is up to the shader to perform the sRGB to linear conversion, if needed. For example, assuming a vec3 color and float alpha this can be achieved like the following:
      float C1 = 0.305306011;
      vec3 C2 = vec3(0.682171111, 0.682171111, 0.682171111);
      vec3 C3 = vec3(0.012522878, 0.012522878, 0.012522878);
      BASE_COLOR = vec4(rgb * (rgb * (rgb * C1 + C2) + C3), alpha);
    • vec3 EMISSIVE_COLOR - The color of self-illumination. Corresponds to the built-in materials' emissive color which is combined by built-in materials's emissiveFactor property and built-in materials's emissiveMap property. The default value is vec3(0.0). When specifying the value directly in shader code, not relying on uniform values exposed from color properties in QML, be aware that it is up to the shader to perform the sRGB to linear conversion, if needed.
    • float METALNESS Metalness amount in range 0.0 - 1.0. The default value is 0. Must be set to a non-zero value to have effect.
    • float ROUGHNESS Roughness value in range 0.0 - 1.0. The default value is 0.
    • float FRESNEL_POWER Specifies the fresnel power. A typical value, and also the default, is 5.0 as that is what a PrincipledMaterial would use.
    • float SPECULAR_AMOUNT Specular amount in range 0.0 - 1.0. The default value is 0.5, matching PrincipledMaterial::specularAmount. Must be set to a non-zero value to have effect.
    • vec3 NORMAL - The normal that comes from the vertex shader in world space. While this property has the same initial value as VAR_WORLD_NORMAL, only changing the value of NORMAL will have an effect on lighting.
    • vec3 TANGENT - The tanget that comes from the vertex shader in world space. This value is potentially adjusted for double-sidedness.
    • vec3 BINORMAL - The binormal that comes from the vertex shader in world space. This value is potentially adjusted for double-sidedness.
    • vec2 UV0 - The first set of texture coordinates from the vertex shader. This property is readonly in the fragment shader.
    • vec2 UV1 - The second set of texture coordinates from the vertex shader. This property is readonly in the fragment shader.

    Note: Unlike with unshaded materials, the fragment MAIN for a shaded material has no direct control over FRAGCOLOR. Rather, it is the DIFFUSE and SPECULAR values written in the light processor functions that decide what the final fragment color is. When a light processor function is not implemented, the relevant default shading calculations are performed as with a PrincipledMaterial, taking BASE_COLOR and other values from the list above into account.

    An example of a simple, metallic custom material shader could be the following:

    void MAIN()
    {
        METALNESS = 1.0;
        ROUGHNESS = 0.5;
        FRESNEL_POWER = 5.0;
    }

    Another example, where the base color and alpha are set by sampling a texture:

    VARYING vec2 texcoord;
    void MAIN()
    {
        BASE_COLOR = texture(uColorMap, texcoord);
    }
  • void AMBIENT_LIGHT() When present, this function is called once for each fragment. The task of the function is to add the total ambient contribution to a writable special variable DIFFUSE. It can of course choose to calculate a different value, or not touch DIFFUSE at all (to ignore ambient lighting completely). When this function is not present at all, the ambient contribution is calculated normally, like a PrincipledMaterial would do.

    The function can write to the following special variables:

    • vec3 DIFFUSE Accumulates the diffuse light contributions, per fragment. The light processor functions will typically add (+=) to it, since overwriting the value would lose the contribution from other lights.

    The function can read the following special variables, in addition to the matrix (such as, MODEL_MATRIX) and vector (such as, CAMERA_POSITION) uniforms from the table above:

    • vec3 TOTAL_AMBIENT_COLOR The total ambient contribution in the scene.

    Example:

    void AMBIENT_LIGHT()
    {
        DIFFUSE += TOTAL_AMBIENT_COLOR;
    }
  • void DIRECTIONAL_LIGHT() When present, this function is called for each active directional light in the scene for each fragment. The task of the function is to add the diffuse contribution to a writable special variable DIFFUSE. The function can also choose to do nothing, in which case diffuse contributions from directional lights are ignored. When the function is not present at all, the diffuse contributions from directional lights are accumulated normally, like a PrincipledMaterial would do.

    The function can write to the following special variables:

    • vec3 DIFFUSE Accumulates the diffuse light contributions, per fragment. The light processor functions will typically add (+=) to it, since overwriting the value would lose the contribution from other lights.

    The function can read the following special variables, in addition to the matrix (such as, MODEL_MATRIX) and vector (such as, CAMERA_POSITION) uniforms from the table above:

    • vec3 LIGHT_COLOR Diffuse light color.
    • float SHADOW_CONTRIB Shadow contribution, or 1.0 if not shadowed at all or not reciving shadows.
    • vec3 TO_LIGHT_DIR Vector pointing towards the light source.
    • vec3 NORMAL The normal vector in world space.
    • vec4 BASE_COLOR The base color and material alpha value.
    • float METALNESS The Metalness amount.
    • float ROUGHNESS The Roughness amount.

    Example:

    void DIRECTIONAL_LIGHT()
    {
        DIFFUSE += LIGHT_COLOR * SHADOW_CONTRIB * vec3(max(0.0, dot(normalize(VAR_WORLD_NORMAL), TO_LIGHT_DIR)));
    }
  • void POINT_LIGHT() When present, this function is called for each active point light in the scene for each fragment. The task of the function is to add the diffuse contribution to a writable special variable DIFFUSE. The function can also choose to do nothing, in which case diffuse contributions from point lights are ignored. When the function is not present at all, the diffuse contributions from point lights are accumulated normally, like a PrincipledMaterial would do.

    The function can write to the following special variables:

    • vec3 DIFFUSE Accumulates the diffuse light contributions, per fragment.

    The function can read the following special variables, in addition to the matrix (such as, MODEL_MATRIX) and vector (such as, CAMERA_POSITION) uniforms from the table above:

    • vec3 LIGHT_COLOR Diffuse light color.
    • float LIGHT_ATTENUATION Light attenuation.
    • float SHADOW_CONTRIB Shadow contribution, or 1.0 if not shadowed at all or not reciving shadows.
    • vec3 TO_LIGHT_DIR Vector pointing towards the light source.
    • vec3 NORMAL The normal vector in world space.
    • vec4 BASE_COLOR The base color and material alpha value.
    • float METALNESS The Metalness amount.
    • float ROUGHNESS The Roughness amount.

    Example:

    void POINT_LIGHT()
    {
        DIFFUSE += LIGHT_COLOR * LIGHT_ATTENUATION * SHADOW_CONTRIB * vec3(max(0.0, dot(normalize(VAR_WORLD_NORMAL), TO_LIGHT_DIR)));
    }
  • void SPOT_LIGHT() When present, this function is called for each active spot light in the scene for each fragment. The task of the function is to add the diffuse contribution to a writable special variable DIFFUSE. The function can also choose to do nothing, in which case diffuse contributions from spot lights are ignored. When the function is not present at all, the diffuse contributions from spot lights are accumulated normally, like a PrincipledMaterial would do.

    The function can write to the following special variables:

    • vec3 DIFFUSE Accumulates the diffuse light contributions, per fragment.

    The function can read the following special variables, in addition to the matrix (such as, MODEL_MATRIX) and vector (such as, CAMERA_POSITION) uniforms from the table above:

    • vec3 LIGHT_COLOR Diffuse light color.
    • float LIGHT_ATTENUATION Light attenuation.
    • float SHADOW_CONTRIB Shadow contribution, or 1.0 if not shadowed at all or not reciving shadows.
    • vec3 TO_LIGHT_DIR Vector pointing towards the light source.
    • float SPOT_FACTOR Spot light factor.
    • vec3 NORMAL The normal vector in world space.
    • vec4 BASE_COLOR The base color and material alpha value.
    • float METALNESS The Metalness amount.
    • float ROUGHNESS The Roughness amount.

    Example:

    void SPOT_LIGHT()
    {
        DIFFUSE += LIGHT_COLOR * LIGHT_ATTENUATION * SPOT_FACTOR * SHADOW_CONTRIB * vec3(max(0.0, dot(normalize(VAR_WORLD_NORMAL), TO_LIGHT_DIR)));
    }
  • void SPECULAR_LIGHT() When present, this function is called for each active light in the scene for each fragment. The task of the function is to add the specular contribution to a writable special variable SPECULAR. The function can also choose to do nothing, in which case specular contributions from lights are ignored. When the function is not present at all, the specular contributions from lights are accumulated normally, like a PrincipledMaterial would do.

    The function can write to the following special variables:

    • vec3 SPECULAR Accumulates the specular light contributions, per frament. The light processor functions will typically add (+=) to it, since overwriting the value would lose the contribution from other lights.

    The function can read the following special variables, in addition to the matrix (such as, MODEL_MATRIX) and vector (such as, CAMERA_POSITION) uniforms from the table above:

    • vec3 LIGHT_COLOR Specular light color.
    • float LIGHT_ATTENUATION Light attenuation. For directional lights the value is 1.0. For spot lights the value is the same as LIGHT_ATTENUATION * SPOT_FACTOR of void SPOT_LIGHT().
    • float SHADOW_CONTRIB Shadow contribution, or 1.0 if not shadowed at all or not reciving shadows.
    • vec3 FRESNEL_CONTRIB Fresnel contribution from built in Fresnel calculation.
    • vec3 TO_LIGHT_DIR Vector pointing towards the light source.
    • vec3 NORMAL The normal vector in world space.
    • vec4 BASE_COLOR The base color and material alpha value.
    • float METALNESS The Metalness amount.
    • float ROUGHNESS The Roughness amount.
    • float SPECULAR_AMOUNT The specular amount. This value will be between 0.0 and 1.0 will be the same value set in the custom MAIN function. This value will useful for calculating Fresnel contributions when not using the built-in Fresnel contribution provided by FRESNEL_CONTRIB.
    void SPECULAR_LIGHT()
    {
        vec3 H = normalize(VIEW_VECTOR + TO_LIGHT_DIR);
        float cosAlpha = max(0.0, dot(H, normalize(NORMAL)));
        float shine = pow(cosAlpha, exp2(15.0 * (1.0 - ROUGHNESS) + 1.0) * 0.25);
        SPECULAR += shine * LIGHT_COLOR * FRESNEL_CONTRIB * SHADOW_CONTRIB * LIGHT_ATTENUATION;
    }
  • void POST_PROCESS() When present, this function is called at the end of the fragment pipeline. The task of the function is to finalize COLOR_SUM with final diffuse, specular and emissive terms. Unlike FRAGCOLOR for a unshaded material, COLOR_SUM will be automatically tonemapped before written to the framebuffer. For debugging purposes it is sometimes useful to output a value that should not be treated as a color. To avoid the tonemapping distorting this value it can be disabled by setting the tonemapMode property to TonemapModeNone

    The function can write to the following special variables:

    • vec4 COLOR_SUM the output from the fragment shader. The default value is vec4(DIFFUSE.rgb + SPECULAR + EMISSIVE, DIFFUSE.a)

    The function can read the following special variables.

    • vec4 DIFFUSE The final diffuse term of the fragment pipeline.
    • vec3 SPECULAR The final specular term of the fragment pipeline.
    • vec3 EMISSIVE The final emissive term of the fragment pipeline.
    • vec2 UV0 - The first set of texture coordinates from the vertex shader.
    • vec2 UV1 - The second set of texture coordinates from the vertex shader.
    void POST_PROCESS()
    {
        float center_x = textureSize(SCREEN_TEXTURE, 0).x * 0.5;
        if (gl_FragCoord.x > center_x)
            COLOR_SUM = DIFFUSE;
        else
            COLOR_SUM = vec4(EMISSIVE, DIFFUSE.a);
    }

Custom variables between functions

Additional variables can be delivered from the MAIN function to the others. The SHARED_VARS keyword can be used for defining new custom variables. These user-defined variables can be accessed with SHARED.<variable name>.

For example, a shaded custom material can fetch a shared value in the MAIN and use it in other functions.

SHARED_VARS {
    vec3 colorThreshold;
};
void MAIN()
{
    BASE_COLOR = texture(baseColorMap, UV0);
    SHARED.colorThreshold = texture(thresholdMap, UV0).rgb;
}
void DIRECTIONAL_LIGHT()
{
    if (DIFFUSE >= SHARED.colorThreshold) {
        DIFFUSE = SHARED.colorThreshold;
        return;
    }
    DIFFUSE += LIGHT_COLOR * SHADOW_CONTRIB;
}

Note: SHARED can be written on all the functions without POST_PROCESS but it is safe to write it on MAIN and read on the other functions.

Note: A recommended use case to write SHARED on LIGHT functions is reseting it on MAIN first and then accumulating it on each LIGHT functions.

SHARED_VARS {
    float sheenIntensity;
    float sheenRoughness;
    vec3 sheenColor;
    vec3 outSheenColor;
};
void MAIN()
{
    ...
    vec4 tex = texture(uSheenMap, UV0);
    SHARED.sheenColor = tex.rgb;
    SHARED.sheenIntensity = tex.a;
    SHARED.sheenRoughness = uSheenRoughness;
    SHARED.outSheenColor = vec3(0.0);
}
void SPECULAR_LIGHT()
{
    SHARED.outSheenColor += ...;
}
void POST_PROCESS()
{
    COLOR_SUM = DIFFUSE + SPECULAR + EMISSIVE + SHARED.outSheenColor;
}

Note: MAIN is called before others, and POST_PROCESS after all others, but that there is no guarantee for any other ordering for light processors.

Additional special keywords

The custom fragment shader code can freely access uniforms (such as, CAMERA_DIRECTION or CAMERA_POSITION), and varyings passed on from the custom vertex shader. Additionally, there are a number of built-in varyings available as special keywords. Some of these are optional in the sense that a vertex MAIN could calculate and pass on these on its own, but to reduce duplicated data fragment shaders can also rely on these built-ins instead. These built-ins are available in light processor functions and in the fragment MAIN.

  • vec3 VAR_WORLD_NORMAL - Interpolated normal transformed by NORMAL_MATRIX.
  • vec3 VAR_WORLD_TANGENT - Interpolated tangent transformed by MODEL_MATRIX.
  • vec3 VAR_WORLD_BINORMAL - Interpolated binormal transformed by MODEL_MATRIX
  • vec3 NORMAL - Unlike VAR_WORLD_NORMAL, which is the interpolated normal as-is, this value is potentially adjusted for double-sidedness: when rendering with culling disabled, the normal will get inverted as necessary. Therefore lighting and other calculations are recommended to use NORMAL instead of VAR_WORLD_NORMAL in order behave correctly with all culling modes.
  • vec3 TANGENT - Like NORMAL, this value is potentially adjusted for double-sidedness: when rendering with culling disabled, the tangent will get inverted as necessary.
  • vec3 BINORMAL - Like NORMAL, this value is potentially adjusted for double-sidedness: when rendering with culling disabled, the binormal will get inverted as necessary.
  • vec3 VAR_WORLD_POSITION - Interpolated world space vertex position ((MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz)
  • vec4 VAR_COLOR - The interpolated vertex color when colors are provided in the mesh. vec4(1.0) otherwise.
  • vec3 VIEW_VECTOR - Points towards the camera. This is effectively the CAMERA_POSITION - VAR_WORLD_POSITION vector normalized.
  • vec4 FRAGCOORD - Contains the window-relative coordinates of the current fragment.
  • float FRAMEBUFFER_Y_UP - The value is 1 when the Y axis points up in the coordinate system for framebuffers (textures), meaning (0, 0) is the bottom-left corner. The value is -1 when the Y axis points down, (0, 0) being the top-left corner. Such differences in the underlying graphics APIs do not concern most custom materials. One notable exception is sampling SCREEN_TEXTURE with texture coordinates not based on FRAGCOORD. As the orientation of SCREEN_TEXTURE is tied to the underlying graphics API by nature, using texture coordinates from a mesh may need appropriate adjustments to the Y coordinate.

    For example, the following fragment shader, suitable for Rectangle or Cube meshes, will display the opaque objects from the scene on the model:

    VARYING vec2 texcoord;
    void MAIN()
    {
        vec2 screencoord = texcoord;
        if (FRAMEBUFFER_Y_UP < 0.0) // effectively: if not OpenGL
            screencoord.y = 1.0 - screencoord.y;
        BASE_COLOR = texture(SCREEN_TEXTURE, screencoord);
    }

    When sampling textures other than SCREEN_TEXTURE and DEPTH_TEXTURE, or when FRAGCOORD is used to calculate the texture coordinate (which would be the typical use case for accessing the screen and depth textures), such an adjustment is not necessary.

  • float NDC_Y_UP - The value is 1 when the Y axis points up in normalized device coordinate space, and -1 when the Y axis points down. Y pointing down is the case when rendering happens with Vulkan. Most materials do not need to be concerned by this, but being able to branch based on this can become useful in certain advanced use cases.
  • float NEAR_CLIP_VALUE - The value is -1 for when the clipping plane range's starts at -1 and goes to 1. This is true when using OpenGL for rendering. For other rendering backends the value of this property will be 0 meaning the clipping plane range is 0 to 1. This value is useful with certain techniques involving the DEPTH_TEXTURE

    For example, the following fragment shader demonstrates a technique for reconstructing the position of a value from the depth buffer to determine the distance from the current position being rendered. When used in combination with INVERSE_PROJECTION_MATRIX the value of depth needs to be in normalized device coordinates so it is important to make sure that the range of depth value reflects that. When the NEAR_CLIP_VALUE is -1 then the depth value gets scaled to be between -1 and 1.

    void MAIN() {
        vec2 screen_uv = FRAGCOORD.xy / vec2(textureSize(SCREEN_TEXTURE, 0));
        float depth = texture(DEPTH_TEXTURE, screen_uv).r;
    
        if (NEAR_CLIP_VALUE < 0.0) // effectively: if opengl
            depth = depth * 2.0 - 1.0;
    
        vec4 unproject = INVERSE_PROJECTION_MATRIX * vec4(screen_uv, depth, 1.0);
        depth = (unproject.xyz / unproject.w).z;
        float viewVectorZ = (VIEW_MATRIX * vec4(VAR_WORLD_POSITION, 1.0)).z;
        depth = viewVectorZ - depth;
    
        BASE_COLOR = vec4(depth, depth, depth, 1.0);
    }

Instancing

When doing instanced rendering, some of the keywords above do not apply. The following keywords are only available with instancing:

  • INSTANCE_MODEL_MATRIX -> mat4, replacement for MODEL_MATRIX, including the instancing transformation.
  • INSTANCE_MODELVIEWPROJECTION_MATRIX -> mat4, replacement for MODELVIEWPROJECTION_MATRIX, including the instancing transformation.
  • INSTANCE_COLOR -> vec4, the instance color: to be combined with COLOR.
  • INSTANCE_DATA -> vec4, instance custom data.
  • INSTANCE_INDEX -> int, the instance number, and index into the instancing table.

Screen, depth, and other textures

The rendering pipeline can expose a number of textures to the custom material shaders with content from special render passes. This applies both to shaded and unshaded custom materials.

For example, a shader may want access to a depth texture that contains the depth buffer contents for the opaque objects in the scene. This is achieved by sampling DEPTH_TEXTURE. Such a texture is not normally generated, unless there is a real need for it. Therefore, the presence of the following keywords in the vertex or fragment shader also acts as a toggle for opting in to the - potentially expensive - passes for generating the texture in question. (of course, it could be that some of these become already enabled due to other settings, such as the ambient occlusion parameters in SceneEnvironment or due to a post-processing effect relying on the depth texture, in which case the textures in question are generated regardless of the custom material and so sampling these special textures in the material comes at no extra cost apart from the texture access itself)

  • SCREEN_TEXTURE - When present, a texture (sampler2D) with the color buffer from a rendering pass containing the opaque objects in the scene is exposed to the shader under this name. This also implies that any object with a custom material where the shaders sample SCREEN_TEXTURE will be treated as if it had semi-transparency enabled on it, even when the object opacity is 1.0 and blending was not enabled on the CustomMaterial. This is because such an object cannot be part of the opaque rendering lists, because it itself depends on the rendering results of those objects and thus cannot be rendered in line together with those. Pixels that are not covered by opaque objects will be set to transparent (vec4(0.0)) in the texture. For example, a fragment shader could contain the following:
    vec2 uv = FRAGCOORD.xy / vec2(textureSize(SCREEN_TEXTURE, 0));
    vec2 displace = vec2(0.1);
    vec4 c = texture(SCREEN_TEXTURE, uv + displace);

    Be aware that using SCREEN_TEXTURE requires appropriate, conscious design of the scene. Objects using such materials have to be positioned carefully, typically above all other objects that are expected to be visible in the texture. Objects that employ semi-transparency in some form are never part of the SCREEN_TEXTURE. Often SCREEN_TEXTURE will be used in combination with BASE_COLOR in MAIN. For example, the following custom fragment shader applies an emboss effect, while keeping fragments not touched by opaque objects transparent. This assumes that the object with the material is placed in the front, and that it has blending enabled.

    void MAIN()
    {
        vec2 size = vec2(textureSize(SCREEN_TEXTURE, 0));
        vec2 uv = FRAGCOORD.xy / size;
    
        // basic emboss effect
        vec2 d = vec2(1.0 / size.x, 1.0 / size.y);
        vec4 diff = texture(SCREEN_TEXTURE, uv + d) - texture(SCREEN_TEXTURE, uv - d);
        float c = (diff.x + diff.y + diff.z) + 0.5;
    
        float alpha = texture(SCREEN_TEXTURE, uv).a;
        BASE_COLOR = vec4(vec3(c), alpha);
    }
  • SCREEN_MIP_TEXTURE - Identical to SCREEN_TEXTURE in most ways, the difference being that this texture has mipmaps generated. This can be an expensive feature performance-wise, depending on the screen size, and due to having to generate the mipmaps every time the scene is rendered. Therefore, prefer using SCREEN_TEXTURE always, unless a technique relying on the texture mip levels (e.g. using textureLod in the shader) is implemented by the custom material.
  • DEPTH_TEXTURE - When present, a texture (sampler2D) with the (non-linearized) depth buffer contents is exposed to the shader under this name. Only opaque objects are included. For example, a fragment shader could contain the following:
    ivec2 dtSize = textureSize(DEPTH_TEXTURE, 0);
    vec2 dtUV = (FRAGCOORD.xy) / vec2(dtSize);
    vec4 depthSample = texture(DEPTH_TEXTURE, dtUV);
    float zNear = CAMERA_PROPERTIES.x;
    float zFar = CAMERA_PROPERTIES.y;
    float zRange = zFar - zNear;
    float z_n = 2.0 * depthSample.r - 1.0;
    float d = 2.0 * zNear * zFar / (zFar + zNear - z_n * zRange);
    d /= zFar;
  • AO_TEXTURE - When present and screen space ambient occlusion is enabled (meaning when the AO strength and distance are both non-zero) in SceneEnvironment, the SSAO texture (sampler2D) is exposed to the shader under this name. Sampling this texture can be useful in unshaded materials. Shaded materials have ambient occlusion support built in. This means that the ambient occlusion factor is taken into account automatically. Whereas in a fragment shader for an unshaded material one could write the following to achieve the same:
    ivec2 aoSize = textureSize(AO_TEXTURE, 0);
    vec2 aoUV = (FRAGCOORD.xy) / vec2(aoSize);
    float aoFactor = texture(AO_TEXTURE, aoUV).x;

See also SceneEnvironment::tonemapMode, Qt Quick 3D - Custom Shaders Example, Qt Quick 3D - Custom Materials Example, and Programmable Materials, Effects, Geometry, and Texture data.

Property Documentation

alwaysDirty : bool

Specifies that the material state is always dirty, which indicates that the material needs to be refreshed every time it is used by the QtQuick3D.

destinationBlend : enumeration

Specifies the destination blend factor. The default value is CustomMaterial.NoBlend.

Constant Value
CustomMaterial.NoBlend
CustomMaterial.Zero
CustomMaterial.One
CustomMaterial.SrcColor
CustomMaterial.OneMinusSrcColor
CustomMaterial.DstColor
CustomMaterial.OneMinusDstColor
CustomMaterial.SrcAlpha
CustomMaterial.OneMinusSrcAlpha
CustomMaterial.DstAlpha
CustomMaterial.OneMinusDstAlpha
CustomMaterial.ConstantColor
CustomMaterial.OneMinusConstantColor
CustomMaterial.ConstantAlpha
CustomMaterial.OneMinusConstantAlpha
CustomMaterial.SrcAlphaSaturate

fragmentShader : url

Specfies the file with the snippet of custom fragment shader code.

The value is a URL and must either be a local file or use the qrc scheme to access files embedded via the Qt resource system. Relative file paths (without a scheme) are also accepted, in which case the file is treated as relative to the component (the .qml file).

See also vertexShader.

lineWidth : real

This property determines the width of the lines rendered, when the geometry is using a primitive type of lines or line strips. The default value is 1.0. This property is not relevant when rendering other types of geometry, such as, triangle meshes.

Warning: Line widths other than 1 may not be suported at run time, depending on the underlying graphics API. When that is the case, the request to change the width is ignored. For example, none of the following can be expected to support wide lines: Direct3D, Metal, OpenGL with core profile contexts.

Note: Unlike the line width, the value of which is part of the graphics pipeline object, the point size for geometries with a topology of points is controlled by the vertex shader (when supported), and has therefore no corresponding QML property.

shadingMode : enumeration

Specifies the type of the material. The default value is Shaded.

Constant Value
CustomMaterial.Unshaded
CustomMaterial.Shaded

sourceBlend : enumeration

Specifies the source blend factor. The default value is CustomMaterial.NoBlend.

Constant Value
CustomMaterial.NoBlend
CustomMaterial.Zero
CustomMaterial.One
CustomMaterial.SrcColor
CustomMaterial.OneMinusSrcColor
CustomMaterial.DstColor
CustomMaterial.OneMinusDstColor
CustomMaterial.SrcAlpha
CustomMaterial.OneMinusSrcAlpha
CustomMaterial.DstAlpha
CustomMaterial.OneMinusDstAlpha
CustomMaterial.ConstantColor
CustomMaterial.OneMinusConstantColor
CustomMaterial.ConstantAlpha
CustomMaterial.OneMinusConstantAlpha
CustomMaterial.SrcAlphaSaturate

vertexShader : url

Specfies the file with the snippet of custom vertex shader code.

The value is a URL and must either be a local file or use the qrc scheme to access files embedded via the Qt resource system. Relative file paths (without a scheme) are also accepted, in which case the file is treated as relative to the component (the .qml file).

See also fragmentShader.

© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-6.2/qml-qtquick3d-custommaterial.html