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