Qt Quick 3D - Principled Material Example
// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick3D 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: { 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 (" + targetMaterial.normalStrength.toFixed(2) + ")" Layout.fillWidth: true } Slider { from: 0 to: 1 value: targetMaterial.normalStrength onValueChanged: 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 (" + targetMaterial.heightAmount.toFixed(2) + ")" Layout.fillWidth: true } Slider { from: 0 to: 1 value: targetMaterial.heightAmount onValueChanged: 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: targetMaterial.heightChannel = currentValue Component.onCompleted: currentIndex = indexOfValue(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: { 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: targetMaterial.occlusionChannel = currentValue Component.onCompleted: currentIndex = indexOfValue(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: { 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: { targetMaterial.emissiveFactor = Qt.vector3d(0, 0, 0) } } Button { text: "All 1.0" onClicked: { targetMaterial.emissiveFactor = Qt.vector3d(1, 1, 1) } } } RowLayout { Label { text: "Red (" + targetMaterial.emissiveFactor.x.toFixed(2) + ")" Layout.fillWidth: true } Slider { from: clampEmissionButton.clampEmission ? 0.0 : -1.0 to: clampEmissionButton.clampEmission ? 1.0 : 2 value: targetMaterial.emissiveFactor.x onValueChanged: targetMaterial.emissiveFactor.x = value } } RowLayout { Label { text: "Green (" + targetMaterial.emissiveFactor.y.toFixed(2) + ")" Layout.fillWidth: true } Slider { from: clampEmissionButton.clampEmission ? 0.0 : -1.0 to: clampEmissionButton.clampEmission ? 1.0 : 2 value: targetMaterial.emissiveFactor.y onValueChanged: targetMaterial.emissiveFactor.y = value } } RowLayout { Label { text: "Blue (" + targetMaterial.emissiveFactor.z.toFixed(2) + ")" Layout.fillWidth: true } Slider { from: clampEmissionButton.clampEmission ? 0.0 : -1.0 to: clampEmissionButton.clampEmission ? 1.0 : 2 value: targetMaterial.emissiveFactor.z onValueChanged: 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: { targetMaterial.emissiveMap = targetTexture } } } }