

UObject 解决的是三个问题:
-
反射 (Reflection)
-
垃圾回收 (Garbage Collection)
-
资产与序列化系统
这三件事在普通 C++ 中都没有。
UObject 的核心结构内存里实际上包含
含义:
| 字段 | 作用 |
|---|---|
| Class | 对象类型信息 |
| Outer | 所属对象(类似层级关系) |
| Name | 对象名称 |
| ObjectFlags | GC/编辑器/运行时状态 |
例如一个材质实例:
MI_Skin_001
Outer = /Game/Characters/Skin/
Class = UMaterialInstanceConstant
OuterClass
| 字段 | 作用 |
|---|---|
| Class | 对象类型信息 |
| Outer | 所属对象(类似层级关系) |
UClass + UProperty + UFunction反射系统

UE 编辑器能显示 Details 面板的原因UE 不使用 delete 管理对象,而是用 GC。
GC 判断引用关系依赖:
UPROPERTY()
UPROPERTY 指针。F → StructT → TemplateAActorA → ActorUTextureU → UObjectTArrayT → TemplateUObject 属于 Game Thread / Editor Thread 层 。渲染线程不直接使用 UObject。UMaterial
↓
FMaterialResourceFMaterialRenderProxy
↓
FMeshDrawCommand ↓
RHIUMaterialInstanceConstant
↓
FMaterialInstanceResource
↓
FMaterialShaderMap所有 UE 资产都是 UObject:UTextureUStaticMesh
USkeletalMesh
存在于:
UPackage
对应磁盘文件:
.uasset
反射系统的宏标记类需要加入 UE 的反射系统。
ENGINE_API
这是 DLL 导出宏(Windows symbol export)。
模块系统里:
class ENGINE_API UMaterial
等价于:
class __declspec(dllexport) UMaterial
或者:
class __declspec(dllimport) UMaterial
取决于当前模块。
UE 每个 module 都有类似宏:
CORE_API
ENGINE_API
RENDERCORE_API
RHI_API
目的是:
让类在不同 DLL 之间可见。
UMaterial 继承链:
UObject
↓
UMaterialInterface
↓
UMaterial
C++ + Unreal Header Tool (UHT)Header Files
↓
UnrealHeaderTool
↓
生成 *.generated.h
↓
C++ Compile解析 .Build.csUnreal Build ToolUHT 是 反射代码生成工具 UnrealBuildTool
↓
UnrealHeaderTool ↓
C++ Compiler解析 Build.cs
↓
调用 UHT
↓
生成 *.generated.h
↓
调用 C++ 编译器Editor
↓
UBT
↓
UHT
↓
MSVC / Clang
↓
Link
↓
DLL / EXE正常情况下这些属性只能在 UMaterial(Master Material)里改。但 UE 允许 Material Instance 覆盖其中一部分 。正常情况下这些属性只能在 UMaterialInstance 覆盖允许boolbOverride_BlendMode;EBlendMode;EMaterialShadingModelbool bOverride_ShadingModel;
EMaterialShadingModel ShadingModel;
bool bOverride_TwoSided;
bool TwoSided;这一栏是和渲染管线关系最强
Enumeration Objects
这是一个比较少见的扩展字段。
TArray<UAssetUserData*>UPhysicalMaterial*
-
碰撞反馈
-
脚步音效
-
Niagara hit effects
-
friction / restitution
几乎不进入渲染管线,也不会影响 shader 或 RHI。
Lightmass Settings
属于静态光照烘焙配置。
GI bake值会在烘焙阶段被 Lightmass 使用,但实时渲染只看到最终生成的 lightmap 纹理。
强相关(会影响 shader / PSO)
- Material Property Overrides
源码架构层看,这些字段都属于:
UMaterialInstance (UObject)
UMaterialInstance
↓
FMaterialInstanceResource
↓
FMaterial
↓
FMaterialShaderMap
↓
FMeshDrawCommand
↓
RHI
大多数 UI 字段都不会一路走到底。
EFieldActivationType 等,就是项目里的 Enum 资产。
enum 不是简单 C++ enum,而是可以作为资产存在,例如:
UENUM(BlueprintType)
enum class EFieldActivationType
{
...
};
编译后会生成一个 UEnum 对象,编辑器里可以浏览。
为了 材质参数系统支持 Enum 类型参数。
材质 graph 里可以有:
-
Static Switch
-
Static Bool
-
Static Enum(较少见)
当某些 Material Function / Parameter 需要枚举值时,编辑器需要知道这个枚举资产。
Material Instance 的 Details 面板里 70% 都是 UObject 资产字段,不是渲染系统字段。
真正会一路影响到 DX12 / RHI 的,其实只有:
-
静态参数
-
BasePropertyOverrides
-
Parent Material
-
纹理 / sampler 参数
左边 Details 面板里的 Shading Model ,是 材质资产级别的枚举属性 。材质资产级别的枚举属性 。根节点持有 Shading Model 这个属性,所以 Find Results 把它列出来了。根节点持有 Shading Model 这个属性这个材质走哪套 BRDF / lighting path
这类值最终会影响:
-
材质允许输出哪些属性
-
BasePass 里使用哪套光照分支
-
GBuffer 打包方式
-
Shader permutation
-
Mesh pass / PSO
Material Domain


