BackFace描边方案
本文最后更新于16 天前,其中的信息可能已经过时,如有错误请留言

Houdini平滑法线

Reference:(53 封私信 / 18 条消息) 少女前线2渲染还原分析—以Cheeta为例 – 知乎

法线外扩 https://zhuanlan.zhihu.com/p/508826073

名称含义
法线外扩顶点沿法线方向移动,是几何处理
BackFace显示模型背面,是面剔除/渲染设置
Inverted Hull将“法线外扩 + 正面剔除”组合起来的完整描边方案
平滑法线让外扩方向更连续,改善描边质量
  • 一般外部的轮廓线采用BackFace,内部的描边线采用法线外扩?
  • 导入模型:
    • Convert to Y-up Coordinate System 可以开,因为 Unity 和 Houdini 都是 Y-up,通常不会有问题
    • Import As New Scene
  • 新建

节点说明

  • Capture Override:这个节点保留骨骼蒙皮信息。后面的处理必须从它之后开始,否则容易丢失 Skin Weights
  • Fuse:作用是把位置非常接近的点合并,方便后面重新计算平滑法线
    • 很多游戏模型在 UV 接缝、硬边、材质分割处会有重复顶点。这些重复顶点虽然位置一样,但法线可能不一样。描边如果直接用这些法线,会在接缝处断裂
    • Snap Distance = 0.0001
      Fuse Snapped Points = 开
      Remove Repeated Vertices and Degenerate Primitives = 开
      Remove Unused Points from Degenerate Primitives = 开
  • Normal 重新计算平滑法线
    • Compute Normals = 开
      Add Normals to = Points
      Weighting Method = By Vertex Angle
      Keep Original Normal Where Computed Normal Is Zero = 开
  • PolyFrame
    • Entity = Points
      Style = Texture UV Gradient
      Tangent Name = tangent
      Bitangent Name = tangentv
      Make Frame Orthogonal = 开
  • Attribute Wrangle
    • 主要做4件事情
      • 1. 读取原模型法线 N
        2. 从第二输入读取平滑描边法线
        3. 读取切线 tangent,构建切线空间
        4. 把平滑描边法线转成切线空间,并写入 uv3 / uv4
    • Houdini 和 Unity 的 UV 命名对应关系大致是:
      • Houdini uv → Unity UV0
      • Houdini uv2 → Unity UV1
      • Houdini uv3 → Unity UV2
      • Houdini uv4 → Unity UV3

不同材质球怎么知道读取哪个 UV?

就比如正常渲染的皮肤材质球和用于描边效果的材质球

看 Shader 代码里的 TEXCOORD

struct Attributes
{
    float4 positionOS : POSITION;
    float3 normalOS   : NORMAL;
    float2 uv         : TEXCOORD0;
};

//然后采样贴图时:
float2 uv = TRANSFORM_TEX(input.uv, _BaseMap);
half4 baseTex = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);

TEXCOORD0 = Unity UV0,所以你的皮肤 BaseMap、NormalMap、RMO 等,默认都是读 UV0

Unity 描边方案

核心原理

BackFace 描边法

  • 1. 正常 Pass 先渲染角色本体。
  • 2. Renderer Feature 额外把角色 Mesh 再画一遍。
  • 3. 第二次绘制时只画背面:Cull Front。
  • 4. 顶点沿法线方向在屏幕空间外扩。
  • 5. 外扩出来的背面部分露在模型边缘,形成黑色轮廓线。

不是后处理描边,而是几何体外扩描边

描边方案

  • 角色原本负责皮肤、衣服、头发、丝袜、Ramp、PBR、MatCap 等正常渲染
  • GF2_OutlineOnly 材质:
    只负责描边,不负责角色本体颜色。
  • GF2CharacterOutlineFeature:
    在 URP 渲染流程中,把指定 Layer 的角色 Mesh 额外绘制一遍,并强制使用 GF2_OutlineOnly 材质

这样做的好处是:不需要修改每个角色 Shader。身体、头发、丝袜、脸部都可以继续使用原本材质,描边由 Renderer Feature 统一处理

Renderer Feature

Renderer Feature 的作用是:找到指定 Layer 的角色物体,并用统一的 Outline 材质重新绘制一次。

// 设置要绘制的物体范围:指定 Layer + 指定渲染队列
filteringSettings = new FilteringSettings(
    RenderQueueRange.all,
    profile.outlineLayerMask
);

// 创建绘制设置,使用角色原本 Shader 的常规 Pass 作为筛选入口
DrawingSettings drawingSettings =
    CreateDrawingSettings(
        shaderTagIdList[0],
        ref renderingData,
        sortingCriteria
    );

// 关键:用 OutlineOnly 材质覆盖原材质
// 这样角色本体 Shader 不需要加 Outline Pass
drawingSettings.overrideMaterial = profile.outlineMaterial;
drawingSettings.overrideMaterialPassIndex = profile.outlineMaterialPassIndex;

