实例化渲染

介绍

Qt Quick 3D supports instancing of Model objects. Instancing refers to a technique where one object is rendered multiple times with a single draw call. (For example the OpenGL function glDrawElementsInstanced )。

Instancing allows duplicating a model with variations. In contrast to using a Repeater3D , the model and its graphics resources are only allocated once. The rendering of the duplicated instances is done at a low level by the GPU. Depending on the complexity of the model, this can give a performance improvement of several orders of magnitude.

In practice, instancing is done by defining a table that specifies how each instance is modified relative to the base model.

Instancing API

The main principle of the instancing API is that it is explicit: It doesn't try to autodetect opportunities for instancing within the existing API. Instead, each model is marked individually by setting its instancing property to reference an Instancing object. The same Instancing object can be used on multiple models at the same time.

The Instancing object specifies a table that defines how each copy is rendered. The available modifications are:

  • transformation : position, rotation, and scale
  • color : a color that is blended with the model’s material
  • custom data : data that can be used by custom materials

Qt provides three QML types that inherit from Instancing:

  • InstanceList enumerates all instances and allows binding to the properties of each instance.
  • RandomInstancing provides a way to quickly test and prototype by generating random instances within defined bounds.
  • FileInstancing reads an instance table from an external file.

instancing example shows how to create a scene using the QML API.

Other kinds of instance tables can be defined in C++ by subclassing QQuick3DInstancing . For example, the particle system uses its own instancing table internally. It is available as ModelParticle3D.instanceTable .

By writing custom shader code, it is possible to use instancing to control additional properties, such as variables for physically based rendering, skeletal animation weights, distortion, or anything else that can be expressed with custom materials. The custom data in the instancing table consists of four floating point numbers.

custom instancing example shows how to combine custom materials and an instance table implemented in C++.

Alpha-blending and instancing

Correct alpha blending requires that semi-transparent objects are rendered back-to-front. For this reason, QtQuick3D sorts opaque and semi-transparent objects separately, and renders them in the correct order. With instancing, however, the GPU will render the instances in the order specified by the instancing table, if depth-sorting is not turned on. For performance reasons, QtQuick3D does not sort the table by default as it can take long time with large number of instances. This means that if semi-transparent instances overlap with each other, or with other semi-transparent objects, the results may look wrong. In general, the error is less visible when the opacity is low.

Fully opaque objects together with non-overlapping semi-transparent objects will always be rendered correctly, since Qt uses depth buffer testing to avoid drawing behind opaque objects. However, the lack of sorting has potential performance implications for opaque objects: They may not be rendered in the optimal order, meaning that the same pixel can be written multiple times, making more work for the fragment shader.

The renderer does not inspect the contents of the instancing table, so it must be specified explicitly when an instance table contains semi-transparent alpha values: Set the hasTransparency property to to true to make sure that the renderer enables alpha blending. This applies to all the instances: Even fully opaque instances will be rendered without depth testing, potentially causing visible errors.

The rendering order relative to the rest of the scene can be adjusted by setting the depth bias of the model.

Transforms and instancing

Each instance has its own transform in the instance table. This is combined with the transforms on the instanced model. This is slightly complex, since there are several use cases:

  • Doing a transform on the model that is applied to each individual instance. This allows cheap animations, for example by rotating all instances at once without having to change the instance table.
  • Transforming the entire group of instances at once.
  • Instancing a model hierarchy.

To support all these cases, The model’s transform is split into two parts: the local instance transform ,和 global instance transform . Conceptually, instancing is performed like this:

  • First the model is transformed according to the local instance transform.
  • Then each instance is calculated by applying the instance table transform
  • Finally, the whole group of instanced objects is transformed according to the global instance transform.

By default, the local instance transform of a model consists of the model’s scale and rotation, while the rest goes into the global instance transform.

This can be controlled by setting the model’s instanceRoot property. This defines the origin of the instance’s coordinate system. The most common use is when instancing a hierarchy of models. For example, a sphere orbiting around a cube:

Model {
    id: cube
    instancing: someInstanceTable
    source: "#Cube"
    materials: DefaultMaterial { diffuseColor: "lightgray" }
    Node {
        Model {
            source: "#Sphere"
            instanceRoot: cube
            instancing: cube.instancing
            x: 150
            materials: DefaultMaterial { diffuseColor: "gray" }
        }
        NumberAnimation on eulerRotation.y {
            from: 0
            to: 360
            duration: 4000
            loops: Animation.Infinite
        }
    }
}
					

instanceRoot is necessary to specify that the sphere instance should be positioned as if it were an element of the cube. Each model in a hierarchy still needs to specify the instancing property: in normal cases they should all be set to the same Instancing 对象。

instanceRoot can also be used when instancing a single model. For example, a cylinder rotating around an off-center point:

 Node {
    id: parentNode
    Model {
        source: "#Cylinder"
        instanceRoot: parentNode
        instancing: anotherInstanceTable
        x: 25
        materials: DefaultMaterial { diffuseColor: "white" }
    }
    NumberAnimation on eulerRotation.y {
        from: 0
        to: 360
        duration: 1000
        loops: Animation.Infinite
    }
}