Use Material AttributesGraph接口切换 。RasterizerState.CullModeTwo SidedShading Model = 算法
Profile = 参数为什么 UE 要拆成两层Shading Model
↓
启用 SSS lighting path
Subsurface Profile
↓
提供 SSS 参数

不是"纯编辑器内联封装块" ,而是一个真正的 UObject 资产 。节点本身是一个 UMaterialExpressionMaterialFunctionCall 。
也就是说,图里的"调用点"本身先是一个 UObject 节点对象;然后它再持有一个"被调用函数资产"的引用。节点本身是一个 UMaterialExpressionMaterialFunctionCall 。
右侧 Details 的:
Material Function = None
本质上就是:
C++
UMaterialFunctionInterface* MaterialFunction;
或者同类指针字段。
从 UE 架构上看,它更像:
C++/Shader Graph 里的"函数调用节点"
而不是:
纯 UI 级注释框 / Group / Inline macro block
普通节点对象
比如:
-
TextureSample
-
Multiply
-
Lerp
-
MaterialFunctionCall
这些都是图里的节点实例,本身是 UMaterialExpression 派生类。
例如:
UMaterialExpressionTextureSample
UMaterialExpressionMultiply
UMaterialExpressionMaterialFunctionCall
它们都是 UObject。
内部也有一张 graph,里面又有很多 UMaterialExpression 节点。
运行期没有"函数调用开销"这个概念,不是像 CPU 代码那样保留一层 call stack。
更像:
Graph级复用
↓
编译期内联展开
↓
生成统一 HLSL
-
修改 BRDF 输入
-
UV / height / normal 等 局部 shading
-
一些屏幕空间操作
-
使用已有的 lighting model
-
光照积分方式shadowing pipelinevisibility / geometry representationglobal illumination 方法GBuffer layout它无法改变:想实现类似 Disney BRDF 变体、cloth microfacet、anisotropic multi-scatter :只能近似。
真正正确的实现需要修改:
ShadingModels.ush
BRDF.usf
DeferredLightingCommon.usf
不是 shader tweak,而是 改变 pipeline stage 。UE 默认:
PCF / VSM / Virtual Shadow Map这些都需要改:
ShadowRendering.cpp
ShadowDepthPixelShader.usfMaterial Graph 做不到。

改 GI / AO 方法
UE5 默认:
Lumen
改 Lumen probe sampling增加 bent normal AOscreen space GI 改采样策略改动都在:
LumenSceneLighting.usf
ScreenProbeGather.usfMaterial Graph 无法介入。
Geometry 表达custom displacementtessellation replacementsigned distance field shading都需要改:
BasePassVertexShader.usf
Nanite shading pathMaterial Graph 只能消费结果。

