CG Kit提供延迟渲染管线功能,可以从默认的前向渲染切换到延迟渲染。
SetMultiThreadRendering(false); SetMSAA(false);
m_mainCamera->SetRenderingPath(RenderingPathType::RENDER_PATH_TYPE_DEFERRED);
以上提供的shader清单是CG Kit默认使用的,开发者可以根据需要进行修改,自定义shader编译替换对应的spv文件即可。
以下是G-Buffer阶段的顶点着色器,用于处理有关几何信息如法线等。
#version 430 core
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec2 inTexcoord0;
layout(location = 2) in vec3 inNormal;
layout(location = 3) in vec4 inTangent;
layout(set = 0, binding = 0) uniform GlobalUniform
{
mat4 view;
mat4 projection;
mat4 projectionOrtho;
mat4 viewProjection;
mat4 viewProjectionInv;
mat4 viewProjectionOrtho;
vec4 resolution;
vec4 cameraPosition;
} globalUniform;
layout(set = 0, binding = 1) uniform ModelUniform
{
mat4 model;
} modelUniform;
layout (location = 0) out vec3 outPosition;
layout (location = 1) out vec2 outTexcoord0;
layout (location = 2) out mat3 outTangentBasis;
vec4 getWorldPosition(vec3 position)
{
return modelUniform.model * vec4(position, 1.0);
}
vec3 getNormal(vec3 normal)
{
return normal;
}
void main()
{
vec4 worldPosition = getWorldPosition(inPosition);
outPosition = worldPosition.xyz;
outTexcoord0 = vec2(inTexcoord0.x, inTexcoord0.y);
vec3 normal = getNormal(inNormal);
// Pass tangent space basis vectors (for normal mapping).
vec3 bitangent = cross(inTangent.xyz, normal) * inTangent.w;
outTangentBasis = mat3(modelUniform.model) * mat3(inTangent.xyz, bitangent, normal);
gl_Position = globalUniform.viewProjection * worldPosition;
}
以下是G-Buffer阶段的片段着色器,用于存储有关几何信息和PBR信息等。
#version 450
layout (location = 0) in vec3 inPosition;
layout (location = 1) in vec2 inTexcoord0;
layout (location = 2) in mat3 inTangentBasis;
layout (location = 0) out vec4 gbuffer0;
layout (location = 1) out vec4 gbuffer1;
layout (location = 2) out vec4 gbuffer2;
layout(set = 0, binding = 2) uniform sampler2D albedoTexture;
layout(set = 0, binding = 3) uniform sampler2D normalTexture;
layout(set = 0, binding = 4) uniform sampler2D pbrTexture;
layout(set = 0, binding = 5) uniform sampler2D emissionTexture;
vec4 getAlbedoChannel()
{
return texture(albedoTexture, inTexcoord0);
}
vec4 getNormalChannel()
{
return texture(normalTexture, inTexcoord0);
}
vec4 getPbrChannel()
{
return texture(pbrTexture, inTexcoord0);
}
vec4 getEmissionChannel()
{
return texture(emissionTexture, inTexcoord0);
}
void main()
{
vec3 pbr = getPbrChannel().xyz;
// pbr.x is Ambient Occlusion
gbuffer0 = vec4(getAlbedoChannel().xyz, pbr.x);
vec3 N = normalize(2.0 * getNormalChannel().xyz - 1.0);
N = normalize(inTangentBasis[0] * N.x + inTangentBasis[1] * N.y + inTangentBasis[2] * N.z);
// encode normal
float scale = 1.7777;
vec2 normalEncode = N.xy / (N.z + 1);
normalEncode /= scale;
normalEncode = normalEncode * 0.5 + 0.5;
// pbr.y is Roughness
gbuffer1 = vec4(normalEncode.x, normalEncode.y, pbr.y, 1.0);
// pbr.z is metalness
gbuffer2 = vec4(getEmissionChannel().xyz, pbr.z);
}
以下是G-Buffer阶段天空盒的片段着色器,与一般对象的差异是:PbrChannel和EnvChannel都是0,Emission Channel对应为envTexture。
#version 450
layout (location = 0) in vec3 inPosition;
layout (location = 1) in vec2 inTexcoord0;
layout (location = 2) in mat3 inTangentBasis;
layout (location = 0) out vec4 gbuffer0;
layout (location = 1) out vec4 gbuffer1;
layout (location = 2) out vec4 gbuffer2;
layout(set = 0, binding = 2) uniform samplerCube envTexture;
vec4 getAlbedoChannel()
{
return vec4(0.0);
}
vec4 getNormalChannel()
{
return vec4(0.0);
}
vec4 getPbrChannel()
{
return vec4(0.0);
}
vec4 getEmissionChannel()
{
vec3 envVector = normalize(inPosition.xyz);
return textureLod(envTexture, envVector, 1.0);
}
vec4 getBRDFChannel(vec2 location)
{
return vec4(0.0);
}
vec4 getEnvChannel(vec3 location, float LOD)
{
return vec4(0.0);
}
void main()
{
vec3 pbr = getPbrChannel().xyz;
// pbr.x is Ambient Occlusion
gbuffer0 = vec4(getAlbedoChannel().xyz, pbr.x);
vec3 N = normalize(2.0 * getNormalChannel().xyz - 1.0);
N = normalize(inTangentBasis[0] * N.x + inTangentBasis[1] * N.y + inTangentBasis[2] * N.z);
// encode normal
float scale = 1.7777;
vec2 normalEncode = N.xy / (N.z + 1);
normalEncode /= scale;
normalEncode = normalEncode * 0.5 + 0.5;
// pbr.y is Roughness
gbuffer1 = vec4(normalEncode.x, normalEncode.y, pbr.y, 1.0);
// pbr.z is metalness
gbuffer2 = vec4(getEmissionChannel().xyz, pbr.z);
}
以下是光照合成阶段的顶点着色器,直接使用shader生成gl_Position,得到屏幕四边形进行延迟渲染绘制。
#version 430 core
layout(location = 0) out vec2 outUV;
void main()
{
outUV = vec2(gl_VertexIndex & 2, (gl_VertexIndex << 1) & 2);
gl_Position = vec4(outUV * 2.0f - 1.0f, 0.0f, 1.0f);
}
以下是平行光类型光照合成阶段的片段着色器。
#version 430 core
precision highp float;
precision mediump sampler2D;
const float PI = 3.141592;
const float Epsilon = 0.00001;
// Constant normal incidence Fresnel factor for all dielectrics.
const vec3 Fdielectric = vec3(0.220916301);
layout (location = 0) in vec2 inUV;
layout (location = 0) out vec4 color;
layout (input_attachment_index = 0, binding = 0) uniform subpassInput gbuffer0;
layout (input_attachment_index = 1, binding = 1) uniform subpassInput gbuffer1;
layout (input_attachment_index = 2, binding = 2) uniform subpassInput gbuffer2;
layout (input_attachment_index = 3, binding = 3) uniform subpassInput gbuffer3;
layout (set = 0, binding = 4) uniform samplerCube envTexture;
layout(set = 0, binding = 5) uniform GlobalUniform
{
mat4 view;
mat4 projection;
mat4 projectionOrtho;
mat4 viewProjection;
mat4 viewProjectionInv;
mat4 viewProjectionOrtho;
vec4 resolution;
vec4 cameraPosition;
} globalUniform;
layout (push_constant) uniform LightInfo{
vec4 color;
vec4 position;
vec4 direction;
vec4 typeAngle;
vec4 attenuation;
vec4 ambient;
vec4 diffuse;
vec4 specular;
} lightInfo;
vec4 getEnvChannel(vec3 location, float LOD)
{
return textureLod(envTexture, location, LOD);
}
float ndfGGX(float nh, float roughness)
{
float alphaSq = roughness * roughness;
float denom = (nh * nh) * (alphaSq - 1.0) + 1.0;
return alphaSq / (PI * denom * denom);
}
float gaSchlickG1(float cosTheta, float k)
{
return cosTheta / (cosTheta * (1.0 - k) + k);
}
float gaSchlickGGX(float nl, float nv, float roughness)
{
float r = roughness + 1.0;
float k = (r * r) / 8.0; // Epic suggests using this roughness remapping for analytic lights.
return gaSchlickG1(nl, k) * gaSchlickG1(nv, k);
}
vec3 fresnelSchlick(vec3 F0, float cosTheta)
{
return F0 + (vec3(1.0) - F0) * pow(1.0 - cosTheta, 5.0);
}
vec3 FresnelLerp(vec3 F0, vec3 F90, float cosA)
{
float t = pow(1 - cosA, 5.0); // ala Schlick interpoliation
return mix(F0, F90, t);
}
float DisneyDiffuse(float NdotV, float NdotL, float LdotH, float perceptualRoughness)
{
float fd90 = 0.5 + 2 * LdotH * LdotH * perceptualRoughness;
// Two schlick fresnel term
float lightScatter = (1 + (fd90 - 1) * pow((1 - NdotL),5.0));
float viewScatter = (1 + (fd90 - 1) * pow((1 - NdotV),5.0));
return lightScatter * viewScatter;
}
void main()
{
// Sample input textures to get shading model params.
vec4 albedoTex = subpassLoad(gbuffer0);
vec4 normalTex = subpassLoad(gbuffer1);
vec4 emissionTex = subpassLoad(gbuffer2);
float depth = subpassLoad(gbuffer3).x;
vec3 albedo = albedoTex.xyz;
float ao = albedoTex.w;
float perceptualRoughness = normalTex.z;
float metalness = emissionTex.w;
float roughness = perceptualRoughness * perceptualRoughness;
vec4 clipPosition;
clipPosition.w = 1.0;
clipPosition.z = depth;
clipPosition.xy = inUV * 2.0 - 1.0;
vec4 worldPosition = globalUniform.viewProjectionInv * clipPosition;
worldPosition.xyz = worldPosition.xyz / worldPosition.w;
// Outgoing light direction (vector from world-space fragment position to the "eye").
vec3 Lo = normalize(globalUniform.cameraPosition.xyz - worldPosition.xyz);
// decode norm
float scale = 1.7777;
vec3 nn = vec3(normalTex.x, normalTex.y, 0) * vec3(2 * scale, 2 * scale, 0) + vec3(-scale, -scale, 1);
float g = 2.0 / dot(nn.xyz, nn.xyz);
// Get current fragment's normal and transform to world space.
vec3 N = vec3(g * nn.xy, g - 1);
// Angle between surface normal and outgoing light direction.
float nv = max(0.0, dot(N, Lo));
// Specular reflection vector.
vec3 Lr = reflect(-Lo, N);
// Fresnel reflectance at normal incidence (for metals use albedo color).
vec3 F0 = mix(Fdielectric, albedo, metalness);
// Direct lighting calculation for analytical lights.
vec3 directLighting = vec3(0);
// Get diffuse contribution factor (as with direct lighting).
float kd = (1 - metalness) * 0.779083699;
vec3 Li = vec3(0, 0, 1);
vec3 Lradiance = lightInfo.color.xyz * lightInfo.color.w;
Li = -normalize(lightInfo.direction.xyz);
// Half-vector between Li and Lo.
vec3 Lh = normalize(Li + Lo);
// Calculate angles between surface normal and various light vectors.
float nl = max(0.0, dot(N, Li));
float nh = max(0.0, dot(N, Lh));
float hl = max(0.0, dot(Lh, Li));
float diffuseTerm = DisneyDiffuse(nv, nl, hl, perceptualRoughness);
vec3 diffuseBRDF = kd * albedo * diffuseTerm;
vec3 F = fresnelSchlick(F0, hl);
float D = ndfGGX(nh, roughness);
float G = gaSchlickGGX(nl, nv, roughness);
float specularTerm = (D * G) * PI / max(Epsilon, 4.0 * nl * nv);
specularTerm = sqrt(max(0.0001, specularTerm));
vec3 specularBRDF = F * specularTerm;
directLighting += (diffuseBRDF + specularBRDF) * Lradiance * nl;
// Ambient lighting (IBL).
vec3 ambientLighting;
{
// Sample diffuse irradiance at normal direction.
vec3 irradiance = getEnvChannel(N, 10.0).xyz;
// Calculate Fresnel term for ambient lighting.
// Since we use pre-filtered cubemap(s) and irradiance is coming from many directions
// use nv instead of angle with light's half-vector (nh above).
// See: https://seblagarde.wordpress.com/2011/08/17/hello-world/
vec3 F = fresnelSchlick(F0, nv);
// Irradiance map contains exitant radiance assuming Lambertian BRDF, no need to scale by 1/PI here either.
vec3 diffuseIBL = kd * albedo * irradiance;
// Sample pre-filtered specular reflection environment at correct mipmap level.
vec3 specularIrradiance = getEnvChannel(Lr, perceptualRoughness * 4.0).xyz;
// Total specular IBL contribution.
float surfaceReduction = 1.0 - 0.28 * roughness * perceptualRoughness;
float smoothness = 1 - perceptualRoughness;
float grazingTerm = clamp((smoothness + (1 - kd)), 0.0, 1.0);
vec3 specularIBL = surfaceReduction * specularIrradiance * FresnelLerp(F0, vec3(grazingTerm), nv);
// Total ambient lighting contribution.
ambientLighting = diffuseIBL + specularIBL;
}
color = vec4(directLighting + ambientLighting * ao + emissionTex.xyz, 1.0);
}
以下是点光源类型光照合成阶段的片段着色器。
#version 430 core
precision highp float;
precision mediump sampler2D;
const float PI = 3.141592;
const float Epsilon = 0.00001;
// Constant normal incidence Fresnel factor for all dielectrics.
const vec3 Fdielectric = vec3(0.220916301);
layout (location = 0) in vec2 inUV;
layout (location = 0) out vec4 color;
layout (input_attachment_index = 0, binding = 0) uniform subpassInput gbuffer0;
layout (input_attachment_index = 1, binding = 1) uniform subpassInput gbuffer1;
layout (input_attachment_index = 2, binding = 2) uniform subpassInput gbuffer2;
layout (input_attachment_index = 3, binding = 3) uniform subpassInput gbuffer3;
layout(set = 0, binding = 5) uniform GlobalUniform
{
mat4 view;
mat4 projection;
mat4 projectionOrtho;
mat4 viewProjection;
mat4 viewProjectionInv;
mat4 viewProjectionOrtho;
vec4 resolution;
vec4 cameraPosition;
} globalUniform;
layout (push_constant) uniform LightInfo{
vec4 color;
vec4 position;
vec4 direction;
vec4 typeAngle;
vec4 attenuation;
vec4 ambient;
vec4 diffuse;
vec4 specular;
} lightInfo;
float ndfGGX(float nh, float roughness)
{
float alphaSq = roughness * roughness;
float denom = (nh * nh) * (alphaSq - 1.0) + 1.0;
return alphaSq / (PI * denom * denom);
}
float gaSchlickG1(float cosTheta, float k)
{
return cosTheta / (cosTheta * (1.0 - k) + k);
}
float gaSchlickGGX(float nl, float nv, float roughness)
{
float r = roughness + 1.0;
float k = (r * r) / 8.0; // Epic suggests using this roughness remapping for analytic lights.
return gaSchlickG1(nl, k) * gaSchlickG1(nv, k);
}
vec3 fresnelSchlick(vec3 F0, float cosTheta)
{
return F0 + (vec3(1.0) - F0) * pow(1.0 - cosTheta, 5.0);
}
float DisneyDiffuse(float NdotV, float NdotL, float LdotH, float perceptualRoughness)
{
float fd90 = 0.5 + 2 * LdotH * LdotH * perceptualRoughness;
// Two schlick fresnel term
float lightScatter = (1 + (fd90 - 1) * pow((1 - NdotL),5.0));
float viewScatter = (1 + (fd90 - 1) * pow((1 - NdotV),5.0));
return lightScatter * viewScatter;
}
void main()
{
// Sample input textures to get shading model params.
vec4 albedoTex = subpassLoad(gbuffer0);
vec4 normalTex = subpassLoad(gbuffer1);
vec4 emissionTex = subpassLoad(gbuffer2);
float depth = subpassLoad(gbuffer3).x;
vec3 albedo = albedoTex.xyz;
float ao = albedoTex.w;
float perceptualRoughness = normalTex.z;
float metalness = emissionTex.w;
float roughness = perceptualRoughness * perceptualRoughness;
vec4 clipPosition;
clipPosition.w = 1.0;
clipPosition.z = depth;
clipPosition.xy = inUV * 2.0 - 1.0;
vec4 worldPosition = globalUniform.viewProjectionInv * clipPosition;
worldPosition.xyz = worldPosition.xyz / worldPosition.w;
// Outgoing light direction (vector from world-space fragment position to the "eye").
vec3 Lo = normalize(globalUniform.cameraPosition.xyz - worldPosition.xyz);
// decode norm
float scale = 1.7777;
vec3 nn = vec3(normalTex.x, normalTex.y, 0) * vec3(2 * scale, 2 * scale, 0) + vec3(-scale, -scale, 1);
float g = 2.0 / dot(nn.xyz, nn.xyz);
// Get current fragment's normal and transform to world space.
vec3 N = vec3(g * nn.xy, g - 1);
// Angle between surface normal and outgoing light direction.
float nv = max(0.0, dot(N, Lo));
// Specular reflection vector.
vec3 Lr = reflect(-Lo, N);
// Fresnel reflectance at normal incidence (for metals use albedo color).
vec3 F0 = mix(Fdielectric, albedo, metalness);
// Direct lighting calculation for analytical lights.
vec3 pointLighting = vec3(0);
// Get diffuse contribution factor (as with direct lighting).
float kd = (1 - metalness) * 0.779083699;
vec3 Li = vec3(0, 0, 1);
vec3 Lradiance = lightInfo.color.xyz * lightInfo.color.w;
Li = lightInfo.position.xyz - worldPosition.xyz;
float dist = length(Li) + 0.001;
Li = Li / dist;
float atten = 1.0 - (dist * dist) / (lightInfo.attenuation.w * lightInfo.attenuation.w);
atten = max(0, atten);
atten *= lightInfo.attenuation.x;
Lradiance *= atten;
// Half-vector between Li and Lo.
vec3 Lh = normalize(Li + Lo);
// Calculate angles between surface normal and various light vectors.
float nl = max(0.0, dot(N, Li));
float nh = max(0.0, dot(N, Lh));
float hl = max(0.0, dot(Lh, Li));
float diffuseTerm = DisneyDiffuse(nv, nl, hl, perceptualRoughness);
vec3 diffuseBRDF = kd * albedo * diffuseTerm;
vec3 F = fresnelSchlick(F0, hl);
float D = ndfGGX(nh, roughness);
float G = gaSchlickGGX(nl, nv, roughness);
float specularTerm = (D * G) * PI / max(Epsilon, 4.0 * nl * nv);
specularTerm = sqrt(max(0.0001, specularTerm));
vec3 specularBRDF = F * specularTerm;
pointLighting += (diffuseBRDF + specularBRDF) * Lradiance * nl;
color = vec4(pointLighting, 1.0);
}
以下是聚光灯类型光照合成阶段的片段着色器。
#version 430 core
precision highp float;
precision mediump sampler2D;
const float PI = 3.141592;
const float Epsilon = 0.00001;
// Constant normal incidence Fresnel factor for all dielectrics.
const vec3 Fdielectric = vec3(0.220916301);
layout (location = 0) in vec2 inUV;
layout (location = 0) out vec4 color;
layout (input_attachment_index = 0, binding = 0) uniform subpassInput gbuffer0;
layout (input_attachment_index = 1, binding = 1) uniform subpassInput gbuffer1;
layout (input_attachment_index = 2, binding = 2) uniform subpassInput gbuffer2;
layout (input_attachment_index = 3, binding = 3) uniform subpassInput gbuffer3;
layout(set = 0, binding = 5) uniform GlobalUniform
{
mat4 view;
mat4 projection;
mat4 projectionOrtho;
mat4 viewProjection;
mat4 viewProjectionInv;
mat4 viewProjectionOrtho;
vec4 resolution;
vec4 cameraPosition;
} globalUniform;
layout (push_constant) uniform LightInfo{
vec4 color;
vec4 position;
vec4 direction;
vec4 typeAngle;
vec4 attenuation;
vec4 ambient;
vec4 diffuse;
vec4 specular;
} lightInfo;
float ndfGGX(float nh, float roughness)
{
float alphaSq = roughness * roughness;
float denom = (nh * nh) * (alphaSq - 1.0) + 1.0;
return alphaSq / (PI * denom * denom);
}
float gaSchlickG1(float cosTheta, float k)
{
return cosTheta / (cosTheta * (1.0 - k) + k);
}
float gaSchlickGGX(float nl, float nv, float roughness)
{
float r = roughness + 1.0;
float k = (r * r) / 8.0; // Epic suggests using this roughness remapping for analytic lights.
return gaSchlickG1(nl, k) * gaSchlickG1(nv, k);
}
vec3 fresnelSchlick(vec3 F0, float cosTheta)
{
return F0 + (vec3(1.0) - F0) * pow(1.0 - cosTheta, 5.0);
}
float DisneyDiffuse(float NdotV, float NdotL, float LdotH, float perceptualRoughness)
{
float fd90 = 0.5 + 2 * LdotH * LdotH * perceptualRoughness;
// Two schlick fresnel term
float lightScatter = (1 + (fd90 - 1) * pow((1 - NdotL),5.0));
float viewScatter = (1 + (fd90 - 1) * pow((1 - NdotV),5.0));
return lightScatter * viewScatter;
}
void main()
{
// Sample input textures to get shading model params.
vec4 albedoTex = subpassLoad(gbuffer0);
vec4 normalTex = subpassLoad(gbuffer1);
vec4 emissionTex = subpassLoad(gbuffer2);
float depth = subpassLoad(gbuffer3).x;
vec3 albedo = albedoTex.xyz;
float ao = albedoTex.w;
float perceptualRoughness = normalTex.z;
float metalness = emissionTex.w;
float roughness = perceptualRoughness * perceptualRoughness;
vec4 clipPosition;
clipPosition.w = 1.0;
clipPosition.z = depth;
clipPosition.xy = inUV * 2.0 - 1.0;
vec4 worldPosition = globalUniform.viewProjectionInv * clipPosition;
worldPosition.xyz = worldPosition.xyz / worldPosition.w;
// Outgoing light direction (vector from world-space fragment position to the "eye").
vec3 Lo = normalize(globalUniform.cameraPosition.xyz - worldPosition.xyz);
// decode norm
float scale = 1.7777;
vec3 nn = vec3(normalTex.x, normalTex.y, 0) * vec3(2 * scale, 2 * scale, 0) + vec3(-scale, -scale, 1);
float g = 2.0 / dot(nn.xyz, nn.xyz);
// Get current fragment's normal and transform to world space.
vec3 N = vec3(g * nn.xy, g - 1);
// Angle between surface normal and outgoing light direction.
float nv = max(0.0, dot(N, Lo));
// Specular reflection vector.
vec3 Lr = reflect(-Lo, N);
// Fresnel reflectance at normal incidence (for metals use albedo color).
vec3 F0 = mix(Fdielectric, albedo, metalness);
// Direct lighting calculation for analytical lights.
vec3 spotLighting = vec3(0);
// Get diffuse contribution factor (as with direct lighting).
float kd = (1 - metalness) * 0.779083699;
vec3 Li = vec3(0, 0, 1);
vec3 Lradiance = lightInfo.color.xyz * lightInfo.color.w;
Li = lightInfo.position.xyz - worldPosition.xyz;
float dist = length(Li) + 0.001;
Li = Li / dist;
float atten = 1.0 / (lightInfo.attenuation.x +
lightInfo.attenuation.y * dist + lightInfo.attenuation.z * (dist * dist));
float theta = dot(Li, normalize(-lightInfo.direction.xyz));
float epsilon = lightInfo.typeAngle.y - lightInfo.typeAngle.z;
float intensity = clamp((theta - lightInfo.typeAngle.z) / epsilon, 0.0, 1.0);
Lradiance *= atten * intensity;
// Half-vector between Li and Lo.
vec3 Lh = normalize(Li + Lo);
// Calculate angles between surface normal and various light vectors.
float nl = max(0.0, dot(N, Li));
float nh = max(0.0, dot(N, Lh));
float hl = max(0.0, dot(Lh, Li));
float diffuseTerm = DisneyDiffuse(nv, nl, hl, perceptualRoughness);
vec3 diffuseBRDF = kd * albedo * diffuseTerm;
vec3 F = fresnelSchlick(F0, hl);
float D = ndfGGX(nh, roughness);
float G = gaSchlickGGX(nl, nv, roughness);
float specularTerm = (D * G) * PI / max(Epsilon, 4.0 * nl * nv);
specularTerm = sqrt(max(0.0001, specularTerm));
vec3 specularBRDF = F * specularTerm;
spotLighting += (diffuseBRDF + specularBRDF) * Lradiance * nl;
color = vec4(spotLighting, 1.0);
}