目录

延迟渲染管线

CG Kit提供延迟渲染管线功能,可以从默认的前向渲染切换到延迟渲染。

使用延迟渲染管线

  1. 关闭多线程和MSAA。
    由于目前CG Kit延迟渲染不支持多线程和MSAA,启用延迟渲染前需要关闭多线程和MSAA。
    SetMultiThreadRendering(false);
    SetMSAA(false);
  2. 设置相机的渲染路径为延迟渲染,调用SetRenderingPath接口,渲染路径设置为“RENDER_PATH_TYPE_DEFERRED”。
    m_mainCamera->SetRenderingPath(RenderingPathType::RENDER_PATH_TYPE_DEFERRED);
  3. 添加延迟渲染所需shader,放置在“assets/shaders”目录下。
    • deferred_pbr_composition.vert.spv:延迟渲染光照合成顶点着色器。
    • deferred_pbr_gbuffer.frag.spv:延迟渲染G-Buffer阶段片段着色器。
    • deferred_pbr_gbuffer.vert.spv:延迟渲染G-Buffer阶段顶点着色器。
    • deferred_pbr_gbuffer_sky.frag.spv:延迟渲染G-Buffer阶段天空盒片段着色器。
    • directional_deferred_pbr_composition.frag.spv:延迟渲染光照合成平行光片段着色器。
    • point_deferred_pbr_composition.frag.spv:延迟渲染光照合成点光源片段着色器。
    • spot_deferred_pbr_composition.frag.spv:延迟渲染光照合成聚光灯片段着色器。

      以上提供的shader清单是CG Kit默认使用的,开发者可以根据需要进行修改,自定义shader编译替换对应的spv文件即可。

  4. 检查材质文件,确认与示例材质用法一致。
    1. 检查材质的techniques含有Deferred字段。
    2. 检查passes中配置有G-Buffer。
    3. 检查“assets/material”目录下存在deferred_composition.cgmat文件,该材质文件用于延迟渲染的光照合成阶段。
  5. 编译运行,即可得到与前向渲染一致的渲染效果。

自定义shader

[h2]deferred_pbr_gbuffer.vert

以下是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;
}

[h2]deferred_pbr_gbuffer.frag

以下是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);
}

[h2]deferred_pbr_gbuffer_sky.frag

以下是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);
}

[h2]deferred_pbr_composition.vert

以下是光照合成阶段的顶点着色器,直接使用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);
}

[h2]directional_deferred_pbr_composition.frag

以下是平行光类型光照合成阶段的片段着色器。

#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);
}

[h2]point_deferred_pbr_composition.frag

以下是点光源类型光照合成阶段的片段着色器。

#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);
}

[h2]spot_deferred_pbr_composition.frag

以下是聚光灯类型光照合成阶段的片段着色器。

#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);
}