Unity URP 仿原神渲染解析

发布时间 2023-09-17 17:10:14作者: AE酱

Outline Pass

用于渲染轮廓。这个 Pass 看起来比较简单,就是对模型正面剔除后,将背面沿法线偏移线宽的距离,然后直接用轮廓线的颜色渲染背面。

ShaderLab

Pass {
    Name "Outline"
    Tags {
        "LightMode" = "SRPDefaultUnlit"
    }

    Cull Front  // 进行了正面剔除

    HLSLPROGRAM
    #pragma vertex OutlinePassVertex        // 顶点着色器: OutlinePassVertex
    #pragma fragment OutlinePassFragment    // 片段着色器: OutlinePassFragment

    #include "ToonInput.hlsl"
    #include "ToonOutlinePass.hlsl"
    ENDHLSL
}

ToonOutlinePass.hlsl

先来看一些 ToonOutlinePass.hlsl 里用到的东西。


VertexPositionInputs

其中顶点位置输入 VertexPositionInputs 结构体定义在 "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" 中,内容如下:

struct VertexPositionInputs {
    float3 positionWS; // World space position
    float3 positionVS; // View space position
    float4 positionCS; // Homogeneous clip space position
    float4 positionNDC;// Homogeneous normalized device coordinates
};

GetVertexPositionInputs(float3 positionOS) 将物体空间下的顶点坐标转换为世界空间、观察空间、裁剪空间、NDC下的坐标,填充给 VertexPositionInputs 结构体并返回。

VertexPositionInputs GetVertexPositionInputs(float3 positionOS) {
    VertexPositionInputs input;
    input.positionWS = TransformObjectToWorld(positionOS);
    input.positionVS = TransformWorldToView(input.positionWS);
    input.positionCS = TransformWorldToHClip(input.positionWS);

    float4 ndc = input.positionCS * 0.5f;
    input.positionNDC.xy = float2(ndc.x, ndc.y * _ProjectionParams.x) + ndc.w;
    input.positionNDC.zw = input.positionCS.zw;

    return input;
}

VertexNormalInputs

顶点法线输入 VertexNormalInputs 结构体同样定义在 "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" 中,内容如下:

struct VertexNormalInputs { // 存储了世界空间下的 TBN 向量
    real3 tangentWS;
    real3 bitangentWS;
    float3 normalWS;
};

GetVertexNormalInputs(float3 normalOS, float4 tangentOS) 接收物体空间下的法线和切线,填充 VertexNormalInputs 结构体并返回。

VertexNormalInputs GetVertexNormalInputs(float3 normalOS, float4 tangentOS) {
    VertexNormalInputs tbn;

    // mikkts space compliant. only normalize when extracting normal at frag.
    real sign = real(tangentOS.w) * GetOddNegativeScale();
    tbn.normalWS = TransformObjectToWorldNormal(normalOS);
    tbn.tangentWS = real3(TransformObjectToWorldDir(tangentOS.xyz));
    tbn.bitangentWS = real3(cross(tbn.normalWS, float3(tbn.tangentWS))) * sign;
    return tbn;
}

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

struct Attributes {                     // 顶点着色器的输入
    float4 positionOS : POSITION;       // 物体空间下的顶点坐标
    float3 normalOS : NORMAL;           // 物体空间下的法线
    float4 tangentOS : TANGENT;         // 物体空间下的切线
    float2 uv : TEXCOORD0;              // uv坐标
};

struct Varyings {                       // 片段着色器的输入
    float2 uv : TEXCOORD0;              // uv坐标
    float4 positionCS : SV_POSITION;    // 裁剪空间下的顶点坐标
};

float3 GetOutlinePosition(VertexPositionInputs vertexInput, VertexNormalInputs normalInput) {
    float z = abs(vertexInput.positionVS.z);                        // 观察空间下的z分量
    float width = _OutlineWidth * saturate(z) * 0.001;              // 宽度和 z 的大小成正比,离摄像机越远,宽度越大
                                                                    // 抵消透视造成的远处的线条变细
                                                                    // 使得远近的线条看起来差不多宽

    return vertexInput.positionWS + normalInput.normalWS * width;   // 将世界空间下的顶点坐标沿法线偏移线宽的距离
}

// 顶点着色器
Varyings OutlinePassVertex(Attributes input) {
    // 填充顶点位置输入结构体
    VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
    // 填充顶点法线输入结构体
    VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);

    float3 positionWS = GetOutlinePosition(vertexInput, normalInput);   // 获取轮廓线的位置

    Varyings output = (Varyings)0;
    output.uv = TRANSFORM_TEX(input.uv, _BaseMap);              // 填充 uv 坐标
    output.positionCS = TransformWorldToHClip(positionWS);      // 填充裁剪空间下的顶点坐标

    return output;
}

// 片段着色器
half4 OutlinePassFragment(Varyings input) : SV_TARGET {
    return _OutlineColor;   // 直接返回顶点颜色
}