// 执行额外绘制
context.DrawRenderers(
    renderingData.cullResults,
    ref drawingSettings,
    ref filteringSettings,
    ref renderStateBlock
);

这段代码的关键点是:

overrideMaterial = profile.outlineMaterial

它表示:虽然场景里的角色原本用的是身体、头发、丝袜等材质,但 Renderer Feature 这次额外绘制时,会临时强制使用 GF2_OutlineOnly 材质。

所以最终效果是:

原材质画角色本体
OutlineOnly 材质额外画一遍描边

OutlineOnly Shader

  • Cull Front ,这样距离摄像机近的正面永远不会被渲染,不会遮挡角色原本的材质
  • 使用模型法线或平滑法线作为外扩方向
    • MeshNormal:
      直接用模型原法线外扩,简单稳定,但硬边和切缝处容易断。
    • UV2_UV3_TangentNormal:
      从 UV2 / UV3 读取预烘焙的平滑描边法线,线条更连续。
// 把模型法线转换到世界空间
half3 meshNormalWS =
    normalize(TransformObjectToWorldNormal(input.normalOS));

// 默认使用模型原始法线
half3 outlineNormalWS = meshNormalWS;

// 如果选择 UV 平滑法线,则从 UV 中读取并转回世界空间
if (_OutlineNormalSource > 0.5h)
{
    half3 outlineNormalTS = GetOutlineNormalTS(input);

    half3 smoothNormalWS =
        OutlineTangentNormalToWorld(
            outlineNormalTS,
            meshNormalWS,
            input.tangentOS
        );

    outlineNormalWS = normalize(
        lerp(meshNormalWS, smoothNormalWS, _OutlineSmoothNormalStrength)
    );
}
  • 屏幕空间像素宽度描边
    • 为了让描边宽度更接近“屏幕像素宽度”,不是直接在模型空间移动顶点,而是在裁剪空间中移动 positionCS.xy
// 世界空间位置转裁剪空间
float4 positionCS = posInput.positionCS;

// 把描边法线转到观察空间
half3 normalVS =
    normalize(mul((float3x3)UNITY_MATRIX_V, outlineNormalWS));

// 使用观察空间法线的 xy 作为屏幕扩张方向
float2 rawDir = normalVS.xy;
float rawLen = length(rawDir);

float2 dir = rawDir;

// 如果法线在屏幕上的投影太短,就不外扩,避免抖动和碎线
if (rawLen < _OutlineMinViewNormalLength)
{
    dir = float2(0.0, 0.0);
}
else
{
    dir /= rawLen;
}

// 将像素宽度换算为裁剪空间偏移
float2 pixelToClip =
    float2(2.0 / _ScreenParams.x, 2.0 / _ScreenParams.y);

// 按屏幕像素宽度外扩
positionCS.xy += dir * pixelWidth * pixelToClip * positionCS.w;
  • 距离衰减:用于解决远距离观察时描边过粗、过黑、糊成一团的问题
half GetOutlineDistanceFade(float3 positionWS)
{
    float dist = distance(_WorldSpaceCameraPos.xyz, positionWS);

    float t = saturate(
        (dist - _OutlineDistanceFadeStart) /
        (_OutlineDistanceFadeEnd - _OutlineDistanceFadeStart)
    );

    // 平滑过渡
    t = t * t * (3.0 - 2.0 * t);

    // 近处为 1,远处变为 _OutlineDistanceFadeMin
    half fade = lerp(1.0h, _OutlineDistanceFadeMin, (half)t);

    return lerp(1.0h, fade, _UseOutlineDistanceFade);
}

最终参与描边宽度计算:

float pixelWidth =
    _OutlineWidthPixels *
    widthMask *
    distanceFade *
    innerSuppress *
    _UseOutline;
  • 内侧线压制:BackFace 描边容易在腋下、手指缝、胸侧、小饰品等内凹区域产生黑块。因此增加了 Inner Line Suppress 来压制内侧杂线。
half GetInnerSuppressMask(float viewNormalXYLength)
{
    half edgeMask = smoothstep(
        _OutlineInnerSuppressThreshold,
        1.0h,
        saturate((half)viewNormalXYLength)
    );

    edgeMask = pow(saturate(edgeMask), _OutlineInnerSuppressPower);

    half suppress = lerp(
        1.0h,
        edgeMask,
        _OutlineInnerSuppressStrength
    );

    return lerp(1.0h, suppress, _UseOutlineInnerSuppress);
}
  • 宽度 Mask:Mask 用来控制不同区域的描边粗细
    • 当前还没有正式使用 Mask,所以通常保持:Use Outline Width Mask = 0
      如果后续在 Houdini 或 Unity Baker 中写入区域 Mask,可以打开:
    • Use Outline Width Mask = 1 Outline Width Mask Source = UV3Y
