- Qt 6.7
- Qt Quick 3D
- Qt Quick 3D Examples and Tutorials
- Qt Quick 3D - Principled Material Example
- Qt Quick 3D | Commercial or GPLv3

# Qt Quick 3D - Principled Material Example

     // Copyright (C) 2023 The Qt Company Ltd.
     // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

     import QtQuick
     import QtQuick.Controls
     import QtQuick.Layouts
     import QtQuick3D

     // qmllint disable missing-property
     // Disabling missing-property because the targetMaterial property
     // will either be a PrincipaledMaterial or SpecularGlossyMaterial
     // but the shared properties are not part of the common base class
     ScrollView {
         id: rootView
         required property Material targetMaterial
         required property View3D view3D
         ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
         width: availableWidth

         ColumnLayout {
             width: rootView.availableWidth
             MarkdownLabel {
                 text: `# Material Details
     This section describes a series of properties to add additional details to
     a material. Not every material will need all of these properties, but for
     specific use cases these details may be exactly what you need.
     ## Normal
     The Normal describes the direction a surface is facing. Each vertex of a Model
     also profiles a normal value to define how each face should be shaded. At this
     level though the amount of detail a material can provide is limited to the source
     mesh's level of detail. Using more detailed meshes can be very expensive, so
     instead a Normal Map texture can be provided to add additional surface details
     without increasing geometry.
     ### Normal Map
     A Normal map is a special kind of texture where directions (normals) are stored
     as color values. These directions are sampled from the Normal map by the material
     and combined with the directions of a models geometry to adjust the way light
     interacts with the material.
     `
             }
             TextureSourceControl {
                 defaultTexture: "maps/metallic/normal.jpg"
                 defaultClearColor: Qt.rgba(0.5, 0.5, 1.0, 1.0)
                 stampMode: true
                 stampSource: "maps/normal_stamp.png"
                 onTargetTextureChanged: {
                     rootView.targetMaterial.normalMap = targetTexture
                 }
             }
             MarkdownLabel {
                 text: `### Normal Strength
     By adjusting the normal strength you will see that the amount of influence the
     normal map has on the material changes.`
             }
             RowLayout {
                 Label {
                     text: "Normal Strength (" + rootView.targetMaterial.normalStrength.toFixed(2) + ")"
                     Layout.fillWidth: true
                 }
                 Slider {
                     from: 0
                     to: 1
                     value: rootView.targetMaterial.normalStrength
                     onValueChanged: rootView.targetMaterial.normalStrength = value
                 }
             }

             VerticalSectionSeparator {}

             MarkdownLabel {
                 text: `## Height
     In addition to providing a Normal map to give the impression of more geometry
     it is possible to also provide a height map for give even more depth to a
     material. This can also be known as a displacement map. Normally there are two
     approaches to displacement: Tessellation and Parallax Occlusion Mapping. The
     PrincipledMaterial currently only supports Parallax Occlusion Mapping which
     means that instead of adding additional geometry based the height map, instead
     we manipulate the way textures are mapped to the geometry to give the illusion
     of more depth. And while this approach is much cheaper than Tessellation, it
     comes with the limitation that it really only works for flat surfaces, and does
     not change the silhouette of a model (how it looks from the side).

     So for our example, any height map you add will only have the desired effect on
     the Cube, and only if other textures are present.
     ### Height Amount
     This is the amount of displacement that should be applied from the height map.
     Unlike many of the other fields, it is unlikely that you will want to just set
     this value to 1.0 (the max).  The amount of displacement needed for a particular
     material will require some adjustment for taste.  A little bit goes a long way.`
             }
             RowLayout {
                 Label {
                     text: "Height Amount  (" + rootView.targetMaterial.heightAmount.toFixed(2) + ")"
                     Layout.fillWidth: true
                 }
                 Slider {
                     from: 0
                     to: 1
                     value: rootView.targetMaterial.heightAmount
                     onValueChanged: rootView.targetMaterial.heightAmount = value
                 }
             }

             MarkdownLabel {
                 text: `### Height Map
     The Height Map is a greyscale (single channel) texture representing to amount
     of displacement that should be applied. A black value (0.0) means none, and
     white (1.0) means the maximum amount, which is determined by the Height Amount
     property.
     `
             }
             ComboBox {
                 id: heightChannelComboBox
                 textRole: "text"
                 valueRole: "value"
                 implicitContentWidthPolicy: ComboBox.WidestText
                 onActivated: rootView.targetMaterial.heightChannel = currentValue
                 Component.onCompleted: currentIndex = indexOfValue(rootView.targetMaterial.heightChannel)
                 model: [
                     { value: PrincipledMaterial.R, text: "Red Channel"},
                     { value: PrincipledMaterial.G, text: "Green Channel"},
                     { value: PrincipledMaterial.B, text: "Blue Channel"},
                     { value: PrincipledMaterial.A, text: "Alpha Channel"}
                 ]
             }
             TextureSourceControl {
                 defaultTexture: "maps/noise.png"
                 defaultClearColor: "black"
                 onTargetTextureChanged: {
                     rootView.targetMaterial.heightMap = targetTexture
                 }
             }

             VerticalSectionSeparator {}

             MarkdownLabel {
                 text: `## Ambient Occlusion
     To understand Ambient Occlusion, you must first understand occlusion. Occlusion is
     about blocking light, or shadowing. If something is occluded it will be unable
     to receive light, and will appear darker than parts that are un-occluded.
     But this simplistic occlusion of light only takes into consideration the first reflection
     of a light on a model. Light will be reflected off surfaces to other surrounding
     surfaces multiple times. This distinction between the first reflection vs any additional
     reflections is referred to as direct vs indirect light. Ambient Occlusion is about
     simulating a behavior of indirect light: when a model has crevasses or corners,
     light is less likely to be reflected into them, leading them to be darker than more
     open faces. Realtime renderers like Qt Quick 3D don't tend to model more than the first
     reflection of light (direct lighting) so baking an ambient occlusion map will provide
     additional realism to materials.
     `
             }
             MarkdownLabel {
                 text: `### Ambient Occlusion Map
     Ambient Occlusion maps are baked in 3D content creation tools for each model
     using ray tracing. Since all three of our models share the same material, if
     an appropriate map is applied, it will only look correct for one of the models
     at a time.  In this case the only model we have to would benefit from an AO map
     is the monkey, since it is the only one with any details that could self occlude.
     If you apply the provided texture you will notice the crevasses around the eyes
     and ears of the monkey model will appear slightly darker.
     `
             }
             ComboBox {
                 id: aoChannelComboBox
                 textRole: "text"
                 valueRole: "value"
                 implicitContentWidthPolicy: ComboBox.WidestText
                 onActivated: rootView.targetMaterial.occlusionChannel = currentValue
                 Component.onCompleted: currentIndex = indexOfValue(rootView.targetMaterial.occlusionChannel)
                 model: [
                     { value: PrincipledMaterial.R, text: "Red Channel"},
                     { value: PrincipledMaterial.G, text: "Green Channel"},
                     { value: PrincipledMaterial.B, text: "Blue Channel"},
                     { value: PrincipledMaterial.A, text: "Alpha Channel"}
                 ]
             }

             TextureSourceControl {
                 defaultTexture: "maps/monkey_ao.jpg"
                 defaultClearColor: "white"
                 onTargetTextureChanged: {
                     rootView.targetMaterial.occlusionMap = targetTexture
                 }
             }

             VerticalSectionSeparator {}

             MarkdownLabel {
                 text: `## Emission
     The emission properties are about the material's ability to produce its own
     light. This light does not affect other materials in the scene, but does add
     energy to the lighting calculations of the material without them coming from
     an external source.`
             }

             MarkdownLabel {
                 text: `### Emissive Factor
     In the absence of an Emissive Map, the amount of light a material emits is
     controlled by the Emissive Factor. Each channel is added as an additional
     light contribution to the material. So if you set the value of Red to 1.0, then
     1.0 of red light will be added to the material's color after all other lighting
     calculations have been done. These 3 channels are representing the amount of
     each color that is added, but the property itself is not a color. That is
     because colors are always clamped to values between 0.0 - 1.0, whereas these
     factors can be any floating point values. In this example these values are clamped
     between 0.0 and 1.0, but you can click the *Un-Clamp* button to experiment with
     values between -1.0 and 2.0. The scene should also look slightly different
     because some post processing effects are enabled to demonstrate handling color
     values greater than 1.0.`
             }

             RowLayout {
                 Button {
                     id: clampEmissionButton
                     property bool clampEmission: true
                     text: clampEmission ? "Un-clamp" : "Clamp"
                     checkable: true
                     checked: clampEmission
                     onClicked: {
                         clampEmission = !clampEmission
                         if (clampEmission) {
                             rootView.view3D.environment.tonemapMode = SceneEnvironment.TonemapModeLinear
                             rootView.view3D.environment.enableEffects = false;
                         } else {
                             rootView.view3D.environment.tonemapMode = SceneEnvironment.TonemapModeNone
                             rootView.view3D.environment.enableEffects = true;
                         }
                     }
                 }
                 Button {
                     text: "All 0.0"
                     onClicked: {
                         rootView.targetMaterial.emissiveFactor = Qt.vector3d(0, 0, 0)
                     }
                 }
                 Button {
                     text: "All 1.0"
                     onClicked: {
                         rootView.targetMaterial.emissiveFactor = Qt.vector3d(1, 1, 1)
                     }
                 }
             }

             RowLayout {
                 Label {
                     text: "Red (" + rootView.targetMaterial.emissiveFactor.x.toFixed(2) + ")"
                     Layout.fillWidth: true
                 }
                 Slider {
                     from: clampEmissionButton.clampEmission ? 0.0 : -1.0
                     to: clampEmissionButton.clampEmission ? 1.0 : 2
                     value: rootView.targetMaterial.emissiveFactor.x
                     onValueChanged: rootView.targetMaterial.emissiveFactor.x = value
                 }
             }
             RowLayout {
                 Label {
                     text: "Green (" + rootView.targetMaterial.emissiveFactor.y.toFixed(2) + ")"
                     Layout.fillWidth: true
                 }

                 Slider {
                     from: clampEmissionButton.clampEmission ? 0.0 : -1.0
                     to: clampEmissionButton.clampEmission ? 1.0 : 2
                     value: rootView.targetMaterial.emissiveFactor.y
                     onValueChanged: rootView.targetMaterial.emissiveFactor.y = value
                 }
             }
             RowLayout {
                 Label {
                     text: "Blue (" + rootView.targetMaterial.emissiveFactor.z.toFixed(2) + ")"
                     Layout.fillWidth: true
                 }

                 Slider {
                     from: clampEmissionButton.clampEmission ? 0.0 : -1.0
                     to: clampEmissionButton.clampEmission ? 1.0 : 2
                     value: rootView.targetMaterial.emissiveFactor.z
                     onValueChanged: rootView.targetMaterial.emissiveFactor.z = value
                 }
             }
             MarkdownLabel {
                 text: `### Emissive Map
     If an Emissive Map is provided, then then the Emissive Factor is used as a
     multiplier for the color values read from the Emissive Map.  This multiplied
     value is then added to the materials color value after all other lighting calculations
     have been preformed.`
             }
             TextureSourceControl {
                 defaultTexture: "maps/monkey_ao.jpg"
                 defaultClearColor: "black"
                 onTargetTextureChanged: {
                     rootView.targetMaterial.emissiveMap = targetTexture
                 }
             }
         }
     }
     // qmllint enable missing-property