multi-layer SSS

非常明显的"2D动画感渲染"(Anime Cel + 轮廓线 + 极端阴影分块) ,它在技术演讲和论文中被频繁引用,原因主要并不是"视觉是否更好看",而是它在 实时图形领域解决了一个长期困难的问题:3D实时渲染如何精确模拟手绘2D动画语言 。传统NPR(Non-Photorealistic Rendering)通常只做到:
-
Toon shading
-
Outline
-
Quantized lighting
但 Arc System Works 在 Xrd 的方案里做的是更激进的一件事:
他们不是让3D看起来像2D,而是让 3D obey 2D animation rules。
面向摄像机的法线重写 (Normal Editing)顶点级阴影控制关键帧形变而非物理正确形变轮廓线权重控制在技术论文中属于 NPR pipeline design case study ,因此被反复引用。手工编辑的顶点法线 + 特殊light vector 控制。
例如角色脸部:
-
鼻子
-
眼窝
-
下巴
这些地方的阴影在动画中是 固定结构,而不是随光线变化。
Arc 的做法:
-
重写顶点法线方向
-
让
dot(N,L)结果稳定 -
阴影边界固定
因此看起来像 赛璐璐动画分块阴影。
这个技术在SIGGRAPH和GDC被反复讲。
用真实透视:
Perspective projection脸会变形。
Xrd做了:
Per-bone scale correction
例如:
-
头骨 scale
-
眼睛位置
-
下巴长度
在动画帧里动态调整。
这本质是 camera-dependent rigging。
所以他们在演讲里经常说:
"We animate for the camera, not for the model."
轮廓线系统比普通Toon复杂很多
普通Toon outline:
backface extrusion

他们的案例非常 可复现
很多技术展示案例(例如电影渲染):
-
数据不可公开
-
pipeline复杂
-
离线渲染
但 Guilty Gear Xrd pipeline 是完整实时:
-
UE3/UE4
-
60fps
-
console hardware
因此在研究里属于:
industry-proven NPR pipeline
所以引用率很高。
技术圈喜欢它还有一个原因:它证明了一个观点
长期以来很多人认为:
"3D永远做不出真正的2D动画感。"
Arc基本上证明:
可以做到80-90%。
所以它成为:
-
GDC经典案例
-
SIGGRAPH course example
-
NPR研究引用对象
这是因为 它是"动画风格正确",不是"视觉复杂度高"。
如果用现代标准看:
-
GI 没有
-
SSS 没有
-
PBR 没有
-
材质复杂度低
但它追求的是:
overflow-visible!Animation readability
而不是:
overflow-visible!visual realism
在《罪恶装备》(Guilty Gear)系列,特别是《罪恶装备Xrd》及其后作(如Strive)的卡通渲染(NPR)技术中,
Inverted Hull(反转外壳/反转法线描边) 是一种核心的、基于3D模型的实时描边技术。
它不是游戏中的一个道具或招式,而是一种实现"2D动画效果"的技术手段,属于"三渲二"渲染管线的一部分。
反转法线: 将这个外层模型的面法线(Face Normals)反转(Invert)指向内部,与Post-processing(后处理)描边相比,Inverted Hull可以随动画动作在模型表面发生形变,描边在细节处更稳定。相比物理渲染,它能让角色在任何动态下都保持2D风格的黑边,是《罪恶装备》标志性3D建模2D感的关键。
什么是Mesh Split(网格分割)?
在传统的3D建模中,一个角色的身体(如手臂)通常是一个连续的网格。而《罪恶装备》为了达到"像2D动画"的描边效果,美术人员会在建模阶段,将模型中 不需要描边 或 需要特殊描边的区域从主体网格中"分割"出来。
在着色器中实现更细的描边,避免手部细节在描边后变得"又粗又糊"。实现内线条(Inner Lines): 除了外轮廓,角色脸部细节(如鼻子、嘴巴、眼睛的阴影)通常需要更细、更精细的线条。Mesh Split 将这些细节部件从脸部主体mesh中分离开来,使得着色器能对它们应用不同的描边逻辑。Mesh Mask (ID) 技术: 分割后的部件可以配合着色器中的Mesh Mask(ID)技术。通过给不同的分割块分配不同的Mask ID,美术人员可以精细地控制某一个部分"是否显示描边"或者"描边向内还是向外扩展"。 核心思路,不是依赖常规 PBR 光照去"自然形成"阴影,而是把 2D 动画里阴影该落在哪、边界该怎么走,尽量前置到模型、法线、拓扑、骨骼和少量控制参数中。复现这类效果,资产侧比 shader 侧更重要。难的是把"阴影边界的艺术意图"稳定地固化到资产里。手绘风格是错的。因为动漫脸部的阴影通常不是"鼻梁真实受光后自然形成",而是"艺术上规定左脸该黑到哪一条线"。脸部低频、干净、可预测的拓扑。鼻翼、颧骨、嘴角这类会导致阴影跳动的位置,尽量减少高频法线扰动。把一整片本来应该同亮同暗的区域做成法线场一致的"阴影岛"。把脸颊一圈法线朝一个更平的方向"梳"过去,让左脸到右脸形成更整齐的阈值过渡。这样 shader 里的阈值一切,边界就更接近 2D 原画。种是直接在 DCC 里手工改 vertex normal。比如 Blender / Maya 中,另一种是把某些区域的法线"借"给代理体。给脸做一个隐藏的球壳或简化代理面,然后把代理面的法线 transfer 到真正渲染的脸网格上。脸的 shading 不再由真实起伏决定,而由一个人为设计的低频体决定。这个思路在很多 Arc 风格复现项目里都被反复采用。GitHub+1
顶点色"阴影控制数据",顶点色。手工改 vertex normal。
https://www.cgzyw.com/65728.html