half GetOutlineWidthMask(Attributes input)
{
    half rawMask = 1.0h;

    if (_OutlineWidthMaskSource > 0.5h && _OutlineWidthMaskSource < 1.5h)
        rawMask = input.color.a;

    if (_OutlineWidthMaskSource > 1.5h && _OutlineWidthMaskSource < 2.5h)
        rawMask = input.uv2.y;

    if (_OutlineWidthMaskSource > 2.5h)
        rawMask = input.uv3.y;

    rawMask = saturate(rawMask);
    rawMask = pow(rawMask, _OutlineWidthMaskPower);
    rawMask = max(rawMask, _OutlineMinWidthMask);

    return lerp(1.0h, rawMask, _UseOutlineWidthMask);
}

Inspector 中可调整参数说明

  • Outline Shape
参数作用
Use Outline是否启用描边。1 为开启,0 为关闭。
Outline Width Pixels描边宽度,近似屏幕像素单位。值越大描边越粗。
Outline Color描边颜色。二游角色一般不要用纯黑,建议用深棕黑、深灰黑或深紫黑。

  • Outline Normal Source
参数作用
Outline Normal Source选择描边外扩使用的法线来源。
MeshNormal使用模型原始法线。简单稳定,但硬边、切缝处可能断。
UV1_UV2_TangentNormal从 Unity UV1 / UV2 读取平滑描边法线。
UV2_UV3_TangentNormal从 Unity UV2 / UV3 读取平滑描边法线,当前推荐预留方案。
Outline Smooth Normal Strength平滑法线混合强度。0 为原始法线,1 为完全使用平滑法线。
Outline Min View Normal Length抑制正对镜头方向的异常外扩。值过大可能导致部分线消失,值过小可能出现碎线。
Outline Clip Space Z Offset深度偏移,用于缓解描边和本体深度冲突。一般保持 0。

  • Outline Distance Fade
参数作用
Use Outline Distance Fade是否启用远距离描边衰减。
Distance Fade Start从多远开始让描边变细。
Distance Fade End到多远时达到最细状态。
Distance Fade Min Width远距离最小保留描边宽度比例。值越低,远处描边越细。

推荐作用:

近距离保留清晰线条。
远距离自动变细,避免角色变成一团黑边。

  • Outline Inner Line Suppress
参数作用
Use Inner Line Suppress是否启用内侧线压制。
Inner Suppress Strength压制强度。越大,腋下、手指缝、小饰品黑线越少。
Inner Suppress Threshold压制阈值。越大,压制范围越广。
Inner Suppress Power压制曲线。越大,过渡越集中,线条更干净但可能更容易断。

调参思路:

腋下、手指缝黑块太重:
提高 Strength / Threshold。

外轮廓也被压没:
降低 Strength / Threshold。
  • Outline Width Mask
参数作用
Use Outline Width Mask是否启用宽度 Mask。
Outline Width Mask Source选择从哪里读取宽度 Mask。
Constant不读 Mask,全身统一宽度。
VertexColorA从顶点色 A 通道读取宽度。
UV2Y从 Unity UV2.y 读取宽度。
UV3Y从 Unity UV3.y 读取宽度,当前预留推荐通道。
Outline Width Mask Power控制 Mask 曲线。大于 1 会让灰色区域更细。
Outline Min Width MaskMask 最低保留值,防止某些地方完全断线。

当前阶段一般关闭:

Use Outline Width Mask = 0

后续需要手动画区域粗细时再打开。


  • Advanced
参数作用
Outline Bitangent Sign修正切线空间副切线方向。只有使用 UV 平滑法线时才重要。描边方向异常时可在 1 和 -1 之间切换。

  • Render State
参数作用
Outline Cull Mode描边绘制面。BackFace 描边应设置为 Front,即 Cull Front,只画背面。
Outline ZTest深度测试方式。一般用 LessEqual,避免描边乱穿透。
Debug Mode调试模式。0 为正常描边,其他模式用于检查法线、Mask、距离衰减、内线压制等。

迭代改进与问题修复记录

  • 最初是在身体 Shader 里直接加入第二个 Outline Pass:
ForwardLit Pass:正常渲染身体。
Outline Pass:Cull Front 后沿法线外扩,生成描边。

优点是效果直接,身体材质自己就能描边。
问题是:如果脸、头发、丝袜、衣服都有不同 Shader,就需要每个 Shader 都复制一份 Outline Pass,维护成本高。

  • 头发和丝袜特殊处理预留

Renderer Feature 中预留了不同 Profile:

Body / Opaque
Hair / Alpha Reserved
Stocking / Transparent Reserved

后续可以通过不同 Layer + 不同 OutlineOnly 材质,让身体、头发、丝袜分别使用不同描边参数

Reference:https://zhuanlan.zhihu.com/p/508826073

学习笔记如有侵权,请提醒我,我会马上删除
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