Blender 中,"自定义法线"确实常被称为
Custom Split Normals (自定义拆分法向)。Blender 里有多种法线概念,但当你进行手动编辑或使用插件调整法线方向时,系统会将其记录为"拆分法线数据"。Split Normals (拆分法向) :这是 Blender 内部控制平滑着色的核心机制。所属的每一个面(Loop)都可以有独立的法向信息。不同面拥有不同的法线方向,就能实现"硬边"效果。:当你手动修改这些拆分法线(例如使用法线编辑插件或"法线编辑"修改器)后,Blender 会生成一组固定的数据来覆盖自动计算的结果。 最新的 Blender 版本中,传统的"自动平滑"勾选项已移至 修改器 堆栈中(名为 按角度平滑 (Smooth by Angle) ),Custom Split Normals 的逻辑依然保留,用于支持更高级的手工法线控制。
虽然物理上只有一个"点",但在 3D 渲染中,它是按"面"来计算光影的。自定义法线(Custom Split Normals)操作的就是 顶点在特定面上的法向数据。
Blender 根据面的角度自动计算(所有连接到该点的面平分法向 = 平滑;每个面保留各自法向 = 硬边)。手动指定了这些法向。让一个点的所有 Split Normals 指向同一个方向 (模拟平整表面)可以让它们完全炸开(制造极端的硬边感)。 -
-
一个顶点连接了 4 个面,它就有 4 个 Split Normals。
-
当你进行"自定义"操作(比如使用 法线传递修改器 或 Alt+N 菜单)时,你其实是在给这 4 个位置分别写入数值。
-
一个点有
𝑛个 Split Normal ,但在进入 GPU 之前,引擎(或 Blender 导出插件)通常会将这个点 拆分成
𝑛 个不同的顶点。- 为什么"硬边"模型在游戏引擎里的顶点数(Vertex Count)通常比 Blender 里的原始点数要多。
-
光栅化器 根据这 3 个法线,计算出三角形内部每个像素点的过渡值。
-
像素着色器 (Fragment Shader) 接收到的是每个像素专属的插值法线。
你提到的"面上所有的像素"并不是用相同的面法线,而是进行 插值(Interpolation):
GPU 的插值阶段 (Rasterization)Shader 里开启了 Flat Shading(平滑关闭),
在 Smooth Shading(平滑着色) 下:
- 每个像素的法线都是从 3 个顶点的 Split Normal 混合出来的。
Mark Sharp(标记锐边) 的作用是告诉 Blender:"请在这里把法线'切断',强制制造出一条硬边 。" 这条边两侧的顶点法线会停止相互融合(Average) 。会为该顶点生成两个(或多个)相互独立的 Split Normals,分别指向两侧面的法线方向。
- 光影在这里产生断层,视觉上出现一条清晰的锐利棱角。
- 它和 Auto Smooth(自动平滑)的关系
它和 Auto Smooth(自动平滑)的关系
这是最关键的一点: Mark Sharp 单独存在时通常不起作用。
- 它必须配合 Smooth by Angle(按角度平滑) 修改器或旧版的 Auto Smooth 选项。
- 逻辑是 :除了根据角度自动判断硬边外,所有被标记为 Sharp 的边会被强制视为硬边。
方向索引图。mask 纹理。顶点色。脸部通常还要额外准备一套"阴影控制数据",编码一个 2D 向量角度索引,shader 把 light direction 投影到角色头部局部空间后,去和这张图比较,决定该像素是否进阴影。是"基于头部局部光向的手绘查表"。它不是物理阴影动画脸部的阴影是跟着角色头朝向和镜头语言定义的,不是跟世界空间 sun light 严格绑定的。脸部的阴影是跟着角色头朝向和镜头语言定义的,镜头语言定角色头朝向
基于头部局部光向的手绘查表
三渲二(Cel Shading)脸部渲染的核心痛点。
直接使用世界空间主光方向(World Space Light Dir)做脸部阴影,在二次元风格里通常是"灾难性"的。你提到的转到 Head Local Space(头部局部空间),本质上是为了解决以下几个关键问题:
- 解决"阴影乱跳"问题(动画一致性)
如果直接用世界空间光:
- 当角色转头时,光线相对于脸的角度会发生剧烈变化。
- 因为人脸结构复杂(鼻梁、眼窝),微小的旋转会导致阴影在脸上瞬间"瞬移"或产生难看的碎块。
- Local Space 的优势 :将光方向转到头部空间后,光线相对于脸的位置是相对固定或受控的。无论头怎么转,阴影的分布逻辑始终基于脸部骨架,保证了动画的平稳。
靠一张(或多张)SDF 贴图(Signed Distance Field)或者偏移法线贴图来控制阴影:贴图是贴在脸上的(UV 空间,关联 Local Space)。Shader 需要计算**"光线在头部局部坐标系里的水平偏角"**。
如果不用 Local Space,你就没法简单地用atan2(light.x,light.z)来索引那张控制阴影形状的 Map。
在影视或高品质手游中,为了让角色在任何角度都"好看",开发者会给头部添加 偏移量(Bias):
- 手动补光:即使主光在脑后,为了不让脸全黑,会在 Local Space 里人为地把光方向往正前方"拉"一点。
- 排除特定角度:比如强制不允许鼻梁阴影盖住另一侧眼睛。这种逻辑只有在头部局部空间(以脸中心为原点,正前方为 Z 轴)才好写逻辑。
硬件层面的真相:它只管插值
在 GPU 的渲染管线里,光栅化器(Rasterizer)是一个"盲目"的数学机器:
- 它接收顶点着色器(Vertex Shader)传出来的 3 个顶点的属性(位置、颜色、法线等)。
- 它对这 3 个属性进行重心坐标插值(Barycentric Interpolation),把结果传给像素着色器(Pixel Shader)。
- 关键点 :如果这 3 个顶点携带的法线方向不同,插值出来的结果在视觉上就是平滑的。
引擎并没有一个"关闭平滑"的开关, 硬边是通过"物理拆点"实现的:
- 当你在 Blender 里标记了 Mark Sharp 或使用了 Split Normal ,导出到 Unity/UE 时,引擎会在那条边上把顶点拆成两份。
- 面 A 的顶点 携带指向面 A 的法线;面 B 的顶点 携带指向面 B 的法线。
- 虽然它们坐标重合,但因为法线数据完全不同,光栅化器在面 A 内部插值时,用的全是 A 的法线;在面 B 内部插值时,用的全是 B 的法线。
- 结果:交界处没有过渡,看起来就是硬的。
- Unity (Standard/URP/HDRP) :默认材质球通常都是处理插值后的法线(Smooth),除非你在导入设置(Import Settings)里把
Normals设为Calculate并且把Smoothing Angle设为 0。 - Unreal Engine :同理。UE 的
Static Mesh导入时也会根据你的 FBX 数据决定是否拆分顶点。如果你的 FBX 里法线是平滑的,UE 就会渲染出平滑效果。
Blender 中的
Cel Ramp (通常被称为 Color Ramp 配合 Shader 实现的卡通着色/单元着色) 是一种用于创建卡通、手绘、非真实感渲染 (NPR) 风格材质的技术不同部位用不同 shadow threshold,而不是全身统一一个值:光线转到 head local,再和脸部贴图结合:GG 类风格一般不止一层暗部,而是 2 段甚至 3 段:叠加一个 AO / cavity 抑制,让鼻底、刘海根部、衣领内侧别因为主光变化而"全亮起来"。造型控制和动画控制绑得很深。GDC 官方介绍里也明确指出,它是在现代 3D 框架里重建传统 2D 表现。这个"重建"包含镜头驱动的形体修正,而不是单纯换个 shader。
GG 方案最值得学的不是 toon ramp,而是这个原则:
把不想交给实时光照系统决定的东西,前置到资产里。
这样 shader 只是执行器,不是决策器。
为什么叫 Cel Ramp?
该技术对应的是传统动画中使用的"赛璐璐片 (Celluloid/Cel)"风格。在这种风格中,为了节省时间,阴影通常只用一层或两层平涂的暗色表现,而不是真实物理光照下的平滑渐变。
Blender中,
Pixel Curvature Shading(基于像素的曲率着色) 通常指利用曲率(Curvature)数据来影响像素着色(Shading),从而在渲染时突出模型的几何细节(如边缘、凸起、凹陷)。它通常用于生成腔体遮罩(Cavity Masking)、材质混合或非真实感渲染(NPR/卡通渲染)的边缘强调。
曲率衡量的是曲面在该点处旋转的速度。
- 凸起(Convexity): 表面向外凸出(如边角),通常用于创建磨损纹理。
- 凹陷(Concavity): 表面向内凹入(如接缝、缝隙),通常用于创建积灰或环境光遮蔽(AO)效果
突出显示模型表面微小几何特征、边缘或弯曲程度的着色技术计算模型表面上每一个像素(或片元)的曲率信息,并根据曲率的大小映射到不同的颜色值,这种技术通过计算模型表面上每一个像素(或片元)的曲率信息,并根据曲率的大小映射到不同的颜色值,从而在渲染时产生一种类似于"分析视图"的效果,能够清晰地观察到物体的凹凸结构。 在像素级别(Pixel/Fragment Shader)进行计算的,而非顶点级别,因此能够捕捉到面片内部的细节,效果比定点着色更平滑、精确。曲率描绘了表面弯曲的程度。一般来说,凸起的边缘会映射为一种颜色(如高亮),平坦部分映射为另一种颜色,而凹陷的连接处映射为第三种颜色。用于快速检查模型曲面的光滑度、连续性或寻找几何缺陷。在艺术渲染或游戏开发中,用于增强模型边缘细节,使模型表面看起来更加立体和精致。曲率着色侧重于几何形状本身的变化,不依赖复杂的光源设定。相比传统着色:与Phong着色等主要计算光照方向不同
