分类目录归档:学习笔记

项目级车漆:从资产对标、HDRP StackLit 到全量延迟

这不是一次“把车漆调亮”的材质练习,而是一次从通用 Shading Model 到项目生产 Shader 的完整落地。我保留 Unity HDRP StackLit 的 VLayered BSDF 核心,围绕车辆资产重新构建材质输入、前向光照接入、反射控制、损伤与雨水模块以及自定义 Shader GUI;随后又把同一套车漆推入全量延迟路径,验证复杂分层材质在 GBuffer 中需要付出什么代价。

Unity 2022.3自研 HDRP 分支
3-LayerBase / Flake / Clear Coat
Forward + Deferred两条完整光照路径
4 × RGBA16全量延迟扩展 GBuffer
项目车辆展示场景中的最终车漆效果
最终延迟版本。高光层、环境反射、几何法线与 SSR 已恢复到接近前向参考的表现。

问题不是“亮不亮”,而是层次是否成立

真实车漆的识别度来自多个尺度同时成立:底漆决定颜色和大尺度反射,金属颗粒打碎近景高光,最外层清漆提供更锐利的第二套法线与 Fresnel。只提高 Smoothness 会得到像塑料一样的单层亮面;简单叠加两个 Specular,又会在高亮区域破坏能量关系。

HDRP StackLit 已经提供了适合这个问题的物理骨架,但原版是面向通用材质系统设计的。项目真正需要的不是把全部 StackLit 选项搬进一个面板,而是确定视觉核心、建立资产约定,并让 Shader 能进入车辆灯光、损伤、天气、SSR 和渲染管线。

先做横向拆解:成品车漆依赖哪些输入

在决定 Shader 功能之前,我先对《异环》《The Crew 2》《Cyberpunk 2077》和《Forza Horizon 5》的公开运行画面与调试截帧做了横向观察。这里必须区分“看见的数据”和“推断的实现”:截帧可以确认某类贴图、遮罩或材质层存在,却不能证明对方使用了某一段具体 BRDF 代码。对标的价值不是复刻别人的参数,而是建立资产需求清单。

两组参考车辆在不同清漆遮罩与光滑度下的高光和反射对比
参考画面的 Coat 扫描。高光形状与环境反射是两个相关但不能混为一个滑杆的视觉维度。
参考对象 截帧中可确认的输入 对项目方案的启发
《异环》 车身法线、Base Color、Emission、Flake Normal、损伤、Cubemap 与打包 Mask 车漆不是单个颜色参数,基础几何细节、颗粒、局部状态和环境响应必须分层
《The Crew 2》 车身法线、损伤、脏迹法线、碳纤维与局部 Mesh/Mask 不同部件需要共享主模型,同时允许局部材质和损伤通道独立
《Cyberpunk 2077》 运行画面中的多材质分区、表面磨损与局部覆盖 视觉复杂度更多来自区域语义和状态叠加,不应只依赖更复杂的高光公式
《Forza Horizon 5》 Damage、Flake Normal、多层脏迹、Cubemap、Normal 与水迹方向 高质量车辆材质需要把“干净车漆”和天气/损伤输入设计成同一数据协议
参考车辆材质中的基础法线、底色、金属颗粒、损伤、环境立方体和打包遮罩
参考资产输入节选。截图用于功能归类,不把画面中的通道布局直接当作对方引擎的最终材质协议。

这轮拆解直接改变了实现优先级:先保证 Base/Normal/Mask、Flake、Coat、Damage/Weather 和 Reflection 六类数据有稳定入口,再讨论更昂贵的波瓣或延迟编码。它也暴露了一个常见误区:环境反射质量高度依赖 Cubemap、Probe、SSR、GI 与场景曝光,不能把所有差异都归因到车漆 Shader。

03Clear Coat

独立光滑度、IOR、厚度、消光色和法线;控制锐利高光与掠射反射。

02Flake / Pigment

高频法线扰动与副波瓣,负责近景颗粒和高光破碎。

01Base Coat

底色、金属度、AO、主法线和主波瓣,提供稳定的大尺度材质形体。

先拆原版 StackLit:数据如何走到光照

我没有重写 StackLit 的主体 BRDF。自定义版本与项目内原版 StackLit.hlsl 仍保持绝大部分代码一致,核心工作是理解并保留它的数据契约。阅读顺序不是从四千多行文件的第一行一路向下,而是沿一条稳定的数据流:

Material Inputs贴图与参数

SurfaceData材质语义

BSDFData光照参数

PreLightData视角相关预计算

LightLoopDirect / SSR / IBL

双波瓣不是“两次高光相加”

StackLit 的底层可以同时持有两组 GGX 波瓣。每组波瓣有自己的粗糙度,最后通过 lobeMix 混合。开启各向异性时,标量粗糙度会进一步展开为切线和副切线方向的粗糙度。它解决的是同一底层材料中宽高光与窄高光共存的问题,而不是清漆层。

baseSpecular = NdotL * bottomF
             * lerp(DV_LobeA * energyA,
                    DV_LobeB * energyB,
                    lobeMix);

Clear Coat 使用 VLayering 处理能量

清漆位于底层上方。StackLit 的 ComputeAdding 会根据 Coat IOR、厚度、消光和观察角度,把顶层反射、底层透射以及等效粗糙度组合进 PreLightData。直接光阶段最终同时计算底层波瓣与 Coat 波瓣,但 Coat 不只是额外加亮,它还会改变进入底层的能量。

coatSpecular = coatNdotL
             * topLayerEnergy
             * coatEnergyCompensation
             * coatDV;

finalSpecular = baseSpecular + coatSpecular;

这也是车漆不能简单退回普通 Lit Clear Coat 的原因:这里不仅有独立 Coat,还要保留双底层波瓣、各向异性、HazyGloss、独立法线以及 SSR/IBL 的逐波瓣权重。

受控矩阵:为什么 Metallic + Coat 暗部更敏感

文档里最有价值的实验,是在固定模型、灯光、曝光和相机后,把 MetallicCoatMask 分别按 0、0.5、0.75、1 扫描,并单独观察 Diffuse、Specular、Reflection 与 GI。它把“暗部发黑”从主观观感拆成了可定位的数据链。

Metallic 与 CoatMask 参数矩阵下的漫反射、高光、环境反射和烘焙 GI 分量
同一组 Metallic × CoatMask 参数分别查看四个光照分量。左上角是低 Metallic/低 Coat,右下角是高 Metallic/高 Coat。

源码中的关系可以简化为三步。首先,金属工作流让 diffuseColor 随 Metallic 增大而趋近于零;其次,VLayering 在 ComputeAdding 中通过 lerp(1, Ti0, coatMask) 计算底层漫反射能量;最后,烘焙漫反射会继续乘上 diffuseFGD × diffuseEnergy,GTAO 的多次反弹着色也使用经过这层能量处理的漫反射颜色。当 Metallic 和 CoatMask 同时接近 1,而 Probe/SSR/GI 又不足以补回镜面环境能量时,暗部自然会非常深。

这不是简单的“模型错误”

StackLit 在表达多层介质的能量关系,结果是否好看取决于材质参数与环境光照是否匹配。线性地把 Lit、UE Clear Coat、StackLit、Substrate 排成“物理精确度排行榜”过于武断,因此我没有沿用原文那张排序图;更可靠的结论是比较每个模型的参数语义、层间透射近似和项目约束。

现象 优先排查 工程处理
正面暗、掠射角正常 Coat Fresnel、正面反射 Remap、曝光 先校准 Probe/SSR,再限定正面反射下限
高 Metallic 区域接近纯黑 Base Color/F0、环境反射覆盖、GI 保证金属区有可反射环境,不用漫反射补假亮度
Coat 开启后整体压暗 diffuseEnergy、Coat IOR/Mask、Extinction 用受控矩阵确定能量缩放范围,并锁定危险组合
缝隙或接触区脏黑 SSAO/GTAO、几何法线、Diffuse tint base 分离查看 AO 与 Beauty,避免 AO 在近零底色上二次压黑

从通用模型到项目级 CarPaint

原版 StackLit 负责“光应该怎么计算”,项目 CarPaint 负责“车辆资产如何稳定地提供这些数据”。我把这部分拆成独立的 CarPaint_Input.hlsl、Forward Pass、Depth/Motion Pass、损伤和雨水模块,再将结果送入 StackLit 的 ConvertSurfaceDataToBSDFData 与 LightLoop。

项目模块 输入或处理 进入 StackLit 的结果
Base / Mask BaseColor、Metallic、AO、Smoothness 通道选择与 Remap 稳定的底层颜色、F0 与粗糙度
Dual Lobe / HazyGloss Smoothness A/B、LobeMix,或 Haziness/HazeExtent 两组底层 GGX 波瓣
Flake 高频法线、独立缩放和强度 打碎底层高光,不改变几何轮廓
Clear Coat Mask、Smoothness、IOR、Thickness、Extinction、独立 Normal VLayered 顶层界面
Reflection Tuning 直接光高光大小/强度/颜色、IBL 粗糙度解耦、正面/掠射 Remap 在物理骨架上提供车辆美术控制
Damage / Weather 8 区域损伤、划痕、泥点、灰尘、雨痕与水滴 联动颜色、法线、AO、Smoothness 与 Coat 移除
Vehicle Lighting 车辆专用灯光倍率、四通道车灯 Emission 接入项目灯光分层和运行时状态

前向路径:直接保留完整材质状态

前向版本是这套 Shader 最直接、数据最完整的实现。Fragment 阶段构建 SurfaceData,转换为 BSDFData,采样 Baked GI,再生成 PreLightData。随后我在进入 LightLoop 前对 Coat 做项目级调制:

  • _CoatSpecularSize 调整直接光 Coat 波瓣粗糙度。
  • _CoatSpecularIntensity × _CoatSpecularColor 调整顶层直接光能量。
  • _CoatReflectionMin / Max 重映射 Coat 的环境反射响应。
  • Reflection Override 将直接光 Smoothness 与 IBL/SSR 使用的反射粗糙度解耦。
  • 车辆灯光倍率在 Punctual 与 Area Light 回调中逐灯生效。
CarPaint 前向渲染参考效果
前向参考。材质常量直接参与 PreLightData 和 LightLoop,不需要经过 GBuffer 量化与重建。

Shader GUI 是生产系统的一部分

复杂 Shader 如果只提供一长列属性,很快会失去可维护性。配套的 CustomShaderGUI_CarPaint 按材质职责组织折叠面板,并由 Toggle 同步 Shader Keyword。常用参数直接暴露,容易破坏资产一致性的高级参数默认锁定,同时提供一键恢复默认值。

基础与车漆颜色法线与 Mask双波瓣高光清漆层Flake反射与 Fresnel车灯 Emission区域材质损伤与污渍雨水破碎玻璃渲染状态

GUI 里最重要的并不是“参数多”,而是把依赖关系说清楚。例如启用 HazyGloss 后,Smoothness B 与 LobeMix 不再由美术直接填写;启用 Reflection Override 后,面板明确区分“直接光 Coat 光滑度”和“环境反射清晰度”;损伤模块则按固定区域 ID 管理局部状态,避免运行时为每个部件复制材质。

参数矩阵把 GUI 约束变成资产规则

单张 Beauty 很难判断参数是否正交,所以我又建立了球阵列测试:分别扫描 Base Smoothness、Metallic、CoatMask、CoatSmoothness、Coat IOR、Thickness 与 Extinction。矩阵的作用不是展示“参数很多”,而是找出哪些参数可以交给美术自由调整,哪些必须成组出现,哪些应该被预设锁定。

Lit 与 StackLit 以及清漆遮罩、金属度、光滑度、折射率、厚度和消光参数矩阵
参数矩阵节选。每组测试只改变标题中的变量,避免场景曝光和灯光变化掩盖材质响应。
参数组 观察结论 GUI 策略
Metallic / Base Smoothness Coat 关闭时遵循常规金属度 PBR 语义 作为基础材质参数直接开放,并提供贴图通道 Remap
CoatMask / CoatSmoothness 前者控制顶层参与度,后者控制顶层波瓣宽度,两者不能合并 常用区开放,但用预设限定有效范围
Coat IOR / CoatSmoothness IOR 同时改变 F0 与角度响应,不只是“高光强度” 归入高级参数,默认锁定在材质类别预设中
Thickness / Extinction 两者共同决定颜色吸收;厚度接近零时 Extinction 基本失去意义 成组显示、联动启用,避免单独调色造成不可解释结果
Environment / Reflection 决定暗部是否有可信的镜面信息,但主体数据来自场景 材质仅保留必要的粗糙度与范围调制,不用局部 Cubemap 掩盖环境缺失

最终资产规则因此比“提供一套默认材质球”更具体:车身至少要有可用的 Base/Normal/Mask,近景车型补 Flake Normal,需要状态变化时再接 Damage、Dirt 与 Water;Coat IOR、Thickness 和 Extinction 由材质类别预设管理。这样既保留 StackLit 的物理层次,也避免每辆车都从几十个自由参数重新试错。

为什么继续尝试全量延迟

前向效果已经成立,但项目场景主体走 Deferred,车漆需要更自然地接入统一的深度、法线、SSR、灯光分层与调试链路。问题是普通 HDRP GBuffer 只为 Lit 的材质模型设计,无法完整表达 StackLit 的双波瓣、VLayer Coat、几何法线、各向异性和项目反射控制。

因此我没有让延迟版本使用一套简化 BRDF,而是要求它恢复出与前向相同的 BSDFData 和 PreLightData。流程变成:

Depth / Stencil可见性与材质标记

HDRP GBuffer公共 Lit 数据

CarPaint Data4 张扩展 RT

Decode重建 BSDFData

StackLit LightLoop同一套光照核心

四张扩展 GBuffer 保存什么

CarPaintGBufferData 是一个独立几何 Pass,在公共 GBuffer 之后写入四张全分辨率 R16G16B16A16_UNorm。使用独立 Pass 是因为 HDRP MRT 还可能被 VT Feedback、Light Layers 和 Shadow Mask 占用,无法稳定追加完整车漆数据。

RT 主要内容 设计目的
CarPaint0 Base Normal、Roughness A/B、Tangent Angle 恢复双波瓣与各向异性坐标系
CarPaint1 Geometry Normal、Lobe/Hazy、Coat IOR、Anisotropy 恢复 VLayering、几何约束与功能分支
CarPaint2 Coat 高光控制、反射范围、Reflection Override、能量与厚度 恢复项目级 PreLightData 调制
CarPaint3 Coat Specular RGB、Extinction RGB、Env Grayscale 恢复完整顶层颜色和介质衰减

每个 16-bit 通道进一步保存两个独立 8-bit 数据。法线使用 Octahedral 编码,切线压缩为相对法线局部坐标系中的一个角度;HazyGloss 则复用在该模式下本来失活的 Lobe B 槽位。这里的目标不是追求极限压缩,而是先建立一份能够与前向逐项核对的“全量基线”。

效果对齐不是一次完成的

第一版延迟虽然能正确受光,但反射明显弱于前向,几何法线也没有真正参与最终层状计算。原因不在单一公式,而在多个阶段共同作用:Coat FGD 被项目反射重映射后又被 SSR 使用、SSR Alpha 同时承担反射层级权重、fullscreen shader 的功能全集改变了法线数组布局,而一些前向材质常量没有完整进入 GBuffer。

初版全量延迟车漆效果
初版 Deferred:反射层次与高光能量不足。
同场景前向车漆参考
Forward Reference:Coat 和环境反射更完整。

最终修正包括:保留 SSR 使用的原始 Coat FGD、恢复 Geometry Normal 与 Coat/Base Normal 的正确槽位、让弱 SSR 不再完全吞掉 Env/Sky/Probe、统一 SSR 高亮压缩,并将前向的 Coat Specular、Extinction、反射粗糙度与能量参数完整编码。

一次典型的跨阶段 Bug:白色为什么变成紫色

最终阶段出现过一个非常有代表性的错误:_CoatSpecularColor 明明是白色,移动灯光时 Coat 高光却呈现紫色。修改 Coat 颜色会改变紫点,因此问题一定在 Coat 直接光链路,但材质常量本身没有偏色。

错误字节打包导致的紫色 Coat 高光
错误的 byte-pair 打包污染了 RGB 通道,紫色高光随后又进入下一帧 SSR Color Pyramid。
根因

公共 Pack2Byte 在高位值仍带小数时先乘 256,最后才取整。对 Coat 白色而言,1.0 / 8.0 应打包为字节 (32, 32),实际却形成 (32, 0)。RGB 解码结果约为 (1.004, 0, 1.004),正好呈紫色。

修复方式是在移位之前先分别把两个值量化到 0–255,再组合为精确 16-bit 整数,并使用 R16_UNorm 保存。GPU 回读验证白色为 (32,32,32),0.8 灰色为 (26,26,26)。这个问题也说明:Deferred 的调试不能只看最终 Beauty,必须能检查 GBuffer 原值、解码值、PreLightData 和 SSR 输入。

Forward 与全量 Deferred 的真实取舍

维度 Forward 全量 Deferred
数据精度 材质常量直接进入 BSDF,无额外量化 复杂参数需要编码、量化和重建
效果一致性 基准实现,透明和特殊功能自然接入 修正后接近前向,但 SSR 层级与量化仍非数学等价
几何与材质成本 一次主要材质计算与 LightLoop 公共 GBuffer、额外数据 Pass、fullscreen lighting
显存与带宽 不需要车漆专用 RT 额外 4 × RGBA16,即 32 Byte/Pixel
管线集成 需要维护 Forward 对公共深度/法线的写入 更容易接入统一 Deferred、SSR 与屏幕空间调试
扩展维护 增加参数后可直接读取 每个新参数都要重新审视 GBuffer 协议

以 1920×1080 计算,四张扩展 RT 额外占用约 63.3 MiB;只算一次写入和一次读取,理论流量约 126.6 MiB/帧。4K 下分配约 253 MiB。实际带宽会受到车辆覆盖率、压缩和 GPU 架构影响,但额外几何 Pass 与全尺寸资源是真实存在的。

现代 HDRP Forward 已使用灯光列表,并不是传统的“每盏灯增加一次 DrawCall”。因此这次全量延迟的主要收益是统一数据链路和验证技术边界,而不是自动获得性能优势。对于当前车漆,Forward 仍然是性价比更高的生产路径;全量 Deferred 更适合作为可验证基线,下一步应尝试只保存公共 GBuffer 无法重建的数据,将四张扩展 RT 收敛到一到两张。

这项工作的最终价值

这个案例的重点不是“使用了 StackLit”,而是把一个通用、庞大的物理材质模型变成可由车辆资产稳定使用的生产系统。我完成了从源码阅读、材质语义、前向 LightLoop、反射层级、项目灯光,到 GUI、损伤、天气和全量延迟实验的整条链路。

工程结论

保留经过验证的 StackLit VLayering 核心,把项目差异集中在 SurfaceData、PreLightData 与渲染管线边界;先建立效果正确的 Forward 基准,再用全量 Deferred 暴露数据需求,最后根据真实性能决定是否压缩,而不是为了“走延迟”牺牲可解释性。

原始分析资料

25 页原始 PDF 保留了参考作品截帧、资产输入、源码思维导图、暗部能量拆解和材质参数矩阵。网页文章在此基础上校正了过度简化的模型排序,并补充项目 CarPaint 前向实现、Shader GUI、全量延迟设计与完整问题复盘。

打开原始 PDF:车漆技术分析

Unity大世界LightMap处理


在Unity中,烘焙LightMap采用的是一个场景烘焙一组LightMap。而对于大世界场景来说,没办法把世界上所有的物体在同一场景下烘焙。Unity提供的解决办法是通过SubScene来解决,就是分场景烘焙,然后再通过加载卸载Scene的方式来实现。但有时候有这样的需求,同一组室内场景可能在多个地方存在,美术希望烘焙好一组物体,能复制到各个地方,并且能很好地预览,这样使用SubScene来说就比较麻烦了。

先说一下 unity的LightMap机制,烘焙分为动态物体和静态物体,动态物体走的是GI,通过环境光,LightProb等这些算出三维光照数据,然后计算动态物体的球谐光照。对于静态物体来说就会烘焙成 lightmap,一般有几组三张贴图(color,dir以及shadow)。静态物体的MeshRender上会有个lightmapIndex存放采用第几组lightmap,还有个lightmapScaleOffset存放uv偏移,通过这两个数据就能显示正确。

知道LightMap的原理后就比较简单了,我们只需要存好我们需要使用的数据,然后设置对应的位置就能正确显示了。

首先,我们定义好我们的数据结构,我们期望在一个prefab上挂一个我们的脚本,然后加载这个prefab上所有的MeshRender。我们就需要一个这样的ScriptObject。

public class CustomLightMapDataMap : ScriptableObject
{
    public MeshLightmapData[] LightMapDatas = null;
}
[Serializable]
public struct CustomLightmapData
{
    /// <summary>
    /// The color for lightmap.
    /// </summary>
    public Texture2D LightmapColor;

    /// <summary>
    /// The dir for lightmap.
    /// </summary>
    public Texture2D LightmapDir;

    /// <summary>
    /// The shadowmask for lightmap.
    /// </summary>
    public Texture2D ShadowMask;

    /// <summary>
    /// Initializes a new instance of the <see cref="CustomLightmapData"/> struct.
    /// </summary>
    /// <param name="data">lightmapdata.</param>
    public CustomLightmapData(LightmapData data)
    {
        this.LightmapColor = data.lightmapColor;
        this.LightmapDir = data.lightmapDir;
        this.ShadowMask = data.shadowMask;
    }

    public bool IsA(LightmapData data)
    {
        return this.LightmapColor == data.lightmapColor &amp;&amp;
        this.LightmapDir == data.lightmapDir &amp;&amp;
        this.ShadowMask == data.shadowMask;
    }

    public LightmapData GetLightmapData()
    {
        LightmapData data = new LightmapData();
        data.lightmapColor = this.LightmapColor;
        data.lightmapDir = this.LightmapDir;
        data.shadowMask = this.ShadowMask;
        return data;
    }
}

[Serializable]
public struct MeshLightmapData
{
    public Vector4 LightmapScaleOffset;

    public CustomLightmapData LightmapData;
}

然后再在编辑器上弄一个菜单,选中物体就能自动干这件事情。

[MenuItem("Window/LightMapGenerate")]
    private static void Generated()
    {
        string outputPath = "Assets/LightMapPrefab";
        var lightmapPath = GetLightMapPath();
        if (!string.IsNullOrEmpty(lightmapPath))
        {
            outputPath = Path.GetDirectoryName(lightmapPath);
        }

        GameObject obj = Selection.activeGameObject;
        if (obj == null)
        {
            return;
        }

        var dataMap = (CustomLightMapDataMap)ScriptableObject.CreateInstance(typeof(CustomLightMapDataMap));
        var renders = obj.GetComponentsInChildren<MeshRenderer>();
        List<MeshLightmapData> datas = new List<MeshLightmapData>();
        var lightmaps = LightmapSettings.lightmaps;
        foreach (var render in renders)
        {
            if (render.lightmapIndex < 0 || render.lightmapIndex >= lightmaps.Length)
            {
                Debug.LogError("lightmap error:" + render.gameObject.name);
                return;
            }
            var data = new MeshLightmapData()
            {
                LightmapScaleOffset = render.lightmapScaleOffset,
                LightmapData = new CustomLightmapData(lightmaps[render.lightmapIndex]),
            };
            datas.Add(data);
        }

        dataMap.LightMapDatas = datas.ToArray();
        var loader = obj.GetComponent<LightMapDataLoader>();
        if (loader == null)
        {
            loader = obj.AddComponent<LightMapDataLoader>();
        }

        outputPath = Path.Combine(outputPath, obj.name + ".asset");
        AssetDatabase.CreateAsset(dataMap, outputPath);
        AssetDatabase.SaveAssets();
        loader.Asset = AssetDatabase.LoadAssetAtPath<CustomLightMapDataMap>(outputPath);
    }

    private static string GetLightMapPath()
    {
        var lightmaps = LightmapSettings.lightmaps;
        if (lightmaps.Length == 0)
        {
            return string.Empty;
        }

        return AssetDatabase.GetAssetPath(lightmaps[0].lightmapColor);
    }

数据保存好了,我们只需要加载就好了。加载除了要加载MeshRender上的数据,还要设置好场景的LightMap。这里还有个特别重要的问题就是卸载,在物体销毁时,我们要处理场景的lightmap,这里需要通过一个计数器去干这件事情,当引用计数为0了,我们就去清理lightmap贴图数据。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

[ExecuteInEditMode]
public class LightMapDataLoader : MonoBehaviour
{
    private static Dictionary<CustomLightmapData, int> lightmapDataRefenceCount = new Dictionary<CustomLightmapData, int>();

    [SerializeField]
    private CustomLightMapDataMap asset;

    private HashSet<CustomLightmapData> lightmapDatas = new HashSet<CustomLightmapData>();

    public CustomLightMapDataMap Asset
    {
        get { return this.asset; }
        set { this.asset = value; }
    }

    public static void Clear()
    {
        lightmapDataRefenceCount.Clear();
    }

    // Start is called before the first frame update
    private void Awake()
    {
        if (this.asset != null)
        {
            var lightmaps = LightmapSettings.lightmaps;
            var renders = this.GetComponentsInChildren<MeshRenderer>();
            var datas = this.asset.LightMapDatas;
            if (datas.Length != renders.Length)
            {
                return;
            }

            List<LightmapData> lightmapList = new List<LightmapData>(lightmaps);
            for (int i = 0; i < datas.Length; i++)
            {
                var lightMapIndex = -1;
                var nullIndex = -1;
                LightmapData currentData = null;
                for (int j = lightmapList.Count - 1; j >= 0; j--)
                {
                    var lightmap = lightmapList[j];
                    if (datas[i].LightmapData.IsA(lightmap))
                    {
                        lightMapIndex = j;
                        currentData = lightmap;
                    }

                    if (lightmap.lightmapColor == null &amp;&amp;
                        lightmap.lightmapDir == null &amp;&amp;
                        lightmap.shadowMask == null)
                    {
                        nullIndex = j;
                    }
                }

                if (lightMapIndex == -1)
                {
                    currentData = datas[i].LightmapData.GetLightmapData();
                    if (nullIndex == -1)
                    {
                        lightmapList.Add(currentData);
                        lightMapIndex = lightmapList.Count - 1;
                    }
                    else
                    {
                        lightmapList[nullIndex] = currentData;
                        lightMapIndex = nullIndex;
                    }
                }

                this.lightmapDatas.Add(datas[i].LightmapData);
                renders[i].lightmapIndex = lightMapIndex;
                renders[i].lightmapScaleOffset = datas[i].LightmapScaleOffset;
            }

            foreach (var data in this.lightmapDatas)
            {
                if (!lightmapDataRefenceCount.TryGetValue(data, out var count))
                {
                    count = 0;
                }
                else
                {
                    lightmapDataRefenceCount.Remove(data);
                }

                count++;
                lightmapDataRefenceCount.Add(data, count);
            }

            LightmapSettings.lightmaps = lightmapList.ToArray();
        }
    }

    private void OnDestroy()
    {
        foreach (var data in this.lightmapDatas)
        {
            if (lightmapDataRefenceCount.TryGetValue(data, out var count))
            {
                count--;
                lightmapDataRefenceCount.Remove(data);
                if (count == 0)
                {
                    var lightmaps = LightmapSettings.lightmaps;
                    for (int i = 0; i < lightmaps.Length; i++)
                    {
                        if (data.IsA(lightmaps[i]))
                        {
                            lightmaps[i].lightmapColor = null;
                            lightmaps[i].lightmapDir = null;
                            lightmaps[i].shadowMask = null;
                        }
                    }

                    LightmapSettings.lightmaps = lightmaps;
                }
                else
                {
                    lightmapDataRefenceCount.Add(data, count);
                }
            }
        }
    }
}

做好这些事情之后,我们就可以在场景中烘焙一组物体,然后选中Root,点击Window/LightMapGenerate,会帮你组织好数据,挂好脚本。你可以把这个物体复制到任何地方都是显示正确,也可以保存成prefab通过程序加载和销毁。

分区域烘焙时交界处的阴影处理

分区域烘焙 LightMap 时,交界处最容易出现的问题是阴影断开、漏光、间接光颜色不连续。这个问题的核心不在于运行时怎么加载,而在于烘焙时如果只把当前区域当成一个孤岛,隔壁区域的遮挡物、灯光和 GI bounce 都没有参与计算,那么当前区域边缘自然就拿不到正确的阴影和间接光。

我的处理原则是:区域可以分开保存,但烘焙时不能分开看。也就是说,每个区域烘焙时都要带上周围区域作为上下文,但最终只导出当前区域自己的 LightMap 绑定数据。

Bake Chunk_A:
Target  : Chunk_A
Context : Chunk_A 周围一圈或多圈邻区
Compute : Target + Context 一起参与阴影、遮挡和 GI
Export  : 只保存 Chunk_A 的 Renderer 映射、LightMap Index、Scale Offset 和被引用的贴图
Runtime : 只加载 Chunk_A 自己的 LightMap 数据

具体可以拆成几个步骤:

  1. 烘焙时加载邻区上下文。例如烘 A 区时,不只加载 A 区,还加载周围 8 个 Chunk,邻区几何和灯光只作为 shadow caster / GI contributor 参与计算。导出阶段只采集 A 区 Renderer 的 lightmapIndex、lightmapScaleOffset 和它实际引用到的 LightMap 贴图。
  2. 给区域边界留 overlap 或 guard band。切块不要刚好卡在可见边界,可以让每个区域多带一圈缓冲范围。运行时只使用中间有效区,缓冲区用于保证边缘阴影和 GI 过渡连续。
  3. 跨边界的大物体不要硬切。如果建筑、山体、墙体或大型遮挡物横跨多个区域,最好让它归属到一个稳定区域,或者做一个专门的边界代理物。最怕的是投影物在 B 区,但烘 A 区时没加载它,A 区地面就永远缺那块阴影。
  4. Context 可以用低成本代理。邻区不一定要用完整美术模型,可以用简化 Mesh、低 lightmap scale 或只保留关键遮挡体。目标是让阴影和 GI 正确,不是把邻区也高质量烘一遍。
  5. 控制 UV padding 和 dilation。区域边缘和高对比阴影处要避免 UV chart 太贴,LightMap padding / dilation 不够时,mipmap 后会串色,边缘就会出现脏边或断层。
  6. 大尺度间接光用 Probe 辅助。LightMap 负责静态细节,跨区域的大范围环境色、角色受光和动态物体受光,建议配合 Light Probe / Probe Volume / SH Grid / Reflection Probe 处理。这样区域流式加载时,不会因为某个 Chunk 的 LightMap 切换导致整体光照突然变色。

在当前这套 Prefab LightMap 数据保存方案里,可以把“目标区”和“上下文区”分开记录:目标区的 Renderer 进入导出列表,上下文区只在 BakeScene 中临时存在。烘焙结束后,导出工具只保存目标区相关的 Renderer 绑定关系;如果某张 LightMap 贴图被目标区 Renderer 引用,就一起保存,否则可以丢弃。这样运行时还是按区域加载 LightMap,但烘焙结果会继承邻区阴影和 GI 信息。

简单说就是:单区输出,多区参与。这是分块 LightMap 里处理接缝阴影最稳的方式。

Custom Shading Model In UE5/自定义光照模型

最近在卷UE5的自定义光照模型,所以就写一个笔记,记录一下/Recently, I’ve been experimenting with custom shading models in Unreal Engine 5 (UE5), so I decided to write some notes to keep track of my progress.

分三个模块,1.拉取源代码,2.C++部分添加custom shading model,3.shader 部分/Divide it into three modules: 1. Fetching the source code, 2. Adding the custom shading model in C++, 3. Working on the shader part.

一. 拉取UE源代码/Fetching the source code

主要步骤还是根据UE官方档案来操作 从源代码构建虚幻引擎 | 虚幻引擎文档 (unrealengine.com)/The main steps to build Unreal Engine from source code should be followed based on the official documentation provided by Unreal Engine.

二.C++部分添加Custom Shading Model/To add a custom shading model in C++

首先我们得先让UE知道我们有一套新的Shading Model,能够在光照模型的下拉菜单中找到我们的custom shading model,所以我们先修改EngineTypes.h跟MaterialShader.cpp这两个文件,本质是添加一个新的枚举/

To make Unreal Engine aware of your custom shading model and have it appear in the dropdown menu of the lighting model, you need to modify the EngineTypes.h and MaterialShader.cpp files. The essence of this modification is to add a new enumeration.

EngineTypes.h的头文件中,在EMaterialShadingModel中添加我们CustonShadingModel
在MaterialShader.cpp文件中,在GetShadingModelString(EMaterialShadingModel ShadingModel)中添加

给shader添加一个宏,让shader走新的分支

HLSLMaterialTranslator.cpp

为了之后的Shader操作,我们需要给我们的shader开放两个CustomData端口,分别在Material.cpp跟MaterialAttributeDefinitionMap.cpp文件

Material.cpp中在两个Customdata中增加我们的ShadingModel的名称
在MaterialAttributeDefinitionMap.cpp增加两个定义

接着我们让新的ShadingModel写入GBuffer,这里我们需要修改两个文件ShaderMaterial.h跟ShaderMaterialDerivedHelpers.cpp

在ShaderMaterial.h中添加我们的光照模型
ShaderMaterialDerivedHelpers.cpp中添加
Dst.WRITES_CUSTOMDATA_TO_GBUFFER = (Dst.USES_GBUFFER &amp;&amp; (Mat.MATERIAL_SHADINGMODEL_SUBSURFACE || Mat.MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN || Mat.MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE || Mat.MATERIAL_SHADINGMODEL_CLEAR_COAT || Mat.MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE || Mat.MATERIAL_SHADINGMODEL_HAIR || Mat.MATERIAL_SHADINGMODEL_CLOTH || Mat.MATERIAL_SHADINGMODEL_EYE || Mat.MATERIAL_SHADINGMODEL_TOON));

接着我们在ShaderGenerationUtil.cpp中添加生成

至此我们的C++部分修改已经完成,总的来说没什么难度,基本就是照葫芦画瓢,我们编译一下看看引擎

At this point, we have completed the modifications in the C++ part. Overall, it wasn’t difficult; it was mostly a matter of following a similar pattern. Let’s compile the engine now to see if our changes are successful.

看到这几个就相当于添加成功了

接着我们开始修改shader部分

Let’s continue with the modification of the shader part.

首先在ShadingCommon.ush中添加custom Shading Model的宏定义,顺序要和之前C++中的保持一致。

我们一开始定义的ShadingModel在NUM之前,所以在这的顺序不变,顺便NUM的ID也从13变为14

接着我们定义一下debug view下的ShadingModel的颜色

依然是在同一个文件中

然后我们在Definitions.usf中定义宏的默认值

现在我们在BasePassCommon.ush输出GBuffer

在最后加上MATERIAL_SHADINGMODEL_TOON

这步之后我们可以在Debug状态下看见我们的ShadingModel,现在我们输出CustomData分别修改DeferredShadingCommon.ush文件与ShadingModelsMaterial.ush

在DeferredShadingCommon.ush文件中增加我们的新建的ShadingModel的ID

下面就是正菜,我们先新建一个ush专门放新加的函数。

这个ush文件放在Engine/Shaders/Private这个路径下

我们下面来到Shadingmodels.ush这个文件先include我们刚才创建的头文件

接着添加着色算法

//Toon Shading model
FDirectLighting ToonBxDF(FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow)
{
	#if GBUFFER_HAS_TANGENT
		half3 X = GBuffer.WorldTangent;
		half3 Y = normalize(cross(N,X));
	#else	
		half3 X = 0;
		half3 Y = 0;
	#endif
	
	BxDFContext Context;
	Init(Context, N, X, Y, V, L);
	SphereMaxNoH(Context, AreaLight.SphereSinAlpha, true);
	Context.NoV = saturate(abs(Context.NoV) + 1e-5);
	
	float SpecularOffset = 0.5;
	float SpecularRange = GBuffer.CustomData.x;
	
	float3 ShadowColor = 0;
	ShadowColor = GBuffer.DiffuseColor * ShadowColor;
	float offset = GBuffer.CustomData.y;
	float SoftScatterStrength = 0;
	
	offset = offset * 2 - 1;
	half3 H = normalize(V + L);
	float NoH = saturate(dot(N, H));
	NoL = (dot(N, L) + 1) / 2; // overwrite NoL to get more range out of it
	half NoLOffset = saturate(NoL + offset);
	
	FDirectLighting Lighting;
	Lighting.Diffuse = AreaLight.FalloffColor * (smoothstep(0, 1, NoLOffset) * Falloff) * Diffuse_Lambert(GBuffer.DiffuseColor) * 2.2;
	
	float InScatter = pow(saturate(dot(L, -V)), 12) * lerp(3, .1f, 1);
	float NormalContribution = saturate(dot(N, H));
	float BackScatter = GBuffer.GBufferAO * NormalContribution / (PI * 2);
	
	Lighting.Specular = ToonStep(SpecularRange, (saturate(D_GGX(SpecularOffset, NoH)))) * (AreaLight.FalloffColor * GBuffer.SpecularColor * Falloff * 8);

	float3 TransmissionSoft = AreaLight.FalloffColor * (Falloff * lerp(BackScatter, 1, InScatter)) * ShadowColor * SoftScatterStrength;
	float3 ShadowLightener = (saturate(smoothstep(0, 1, saturate(1 - NoLOffset))) * ShadowColor * 0.1);
	
	Lighting.Transmission = (ShadowLightener + TransmissionSoft) * Falloff;
	
	return Lighting;
	
}

			float3 Attenuation = 1;
			BRANCH

			if (GBuffer.ShadingModelID == SHADINGMODELID_TOON)
			{
				float offset = GBuffer.CustomData.y;
				float TerminatorRange = saturate(GBuffer.Roughness - 0.5);
				
				offset = offset * 2 - 1;
				
				BRANCH

				if (offset >= 1)
				{
					Attenuation = 1;
				}
				else
				{
					float NoL = (dot(N, L) + 1) / 2;
					float NoLOffset = saturate(NoL + offset);
					float LightAttenuationOffset = saturate(Shadow.SurfaceShadow + offset);
					float ToonSurfaceShadow = smoothstep(0.5 - TerminatorRange, 0.5 + TerminatorRange, LightAttenuationOffset);

					Attenuation = smoothstep(0.5 - TerminatorRange, 0.5 + TerminatorRange, NoLOffset) * ToonSurfaceShadow;
				}
			}
			//Toon Shading Model
在DefeeredLightingCommon.ush中添加以上代码以及修改以下代码

最后我们重新编译一下看一下效果

Material Blend Tools

写了个基于Height Map的Texture Mix的工具,应该还有提升的空间先看看效果

主要思路是通过splat_control这张贴图的四个通道控制_Splat0~_Splat3这四张贴图的混合,如果splat_control对应通道的值为1,那么这个通道对应的贴图就完全显示,为0则完全不显示,通过修改splat_control贴图就可以实现想要的混合效果了;

核心代码如下

fixed4 splat_control = tex2D (_Control, IN.uv_Control).rgba;	
	fixed3 lay1 = tex2D (_Splat0, IN.uv_Splat0);
	fixed3 lay2 = tex2D (_Splat1, IN.uv_Splat1);
	fixed3 lay3 = tex2D (_Splat2, IN.uv_Splat2);
	fixed3 lay4 = tex2D (_Splat3, IN.uv_Splat3);
	_Alpha = 0.0;
	Albedo.rgb = (lay1 * splat_control.r + lay2 * splat_control.g + lay3 * splat_control.b+ lay4 * splat_control.a);

混合处

float3 blend(float3 lay1, float3 lay2, float4 splat_control)
{
        float b1 = lay1.a * splat_control.r;
        float b2 = lay2.a * splat_control.g;
        float ma = max(b1,b2);
        b1 = max(b1 - (ma – 0.3), 0) * splat_control.r;
        b2 = max(b2 - (ma – 0.3), 0) * splat_control.g;

        return (lay1.rgb * b1 + lay2.rgb * b2)/(b1 + b2);
}

下面是工具

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
[RequireComponent(typeof(MeshCollider))]
public class MeshPainter : MonoBehaviour {

    void Start () {
	
	}
	

	void Update () {
	
	}
}

using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections;

[CustomEditor(typeof(MeshPainter))]
[CanEditMultipleObjects]
public class MeshPainterStyle : Editor
{
    string contolTexName = "";

    bool isPaint;

    float brushSize = 16f;
    float brushStronger = 0.5f;

    Texture[] brushTex;
    Texture[] texLayer;

    int selBrush = 0;
    int selTex = 0;


    int brushSizeInPourcent;
    Texture2D MaskTex;
    void OnSceneGUI()
    {
        if (isPaint)
        {
            Painter();
        }

    }
    public override void OnInspectorGUI()
        
    {
        if (Cheak())
        {
            GUIStyle boolBtnOn = new GUIStyle(GUI.skin.GetStyle("Button"));//得到Button样式
            GUILayout.BeginHorizontal();
                GUILayout.FlexibleSpace();
                isPaint = GUILayout.Toggle(isPaint, EditorGUIUtility.IconContent("ClothInspector.PaintValue"), boolBtnOn, GUILayout.Width(35), GUILayout.Height(25));//编辑模式开关
            GUILayout.FlexibleSpace();
            GUILayout.EndHorizontal();
            brushSize = (int)EditorGUILayout.Slider("Brush Size", brushSize, 1, 36);//笔刷大小
            brushStronger = EditorGUILayout.Slider("Brush Stronger", brushStronger, 0, 1f);//笔刷强度

            IniBrush();
            layerTex();
            GUILayout.BeginHorizontal();
                GUILayout.FlexibleSpace();
                    GUILayout.BeginHorizontal("box", GUILayout.Width(340));
                    selTex = GUILayout.SelectionGrid(selTex, texLayer, 4, "gridlist", GUILayout.Width(340), GUILayout.Height(86));
                    GUILayout.EndHorizontal();
                GUILayout.FlexibleSpace();
            GUILayout.EndHorizontal();

            GUILayout.BeginHorizontal();
                GUILayout.FlexibleSpace();
                    GUILayout.BeginHorizontal("box", GUILayout.Width(318));
                    selBrush = GUILayout.SelectionGrid(selBrush, brushTex, 9, "gridlist", GUILayout.Width(340), GUILayout.Height(70));
                    GUILayout.EndHorizontal();
                GUILayout.FlexibleSpace();
            GUILayout.EndHorizontal();
        }
        
    }

    //获取材质球中的贴图
    void layerTex()
    {
        Transform Select = Selection.activeTransform;
        texLayer = new Texture[4];
        texLayer[0] = AssetPreview.GetAssetPreview(Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.GetTexture("_Splat0")) as Texture;
        texLayer[1] = AssetPreview.GetAssetPreview(Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.GetTexture("_Splat1")) as Texture;
        texLayer[2] = AssetPreview.GetAssetPreview(Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.GetTexture("_Splat2")) as Texture;
        texLayer[3] = AssetPreview.GetAssetPreview(Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.GetTexture("_Splat3")) as Texture;
    }

    //获取笔刷  
    void IniBrush()
    {
        string MeshPaintEditorFolder = "Assets/MeshPaint/Editor/";
        ArrayList BrushList = new ArrayList();
        Texture BrushesTL;
        int BrushNum = 0;
        do
        {
            BrushesTL = (Texture)AssetDatabase.LoadAssetAtPath(MeshPaintEditorFolder + "Brushes/Brush" + BrushNum + ".png", typeof(Texture));

            if (BrushesTL)
            {
                BrushList.Add(BrushesTL);
            }
            BrushNum++;
        } while (BrushesTL);
        brushTex = BrushList.ToArray(typeof(Texture)) as Texture[];
    }

    //检查
    bool Cheak()
    {
        bool Cheak = false;
        Transform Select = Selection.activeTransform;
        Texture ControlTex = Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.GetTexture("_Control");
        if(Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.shader == Shader.Find("4Tex_Blend_Normal") || Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.shader == Shader.Find("zcxshaderlibrary/texBlendWithBump"))
        {
            if(ControlTex == null)
            {
                EditorGUILayout.HelpBox("当前模型材质球中未找到Control贴图,绘制功能不可用!", MessageType.Error);
                if (GUILayout.Button("创建Control贴图"))
                {
                    creatContolTex();
                    //Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.SetTexture("_Control", creatContolTex());
                }
            }
            else
            {
                Cheak = true;
            }
        }
        else 
        {
            EditorGUILayout.HelpBox("当前模型shader错误!请更换!", MessageType.Error);
        }
        return Cheak;
    }

    //创建Contol贴图
    void creatContolTex()
    {

        //创建一个新的Contol贴图
        string ContolTexFolder = "Assets/MeshPaint/Controler/";
        Texture2D newMaskTex = new Texture2D(512, 512, TextureFormat.ARGB32, true);
        Color[] colorBase = new Color[512 * 512];
        for(int t = 0; t< colorBase.Length; t++)
        {
            colorBase[t] = new Color(1, 0, 0, 0);
        }
        newMaskTex.SetPixels(colorBase);

        //判断是否重名
        bool exporNameSuccess = true;
        for(int num = 1; exporNameSuccess; num++)
        {
            string Next = Selection.activeTransform.name +"_"+ num;
            if (!File.Exists(ContolTexFolder + Selection.activeTransform.name + ".png"))
            {
                contolTexName = Selection.activeTransform.name;
                exporNameSuccess = false;
            }
            else if (!File.Exists(ContolTexFolder + Next + ".png"))
            {
                contolTexName = Next;
                exporNameSuccess = false;
            }

        }

        string path = ContolTexFolder + contolTexName + ".png";
        byte[] bytes = newMaskTex.EncodeToPNG();
        File.WriteAllBytes(path, bytes);//保存


        AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);//导入资源
        //Contol贴图的导入设置
        TextureImporter textureIm = AssetImporter.GetAtPath(path) as TextureImporter;
        TextureImporterPlatformSettings texset = textureIm.GetDefaultPlatformTextureSettings();
        texset.format = TextureImporterFormat.RGBA32;
        //texset.maxTextureSize=512;
        textureIm.SetPlatformTextureSettings(texset);
        textureIm.isReadable = true;
        textureIm.anisoLevel = 9;
        textureIm.mipmapEnabled = false;
        textureIm.wrapMode = TextureWrapMode.Clamp;
        AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);//刷新


        setContolTex(path);//设置Contol贴图

    }

    //设置Contol贴图
    void setContolTex(string peth)
    {
        Texture2D ControlTex = (Texture2D)AssetDatabase.LoadAssetAtPath(peth, typeof(Texture2D));
        Selection.activeTransform.gameObject.GetComponent<MeshRenderer>().sharedMaterial.SetTexture("_Control", ControlTex);
    }

    void Painter()
    {
        
        
        Transform CurrentSelect = Selection.activeTransform;
        MeshFilter temp = CurrentSelect.GetComponent<MeshFilter>();//获取当前模型的MeshFilter
        float orthographicSize = (brushSize * CurrentSelect.localScale.x) * (temp.sharedMesh.bounds.size.x / 200);//笔刷在模型上的正交大小
        MaskTex = (Texture2D)CurrentSelect.gameObject.GetComponent<MeshRenderer>().sharedMaterial.GetTexture("_Control");//从材质球中获取Control贴图

        brushSizeInPourcent = (int)Mathf.Round((brushSize * MaskTex.width) / 100);//笔刷在模型上的大小
        bool ToggleF = false;
        Event e = Event.current;//检测输入
        HandleUtility.AddDefaultControl(0);
        RaycastHit raycastHit = new RaycastHit();
        Ray terrain = HandleUtility.GUIPointToWorldRay(e.mousePosition);//从鼠标位置发射一条射线
        if (Physics.Raycast(terrain, out raycastHit, Mathf.Infinity, 1 << LayerMask.NameToLayer("ground")))//射线检测名为"ground"的层
        {
            Handles.color = new Color(1f, 1f, 0f, 1f);//颜色
            Handles.DrawWireDisc(raycastHit.point, raycastHit.normal, orthographicSize);//根据笔刷大小在鼠标位置显示一个圆

            //鼠标点击或按下并拖动进行绘制
            if ((e.type == EventType.MouseDrag &amp;&amp; e.alt == false &amp;&amp; e.control == false &amp;&amp; e.shift == false &amp;&amp; e.button == 0) || (e.type == EventType.MouseDown &amp;&amp; e.shift == false &amp;&amp; e.alt == false &amp;&amp; e.control == false &amp;&amp; e.button == 0 &amp;&amp; ToggleF == false))
            {
                //选择绘制的通道
                Color targetColor = new Color(1f, 0f, 0f, 0f);
                switch (selTex)
                {
                    case 0:
                        targetColor = new Color(1f, 0f, 0f, 0f);
                        break;
                    case 1:
                        targetColor = new Color(0f, 1f, 0f, 0f);
                        break;
                    case 2:
                        targetColor = new Color(0f, 0f, 1f, 0f);
                        break;
                    case 3:
                        targetColor = new Color(0f, 0f, 0f, 1f);
                        break;

                }

                //targetColor = new Color(0f, 0f, 0f, 0f);

                Vector2 pixelUV = raycastHit.textureCoord;

                //计算笔刷所覆盖的区域
                int PuX = Mathf.FloorToInt(pixelUV.x * MaskTex.width);
                int PuY = Mathf.FloorToInt(pixelUV.y * MaskTex.height);
                int x = Mathf.Clamp(PuX - brushSizeInPourcent / 2, 0, MaskTex.width - 1);
                int y = Mathf.Clamp(PuY - brushSizeInPourcent / 2, 0, MaskTex.height - 1);
                int width = Mathf.Clamp((PuX + brushSizeInPourcent / 2), 0, MaskTex.width) - x;
                int height = Mathf.Clamp((PuY + brushSizeInPourcent / 2), 0, MaskTex.height) - y;

                Color[] terrainBay = MaskTex.GetPixels(x, y, width, height, 0);//获取Control贴图被笔刷所覆盖的区域的颜色

                Texture2D TBrush = brushTex[selBrush] as Texture2D;//获取笔刷性状贴图
                float[] brushAlpha = new float[brushSizeInPourcent * brushSizeInPourcent];//笔刷透明度

                //根据笔刷贴图计算笔刷的透明度
                for (int i = 0; i < brushSizeInPourcent; i++)
                {
                    for (int j = 0; j < brushSizeInPourcent; j++)
                    {
                        brushAlpha[j * brushSizeInPourcent + i] = TBrush.GetPixelBilinear(((float)i) / brushSizeInPourcent, ((float)j) / brushSizeInPourcent).a;
                    }
                }

                //计算绘制后的颜色
                for (int i = 0; i < height; i++)
                {
                    for (int j = 0; j < width; j++)
                    {
                        int index = (i * width) + j;
                        float Stronger = brushAlpha[Mathf.Clamp((y + i) - (PuY - brushSizeInPourcent / 2), 0, brushSizeInPourcent - 1) * brushSizeInPourcent + Mathf.Clamp((x + j) - (PuX - brushSizeInPourcent / 2), 0, brushSizeInPourcent - 1)] * brushStronger;

                        terrainBay[index] = Color.Lerp(terrainBay[index], targetColor, Stronger);
                    }
                }
                Undo.RegisterCompleteObjectUndo(MaskTex, "meshPaint");//保存历史记录以便撤销

                MaskTex.SetPixels(x, y, width, height, terrainBay, 0);//把绘制后的Control贴图保存起来
                MaskTex.Apply();
                ToggleF = true;
            }

              if(e.type == EventType.MouseUp &amp;&amp; e.alt == false &amp;&amp; e.button == 0 &amp;&amp; ToggleF == true)
            {

                SaveTexture();//绘制结束保存Control贴图
                ToggleF = false;
            }
        }
    }
    public void SaveTexture()
    {
        var path = AssetDatabase.GetAssetPath(MaskTex);
        var bytes = MaskTex.EncodeToPNG();
        File.WriteAllBytes(path, bytes);
        AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);//刷新
    }
}

Shader模块

Shader "zcxshaderlibrary/texBlendWithBump"
{
    Properties
    {
        _SpecularColor("SpecularColor",Color)=(1,1,1,1)
        _Smoothness("Smoothness",range(0,20))=10
        _Cutoff("Cutoff",float)=0.5
        [Space(10)][Header(Layer)]
        [Space(30)][Header(Layer1Map)]
        _Splat0 ("Layer 1(RGBA)",2D) = "white"{}
        _Splat0_Color("_Splat1 Color",Color) = (1,1,1,1)
        _BumpSplat0 ("Layer 1 Normal(Bump)", 2D) = "Bump" {}
        _BumpSplat0Scale("Layer 1 Normal Scale",float)=1
        [Space(30)][Header(Layer2Map)]
        _Splat1 ("Layer 2(RGBA)", 2D) = "white" {}
        _Splat1_Color("_Splat2 Color",Color) = (1,1,1,1)
        _BumpSplat1 ("Layer 2 Normal(Bump)", 2D) = "Bump" {}
        _BumpSplat1Scale("Layer 2 Normal Scale",float)=1
        [Space(30)][Header(Layer3Map)]
        _Splat2 ("Layer 3(RGBA)", 2D) = "white" {}
        _Splat2_Color("_Splat3 Color",Color) = (1,1,1,1)
        _BumpSplat2 ("Layer 3 Normal(Bump)", 2D) = "Bump" {}
        _BumpSplat2Scale("Layer 3 Normal Scale",float)=1
        [Space(30)][Header(Layer4Map)]
        _Splat3 ("Layer 4(RGBA)", 2D) = "white" {}
        _Splat3_Color("_Splat4 Color",Color) = (1,1,1,1)
        _BumpSplat3 ("Layer 4 Normal(Bump)", 2D) = "Bump" {}
        _BumpSplat3Scale("Layer 4 Normal Scale",float)=1

        [Space(30)][Header(Blend Texture)]
        _Control ("Control (RGBA)", 2D) = "white" {}
        _Weight("Blend Weight" , Range(0.001,1)) = 0.2 
        
    }
    SubShader
    {
        Tags
        {
            "RenderPipeline"="UniversalPipeline"
            "Queue"="Geometry"
            "RenderType"="Opaque"
        }

        HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        CBUFFER_START(UnityPerMaterial)
        
        float4 _BaseColor;
        float4 _Splat0_ST;
        float4 _Splat0_Color;
        float4 _SpecularColor;
        float4 _BumpSplat0_ST;
        float4 _Splat1_ST;
        float4 _Splat1_Color;
        float4 _BumpSplat1_ST;
        float4 _Splat2_ST;
        float4 _Splat2_Color;
        float4 _BumpSplat2_ST;
        float4 _Splat3_ST;
        float4 _Splat3_Color;
        float4 _BumpSplat3_ST;
        float4 _Control_ST;

        //float _Control;
        float _Weight;

        float _Smoothness;
        float _Cutoff;
        
        float _BumpSplat0Scale;
        float _BumpSplat1Scale;
        float _BumpSplat2Scale;
        float _BumpSplat3Scale;


        TEXTURE2D(_Splat0);
        SAMPLER(sampler_Splat0);  
        TEXTURE2D(_Splat1);
        SAMPLER(sampler_Splat1);  
        TEXTURE2D(_Splat2);
        SAMPLER(sampler_Splat2);  
        TEXTURE2D(_Splat3);
        SAMPLER(sampler_Splat3);  
        TEXTURE2D(_Control);
        SAMPLER(sampler_Control); 
        TEXTURE2D(_BumpSplat0);
        SAMPLER(sampler_BumpSplat0);
        TEXTURE2D(_BumpSplat1);
        SAMPLER(sampler_BumpSplat1);
        TEXTURE2D(_BumpSplat2);
        SAMPLER(sampler_BumpSplat2);  
        TEXTURE2D(_BumpSplat3);
        SAMPLER(sampler_BumpSplat3);  





        CBUFFER_END
        float4 _BaseMap_ST;
        
        ENDHLSL
    

        Pass
        {
            Name "URPSimpleLit" 
            Tags{"LightMode"="UniversalForward"}

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

            #pragma vertex vert
            #pragma fragment frag

            

            struct Attributes
            {
                float4 positionOS : POSITION;
                float4 normalOS : NORMAL;
                float4 tangentOS  : TANGENT;
                float2 uv0 : TEXCOORD0;
                float2 uv1 : TEXCOORD1;
                float2 uv2 : TEXCOORD2;
                float2 uv3 : TEXCOORD3;
                float2 uv4 : TEXCOORD4;
            };
            struct Varings
            {
                float4 positionCS : SV_POSITION;
                float2 uv0 : TEXCOORD0;
                float2 uv1 : TEXCOORD1;
                float2 uv2 : TEXCOORD2;
                float2 uv3 : TEXCOORD3;
                float2 uv4 : TEXCOORD4;
                float3 positionWS : TEXCOORD5;
                float3 viewDirWS : TEXCOORD6;
                float3 normalWS : NORMAL_WS;
                //float3 normalWS1   : NORMAL_WS;
                float4 tangentWS  : TANGENT_WS;
            };
            
  
            
            inline float4 Blend(float depth1 ,float depth2,float depth3,float depth4 , float4 control) 
            {
                float4 blend ;
                
                blend.r =depth1 * control.r;
                blend.g =depth2 * control.g;
                blend.b =depth3 * control.b;
                blend.a =depth4 * control.a;
                
                float ma = max(blend.r, max(blend.g, max(blend.b, blend.a)));
                blend = max(blend - ma +_Weight , 0) * control;
                return blend/(blend.r + blend.g + blend.b + blend.a);
            }

            Varings vert(Attributes IN)
            {
                Varings OUT;
                VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
                VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS.xyz);
                real sign = IN.tangentOS.w * GetOddNegativeScale();
                OUT.positionCS = positionInputs.positionCS;
                OUT.positionWS = positionInputs.positionWS;
                OUT.viewDirWS = GetCameraPositionWS() - positionInputs.positionWS;
                OUT.normalWS = normalInputs.normalWS;
                OUT.tangentWS  = real4(normalInputs.tangentWS, sign);
                OUT.uv0=TRANSFORM_TEX(IN.uv0,_Splat0);
                OUT.uv1=TRANSFORM_TEX(IN.uv1,_Splat1);
                OUT.uv2=TRANSFORM_TEX(IN.uv2,_Splat2);
                OUT.uv3=TRANSFORM_TEX(IN.uv3,_Splat3);
                OUT.uv4=TRANSFORM_TEX(IN.uv4,_Control);
                return OUT;
            }
            
            float4 frag(Varings IN):SV_Target
            {   
                //
                float4 splat_control = SAMPLE_TEXTURE2D(_Control, sampler_Control, IN.uv4);
                //纹理贴图
                float4 lay1 = SAMPLE_TEXTURE2D(_Splat0, sampler_Splat0, IN.uv0);     
                float4 lay2 = SAMPLE_TEXTURE2D(_Splat1, sampler_Splat1, IN.uv1);
                float4 lay3 = SAMPLE_TEXTURE2D(_Splat2, sampler_Splat2, IN.uv2);
                float4 lay4 = SAMPLE_TEXTURE2D(_Splat3, sampler_Splat3, IN.uv3); 

                //Bump贴图
                float3 nor1TS = UnpackNormalScale(SAMPLE_TEXTURE2D (_BumpSplat0, sampler_BumpSplat0,IN.uv0),_BumpSplat0Scale);
                float3 nor2TS = UnpackNormalScale(SAMPLE_TEXTURE2D (_BumpSplat1, sampler_BumpSplat1,IN.uv1),_BumpSplat1Scale);
                float3 nor3TS = UnpackNormalScale(SAMPLE_TEXTURE2D (_BumpSplat2, sampler_BumpSplat2,IN.uv2),_BumpSplat2Scale);
                float3 nor4TS = UnpackNormalScale(SAMPLE_TEXTURE2D (_BumpSplat3, sampler_BumpSplat3,IN.uv3),_BumpSplat3Scale);



                //BaseColor附加到各个图层
                lay1.rgb*=lay1.rgb*_Splat0_Color.rgb;
                lay2.rgb*=lay2.rgb*_Splat1_Color.rgb;
                lay3.rgb*=lay3.rgb*_Splat2_Color.rgb;
                lay4.rgb*=lay4.rgb*_Splat3_Color.rgb;
                
                

                //Normal计算
                real sgn = IN.tangentWS.w;
                real3 bitangent = sgn * cross(IN.normalWS.xyz, IN.tangentWS.xyz);
                real3 nor1WS = mul(nor1TS, real3x3(IN.tangentWS.xyz, bitangent.xyz, IN.normalWS.xyz));
                real3 nor2WS = mul(nor2TS, real3x3(IN.tangentWS.xyz, bitangent.xyz, IN.normalWS.xyz));
                real3 nor3WS = mul(nor3TS, real3x3(IN.tangentWS.xyz, bitangent.xyz, IN.normalWS.xyz));
                real3 nor4WS = mul(nor4TS, real3x3(IN.tangentWS.xyz, bitangent.xyz, IN.normalWS.xyz)); // 转换至世界空间
                //计算混合
                float4 blend = Blend(lay1.a,lay2.a,lay3.a,lay4.a,splat_control);
                float3 blendNor = nor1WS*blend.r+nor2WS*blend.g+nor3WS*blend.b+nor4WS*blend.a;

                //计算主光
                Light light = GetMainLight();
                float3 diffuse = LightingLambert(light.color, light.direction, IN.normalWS);
                float3 specular = LightingSpecular(light.color, light.direction, normalize(blendNor), normalize(IN.viewDirWS), _SpecularColor, _Smoothness);

                //计算附加光照
                uint pixelLightCount = GetAdditionalLightsCount();
                for (uint lightIndex = 0; lightIndex < pixelLightCount; ++lightIndex)
                {
                    Light light = GetAdditionalLight(lightIndex, IN.positionWS);
                    diffuse += LightingLambert(light.color, light.direction, IN.normalWS);
                    specular += LightingSpecular(light.color, light.direction, normalize(blendNor), normalize(IN.viewDirWS), _SpecularColor, _Smoothness);
                }


                

                float3 basecolor=lay1.rgb *blend.r  + lay2.rgb* blend.g + lay3.rgb * blend.b + lay4.rgb * blend.a;//混合
                
                
                float4 ambient=float4(SampleSH(blendNor), 1.0)*(basecolor,0);

                float3 diff=saturate(dot(light.direction,blendNor))*basecolor;

                real3 viewDirectionWS = SafeNormalize(GetCameraPositionWS() - IN.positionWS); // safe防止分母为0
                real3 h = SafeNormalize(viewDirectionWS + light.direction);
                float3 color=diff*diffuse+specular+ambient.rgb;
                clip(lay1.a*lay2.a*lay3.a*lay4.a-_Cutoff);
                return float4(color,0);
            }
            ENDHLSL            
        }

        
          Pass
        {
            Name "ShadowCaster"
            Tags{"LightMode" = "ShadowCaster"}

            ZWrite On
            ZTest LEqual
            Cull[_Cull]

            HLSLPROGRAM
            // Required to compile gles 2.0 with standard srp library
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0

            // -------------------------------------
            // Material Keywords
            #pragma shader_feature _ALPHATEST_ON
            #pragma shader_feature _GLOSSINESS_FROM_BASE_ALPHA

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing

            #pragma vertex ShadowPassVertex
            #pragma fragment ShadowPassFragment


            //由于这段代码中声明了自己的CBUFFER,与我们需要的不一样,所以我们注释掉他
            //#include "Packages/com.unity.render-pipelines.universal/Shaders/SimpleLitInput.hlsl"
            //它还引入了下面2个hlsl文件
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
            ENDHLSL
        }

    }
}

RenderFeatureToggler

最近在搞renderFeature,写了个脚本来控制他的开关

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering.Universal;
 
[System.Serializable]
public struct RenderFeatureToggle
{
    public ScriptableRendererFeature feature;
    public bool isEnabled;
}
 
[ExecuteAlways]
public class RenderFeatureToggler : MonoBehaviour
{
    [SerializeField]
    private List<RenderFeatureToggle> renderFeatures = new List<RenderFeatureToggle>();
    [SerializeField]
    private UniversalRenderPipelineAsset pipelineAsset;
 
    private void Update()
    {
        foreach (RenderFeatureToggle toggleObj in renderFeatures)
        {
            toggleObj.feature.SetActive(toggleObj.isEnabled);
        }
    }
}

改一下可以用脚本控制

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering.Universal;

[System.Serializable]
public struct RenderFeatureToggle
{
    public ScriptableRendererFeature feature;
    public bool isEnabled;
}

[ExecuteAlways]
public class RenderFeatureToggler : MonoBehaviour
{
    [SerializeField]
    private List<RenderFeatureToggle> renderFeatures = new List<RenderFeatureToggle>();
    [SerializeField]
    private UniversalRenderPipelineAsset pipelineAsset;

    private void Update()
    {
        //foreach (RenderFeatureToggle toggleObj in renderFeatures)
        //{
        //    toggleObj.feature.SetActive(toggleObj.isEnabled);
        //}
    }

    public void OpenRenderFeatureToggle()
    {
        foreach (RenderFeatureToggle toggleObj in renderFeatures)
        {
            toggleObj.feature.SetActive(true);
        }
    }

    public void OffRenderFeatureToggle()
    {
        foreach (RenderFeatureToggle toggleObj in renderFeatures)
        {
            toggleObj.feature.SetActive(false);
        }
    }

}

物体遮挡剔除处理

先看下效果

视频有压缩,效果有点不对,下面是正确的效果

先上两段代码

using System;
using System.Collections.Generic;
using UnityEngine;

public class FadingObject : MonoBehaviour, IEquatable<FadingObject>
{
    public List<Renderer> Renderers = new List<Renderer>();
    public Vector3 Position;
    public List<Material> Materials = new List<Material>();
    [HideInInspector]
    public float InitialAlpha;
    //public float InitialFadeScale;


    private void Awake()
    {
        Position = transform.position;

        if (Renderers.Count == 0)
        {
            Renderers.AddRange(GetComponentsInChildren<Renderer>());
        }
        foreach(Renderer renderer in Renderers)
        {
            Materials.AddRange(renderer.materials);
        }

        InitialAlpha = Materials[0].color.a;
        Materials[0].SetFloat("_DitherThreshold", 1f);
    }

    public bool Equals(FadingObject other)
    {
        return Position.Equals(other.Position);
    }

    public override int GetHashCode()
    {
        return Position.GetHashCode();
    }
}


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FadeObjectBlockingObject : MonoBehaviour
{
    [SerializeField]
    private LayerMask LayerMask;
    [SerializeField]
    private Transform Target;
    [SerializeField]
    private Camera Camera;
    [SerializeField]
    [Range(0, 1f)]
    private float FadedAlpha = 0.33f;
    [SerializeField]
    private bool RetainShadows = true;
    [SerializeField]
    private Vector3 TargetPositionOffset = Vector3.up;
    [SerializeField]
    private float FadeSpeed = 1;

    [Header("Read Only Data")]
    [SerializeField]
    private List<FadingObject> ObjectsBlockingView = new List<FadingObject>();
    private Dictionary<FadingObject, Coroutine> RunningCoroutines = new Dictionary<FadingObject, Coroutine>();

    private RaycastHit[] Hits = new RaycastHit[10];

    private void OnEnable()
    {
        StartCoroutine(CheckForObjects());
    }

    private IEnumerator CheckForObjects()
    {
        while (true)
        {
            int hits = Physics.RaycastNonAlloc(
                Camera.transform.position,
                (Target.transform.position + TargetPositionOffset - Camera.transform.position).normalized,
                Hits,
                Vector3.Distance(Camera.transform.position, Target.transform.position + TargetPositionOffset),
                LayerMask
            );

            if (hits > 0)
            {
                for (int i = 0; i < hits; i++)
                {
                    FadingObject fadingObject = GetFadingObjectFromHit(Hits[i]);

                    if (fadingObject != null &amp;&amp; !ObjectsBlockingView.Contains(fadingObject))
                    {
                        if (RunningCoroutines.ContainsKey(fadingObject))
                        {
                            if (RunningCoroutines[fadingObject] != null)
                            {
                                StopCoroutine(RunningCoroutines[fadingObject]);
                            }

                            RunningCoroutines.Remove(fadingObject);
                        }

                        RunningCoroutines.Add(fadingObject, StartCoroutine(FadeObjectOut(fadingObject)));
                        ObjectsBlockingView.Add(fadingObject);
                    }
                }
            }

            FadeObjectsNoLongerBeingHit();

            ClearHits();

            yield return null;
        }
    }

    private void FadeObjectsNoLongerBeingHit()
    {
        List<FadingObject> objectsToRemove = new List<FadingObject>(ObjectsBlockingView.Count);

        foreach (FadingObject fadingObject in ObjectsBlockingView)
        {
            bool objectIsBeingHit = false;
            for (int i = 0; i < Hits.Length; i++)
            {
                FadingObject hitFadingObject = GetFadingObjectFromHit(Hits[i]);
                if (hitFadingObject != null &amp;&amp; fadingObject == hitFadingObject)
                {
                    objectIsBeingHit = true;
                    break;
                }
            }

            if (!objectIsBeingHit)
            {
                if (RunningCoroutines.ContainsKey(fadingObject))
                {
                    if (RunningCoroutines[fadingObject] != null)
                    {
                        StopCoroutine(RunningCoroutines[fadingObject]);
                    }
                    RunningCoroutines.Remove(fadingObject);
                }

                RunningCoroutines.Add(fadingObject, StartCoroutine(FadeObjectIn(fadingObject)));
                 objectsToRemove.Add(fadingObject);
            }
        }

        foreach(FadingObject removeObject in objectsToRemove)
        {
            ObjectsBlockingView.Remove(removeObject);
        }
    }

    private IEnumerator FadeObjectOut(FadingObject FadingObject)
    {
        foreach (Material material in FadingObject.Materials)
        {
            material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
            material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
            material.SetInt("_ZWrite", 0);
            material.SetInt("_Surface", 1);

            material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;

            material.SetShaderPassEnabled("DepthOnly", false);
            material.SetShaderPassEnabled("SHADOWCASTER", RetainShadows);

            material.SetOverrideTag("RenderType", "Transparent");

            material.EnableKeyword("_SURFACE_TYPE_TRANSPARENT");
            material.EnableKeyword("_ALPHAPREMULTIPLY_ON");
        }

        float time = 0;

        while (FadingObject.Materials[0].color.a > FadedAlpha)
        {
            foreach (Material material in FadingObject.Materials)
            {
                if (material.HasProperty("_BaseColor"))
                {
                    float a = Mathf.Lerp(FadingObject.InitialAlpha, FadedAlpha, time * FadeSpeed);
                    material.color = new Color(
                        material.color.r,
                        material.color.g,
                        material.color.b,
                        a
                        
                    );

                    //print("_1");
                    //Setting fade value
                    material.SetFloat("_DitherThreshold", a);
                }
            }

            time += Time.deltaTime;
            yield return null;
        }

        if (RunningCoroutines.ContainsKey(FadingObject))
        {
            StopCoroutine(RunningCoroutines[FadingObject]);
            RunningCoroutines.Remove(FadingObject);
        }
    }

    private IEnumerator FadeObjectIn(FadingObject FadingObject)
    {
        float time = 0;

        while (FadingObject.Materials[0].color.a < FadingObject.InitialAlpha)
        {
            foreach (Material material in FadingObject.Materials)
            {
                if (material.HasProperty("_BaseColor"))
                {
                    float a = Mathf.Lerp(FadedAlpha, FadingObject.InitialAlpha, time * FadeSpeed);

                    material.color = new Color(
                        material.color.r,
                        material.color.g,
                        material.color.b,
                        a
                    );

                    //Setting fade value
                    material.SetFloat("_DitherThreshold", a);

                }
            }

            time += Time.deltaTime;
            yield return null;
        }

        foreach (Material material in FadingObject.Materials)
        {
            material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
            material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero);
            material.SetInt("_ZWrite", 1);
            material.SetInt("_Surface", 0);

            material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Geometry;

            material.SetShaderPassEnabled("DepthOnly", true);
            material.SetShaderPassEnabled("SHADOWCASTER", true);

            material.SetOverrideTag("RenderType", "Opaque");

            material.DisableKeyword("_SURFACE_TYPE_TRANSPARENT");
            material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
        }

        if (RunningCoroutines.ContainsKey(FadingObject))
        {
            StopCoroutine(RunningCoroutines[FadingObject]);
            RunningCoroutines.Remove(FadingObject);
        }
    }

    private void ClearHits()
    {
        System.Array.Clear(Hits, 0, Hits.Length);
    }

    private FadingObject GetFadingObjectFromHit(RaycastHit Hit)
    {
        return Hit.collider != null ? Hit.collider.GetComponent<FadingObject>() : null;
    }

}


放一个自动抓取场景中的物体然后再实现遮挡处理的脚本,这个主要是用在main camera需要从全局调用的场景

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FadeObjectBlockingObject : MonoBehaviour
{
    [SerializeField]
    private LayerMask LayerMask;
    [SerializeField]
    private Transform Target;
    [SerializeField]
    private Camera Camera;
    [SerializeField]
    [Range(0, 1f)]
    private float FadedAlpha = 0.33f;
    [SerializeField]
    private bool RetainShadows = true;
    [SerializeField]
    private Vector3 TargetPositionOffset = Vector3.up;
    [SerializeField]
    private float FadeSpeed = 1;

    [Header("Read Only Data")]
    [SerializeField]
    private List<FadingObject> ObjectsBlockingView = new List<FadingObject>();
    private Dictionary<FadingObject, Coroutine> RunningCoroutines = new Dictionary<FadingObject, Coroutine>();

    private RaycastHit[] Hits = new RaycastHit[10];
    void Start()
    {
        this.Camera = Camera.main;
        this.Target = this.transform;
        if (this.transform.parent.name == "ePlayer")
        {
            StartCoroutine(CheckForObjects());    
        }
    }

    private void OnEnable()
    {
        
    }

    private IEnumerator CheckForObjects()
    {
        while (true)
        {
            int hits = Physics.RaycastNonAlloc(
                Camera.transform.position,
                (Target.transform.position + TargetPositionOffset - Camera.transform.position).normalized,
                Hits,
                Vector3.Distance(Camera.transform.position, Target.transform.position + TargetPositionOffset),
                LayerMask
            );

            // Gizmos.DrawLine(Camera.transform.position, Target.transform.position);
            // Debug.DrawLine(Camera.transform.position, Target.transform.position , Color.red, 0.5f ); 
            if (hits > 0)
            {
                for (int i = 0; i < hits; i++)
                {
                    FadingObject fadingObject = GetFadingObjectFromHit(Hits[i]);

                    if (fadingObject != null &amp;&amp; !ObjectsBlockingView.Contains(fadingObject))
                    {
                        if (RunningCoroutines.ContainsKey(fadingObject))
                        {
                            if (RunningCoroutines[fadingObject] != null)
                            {
                                StopCoroutine(RunningCoroutines[fadingObject]);
                            }

                            RunningCoroutines.Remove(fadingObject);
                        }

                        RunningCoroutines.Add(fadingObject, StartCoroutine(FadeObjectOut(fadingObject)));
                        ObjectsBlockingView.Add(fadingObject);
                    }
                }
            }

            FadeObjectsNoLongerBeingHit();

            ClearHits();

            yield return null;
        }
    }

    private void FadeObjectsNoLongerBeingHit()
    {
        List<FadingObject> objectsToRemove = new List<FadingObject>(ObjectsBlockingView.Count);

        foreach (FadingObject fadingObject in ObjectsBlockingView)
        {
            bool objectIsBeingHit = false;
            for (int i = 0; i < Hits.Length; i++)
            {
                FadingObject hitFadingObject = GetFadingObjectFromHit(Hits[i]);
                if (hitFadingObject != null &amp;&amp; fadingObject == hitFadingObject)
                {
                    objectIsBeingHit = true;
                    break;
                }
            }

            if (!objectIsBeingHit)
            {
                if (RunningCoroutines.ContainsKey(fadingObject))
                {
                    if (RunningCoroutines[fadingObject] != null)
                    {
                        StopCoroutine(RunningCoroutines[fadingObject]);
                    }
                    RunningCoroutines.Remove(fadingObject);
                }

                RunningCoroutines.Add(fadingObject, StartCoroutine(FadeObjectIn(fadingObject)));
                 objectsToRemove.Add(fadingObject);
            }
        }

        foreach(FadingObject removeObject in objectsToRemove)
        {
            ObjectsBlockingView.Remove(removeObject);
        }
    }

    private IEnumerator FadeObjectOut(FadingObject FadingObject)
    {
        foreach (Material material in FadingObject.Materials)
        {
            material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
            material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
            material.SetInt("_ZWrite", 0);
            material.SetInt("_Surface", 1);

            material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;

            material.SetShaderPassEnabled("DepthOnly", false);
            material.SetShaderPassEnabled("SHADOWCASTER", RetainShadows);

            material.SetOverrideTag("RenderType", "Transparent");

            material.EnableKeyword("_SURFACE_TYPE_TRANSPARENT");
            material.EnableKeyword("_ALPHAPREMULTIPLY_ON");
        }

        float time = 0;

        while (FadingObject.Materials[0].color.a > FadedAlpha)
        {
            foreach (Material material in FadingObject.Materials)
            {
                if (material.HasProperty("_BaseColor"))
                {
                    float a = Mathf.Lerp(FadingObject.InitialAlpha, FadedAlpha, time * FadeSpeed);
                    material.color = new Color(
                        material.color.r,
                        material.color.g,
                        material.color.b,
                        a
                        
                    );

                    //print("_1");
                    //Setting fade value
                    material.SetFloat("_DitherThreshold", a);
                }
            }

            time += Time.deltaTime;
            yield return null;
        }

        if (RunningCoroutines.ContainsKey(FadingObject))
        {
            StopCoroutine(RunningCoroutines[FadingObject]);
            RunningCoroutines.Remove(FadingObject);
        }
    }

    private IEnumerator FadeObjectIn(FadingObject FadingObject)
    {
        float time = 0;

        while (FadingObject.Materials[0].color.a < FadingObject.InitialAlpha)
        {
            foreach (Material material in FadingObject.Materials)
            {
                if (material.HasProperty("_BaseColor"))
                {
                    float a = Mathf.Lerp(FadedAlpha, FadingObject.InitialAlpha, time * FadeSpeed);

                    material.color = new Color(
                        material.color.r,
                        material.color.g,
                        material.color.b,
                        a
                    );

                    //Setting fade value
                    material.SetFloat("_DitherThreshold", a);

                }
            }

            time += Time.deltaTime;
            yield return null;
        }

        foreach (Material material in FadingObject.Materials)
        {
            material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
            material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero);
            material.SetInt("_ZWrite", 1);
            material.SetInt("_Surface", 0);

            material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Geometry;

            material.SetShaderPassEnabled("DepthOnly", true);
            material.SetShaderPassEnabled("SHADOWCASTER", true);

            material.SetOverrideTag("RenderType", "Opaque");

            material.DisableKeyword("_SURFACE_TYPE_TRANSPARENT");
            material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
        }

        if (RunningCoroutines.ContainsKey(FadingObject))
        {
            StopCoroutine(RunningCoroutines[FadingObject]);
            RunningCoroutines.Remove(FadingObject);
        }
    }

    private void ClearHits()
    {
        System.Array.Clear(Hits, 0, Hits.Length);
    }

    private FadingObject GetFadingObjectFromHit(RaycastHit Hit)
    {
        return Hit.collider != null ? Hit.collider.GetComponent<FadingObject>() : null;
    }

}

在Unity下实现Normal Smooth以解决描边断裂的问题

啥也不说先上代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using Unity.Jobs;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;

public class OutLineBaker : AssetPostprocessor
{
    static bool bExecuting = false;
    [MenuItem("Tools/BakeNormal", false, 100)]
    
    public static void BakeNormal()
        {
        Object obj = Selection.activeObject;
        string path = AssetDatabase.GetAssetPath(obj);
        string ext = Path.GetExtension(path);

        if(ext == ".fbx")
        {
            bExecuting = true;
            string newPath = Path.GetDirectoryName(path) + "/copy_@@@" + Path.GetFileName(path);
            Debug.Log($"new Path= {newPath}");
            AssetDatabase.CopyAsset(path, newPath);
            //AssetDatabase.ImportAsset(newPath);


        }
        else
        {
            Debug.Log($"必须对.fbx文件进行此操作");
        }
        //Debug.Log($"EXT name = {ext}");

        }
    static void OnPostOver(string path,bool isCopy)
    {
        if (isCopy)
        {
            string srcPath = path.Replace("copy_@@@", "");
            Debug.Log($"复制体后处理后,本体的path={srcPath}");
            AssetDatabase.ImportAsset(srcPath);
        }
        else
        {
            Debug.Log($"处理完毕,关闭开关");
            bExecuting = false;
        }

    }
    void OnPreprocessModel()
    {
        if (!bExecuting) return;
        //Input Settings

        ModelImporter importer = assetImporter as ModelImporter;

        if (!assetImporter.assetPath.Contains("copy_@@@")) return;

        importer.importNormals = ModelImporterNormals.Calculate;
        importer.normalCalculationMode = ModelImporterNormalCalculationMode.AngleWeighted;
        importer.normalSmoothingAngle = 180f;


        importer.importAnimation = false;//no animation imported
        importer.materialImportMode = ModelImporterMaterialImportMode.None;//no material imported
        Debug.Log($"1");
    }

    void OnPostprocessModel(GameObject go)
    {
        if (!bExecuting) return;
        //AfterInputing Settings

        if(go.name.Contains("copy_@@@"))
        {
            //Debug.Log($"Enter the copy obj postprocess");
            OnPostOver(assetPath, true);
        }
        else
        {
           // Debug.Log($"Self");
            string copyPath = Path.GetDirectoryName(assetPath) + "/copy_@@@" + Path.GetFileName(assetPath);
            Debug.Log($"复制体的Path = {copyPath}");
            GameObject cGo = AssetDatabase.LoadAssetAtPath<GameObject>(copyPath);

            Dictionary<string, Mesh> dic_name2mesh_src = GetMesh(go);//原本模型的各个子Mesh,通过节点名索引
            Dictionary<string, Mesh> dic_name2mesh_copy = GetMesh(cGo);//平滑模型各个子Mesh,通过节点名索引

            foreach (var item in dic_name2mesh_src) 
            {
                item.Value.colors = GetColor(item.Value,dic_name2mesh_copy[item.Key]);
            }

            OnPostOver("", false);


        }

      
    }
    Dictionary<string,Mesh>GetMesh(GameObject go)
    {
        Dictionary<string, Mesh> dic = new();
        foreach (var item in go.GetComponentsInChildren<MeshFilter>())
        {
            string n = item.name.Replace("copy_@@@", "");
            dic.Add(n, item.sharedMesh);
        }

        if (dic.Count == 0)
        {
            foreach (var item in go.GetComponentsInChildren<SkinnedMeshRenderer>())
            {
                string n = item.name.Replace("copy_@@@", "");
                dic.Add(n, item.sharedMesh);
            }
        }

        return dic;
    }

    Color[] GetColor(Mesh srcMesh,Mesh smthMesh)
    {
        //按照顶点位置索引,烘焙信息
        int lenSrc = srcMesh.vertices.Length;
        int lenSmth = smthMesh.vertices.Length;
        int maxOverlap = 10;

        NativeArray<Vector3> arr_vert_smth = new(smthMesh.vertices,Allocator.Persistent);//Allocator为资源分配的方式,一般选择Persistent
        NativeArray<Vector3> arr_normal_smth = new(smthMesh.normals, Allocator.Persistent);

        NativeArray<UnsafeHashMap<Vector3, Vector3>> arr_vert2normal = new(maxOverlap, Allocator.Persistent);
        NativeArray<UnsafeHashMap<Vector3, Vector3>.ParallelWriter> arr_vert2normal_writer = new(maxOverlap, Allocator.Persistent);

        for (int i = 0; i < maxOverlap; i++)            
        {
            arr_vert2normal[i] = new UnsafeHashMap<Vector3, Vector3>(lenSmth, Allocator.Persistent);
            arr_vert2normal_writer[i] = arr_vert2normal[i].AsParallelWriter();
        }


        CollectNormalJob collectJob = new()
        {
            verts = arr_vert_smth,
            normals = arr_normal_smth,
            arr_vert2normal= arr_vert2normal_writer
        };

        JobHandle collectHandle = collectJob.Schedule(lenSmth, 128);
        collectHandle.Complete();




        NativeArray<Vector3> arr_vert_src = new(srcMesh.vertices, Allocator.Persistent);
        NativeArray<Vector3> arr_nor_src = new(srcMesh.normals, Allocator.Persistent);
        NativeArray<Vector4> arr_tgt_src = new(srcMesh.tangents, Allocator.Persistent);

        NativeArray<Color> arr_color = new(lenSrc, Allocator.Persistent);

        BakeNormalJob bakeJob = new()
        {
            normals = arr_nor_src,
            verts = arr_vert_src,
            tangents = arr_tgt_src,
            bakedNormals = arr_vert2normal,
            colors = arr_color

        };

        JobHandle bakeHandel = bakeJob.Schedule(lenSrc,128);
        bakeHandel.Complete();//烘焙完成

        Color[] cols = new Color[lenSrc];
        arr_color.CopyTo(cols);

        foreach (var item in arr_vert2normal)
        {
            item.Dispose();
        }
        arr_vert_smth.Dispose();
        arr_normal_smth.Dispose();
        arr_vert2normal.Dispose();
        arr_vert2normal_writer.Dispose();
        arr_vert_src.Dispose();
        arr_nor_src.Dispose();
        arr_tgt_src.Dispose();
        arr_color.Dispose();


        return cols;









    }

    struct CollectNormalJob : IJobParallelFor
    {
        [ReadOnly] public NativeArray<Vector3> verts;

        [ReadOnly] public NativeArray<Vector3> normals;


        //顶点-法线的映射
        [NativeDisableContainerSafetyRestriction]//解除安全锁定


        //顶点-法线
        public NativeArray<UnsafeHashMap<Vector3, Vector3>.ParallelWriter> arr_vert2normal;//NativeArray是一个平行字典,有一个安全封装,一般是无法写入的,所以需要一个.ParallelWriter的写入器
        


        public void Execute(int index)
        {
            //每次执行
            for (int i = 0; i < arr_vert2normal.Length; i++)
            {
                if (i==arr_vert2normal.Length)
                {
                    Debug.Log($"超出顶点数量");
                    break;
                }
                if (arr_vert2normal[i].TryAdd(verts[index], normals[index]))
                {
                    Debug.Log($"当前添加:顶点:{verts[index]},法线:{normals[index]}");
                    break;
                } 
            }
        }
    }

    struct BakeNormalJob : IJobParallelFor
    {
        //切线空间 切线 法线 顶点位置 烘焙好的法线 输出-颜色数组
        [ReadOnly] public NativeArray<Vector3> normals;
        [ReadOnly] public NativeArray<Vector3> verts;
        [ReadOnly] public NativeArray<Vector4> tangents;
        [ReadOnly][NativeDisableContainerSafetyRestriction] public NativeArray<UnsafeHashMap<Vector3, Vector3>> bakedNormals;
        public NativeArray<Color> colors;

        public void Execute(int index)
        {
            Vector3 newNormal = Vector3.zero;

            for (int i = 0; i < bakedNormals.Length; i++)
            {
                if (bakedNormals[i][verts[index]]!= Vector3.zero){
                    newNormal += bakedNormals[i][verts[index]];
                }
                else
                {
                    break;
                }
            }
            newNormal = newNormal.normalized;

            //tangent作为VEC4数值,里面的W存储的是+1或者-1,因为opengl于DX坐标一个左手一个右手,所以拿Z区分
            Vector3 bitangent = (Vector3.Cross(normals[index], tangents[index]) * tangents[index].w).normalized;

            Matrix4x4 tbn = new(
                tangents[index],
                bitangent,
                normals[index],
                Vector4.zero
                );

            tbn = tbn.transpose;

            Vector3 finalNormal = tbn.MultiplyVector(newNormal).normalized;

            Color col = new(

                finalNormal.x*0.5f+0.5f,
                finalNormal.y*0.5f+0.5f,
                finalNormal.z*0.5f+0.5f,
                1



                );

            colors[index] = col;
        }
    }
}

使用之前,现在unity中载入Collection,如果Unity版本大于2022,需要将UnsafeHashMap替代成UnsafeParallelHashMap

1.C++编程简介

基本基础具备:

·学过某种procedure language(C 语言最佳)

·变量

·类型:int,float,char.struct …

·作用域

·循环

·流程控制: if-else,switch-case

·知道一个程序需要编译、链接才能被执行

·知道如何编译和链接


C++ Class

·class without pointer members

-Complex

·class with poniter members

-String

Classes之间的关系

-继承(inheritance)

-复合(composition)

-委托(delegation)

单一Class的设计被称为Object Based(基于对象)

符合Class相互之间有某种关联被称为Object Oriented(面向对象)


C++历史。。略过吧,基本就是发源于C,之前还有B语言,这些在C的时候已经有点了解了

面向对象不止C++还有JAVA跟我们最熟悉的C#


C++

C++语言

C++标准库


捏脸、换装系统总结

最近工作上遇到了与捏脸换装相关的工作内容,由于涉及到了以前未接触过的领域,在这做个工作总结。

捏脸这部分市面上一般有四种解决方案:

1.基于骨骼调整:调整骨骼的Scale、Rotation、Position

2.基于MESH差值:制作两个最大和最小值的Mesh,然后根据差值计算中间的Mesh,运行的时候生产最终的mesh

3.基于材质:简单的来说通过shader与贴图来控制肤色、瞳孔颜色、唇色等。

4.替换Mesh:这个属于是最简单的,但对于美术来说会产生巨量的工作量,一般是用作特殊配件,比如发型、时装、额外的饰品等。

捏脸的系统要保证运行时效率的同时,预览的速度也要做保证,有时候这两者会有一些冲突,可以通过多方案支持、内存换效率之类的方式来进行优化。如果是手游,最好关注下极限情况下的渲染效率和内存消耗,毕竟如果有材质参数的修改是无法合批的,基于骨骼或者mesh的修改的话对于内存也有一定的额外消耗。

基于骨骼调整

其实基于骨骼调整捏脸比较有参考价值的是《Honey Select》中的捏脸系统

对模型资源的规格进行分析, 发现存在大量的morph动画. 也就是说, HS中的的头部骨骼, 全部是用于捏脸的, 表情动画使用MorphTargets(BlendShape)驱动.

对于头部来说这种方式非常便捷,因为HS作为一款端游卖点就是高精度自定义,如果是手机端的话可能可以做相应的取舍。

下面是身体部分

查看其蒙皮信息可以发现, 所有影响顶点的骨骼名字全部带有”_s_”字样, 其父骨骼都是不带”_s_”的同名骨骼. 也就是说, HS的身体骨架中, 父骨骼负责动画, 子骨骼负责蒙皮.

基础的美术工作到这就差不多,但当我们进行调节的时候应该有一个更加平白的表达方式,比如说调节鼻子骨骼的信息,我们的确只需要调节NoseBase的Y值就好了,我们需要做的就是根据滑杆在最大值和最小值之间进行线性插值.

对于”眉毛角度Z轴”的调节, 这时只调节一根骨骼就不对了, 需要左右对称着来. 也就是说, 有一些调节项需要同时调节左右对称的两根骨骼.

对于”眉毛左右位置”, 如果在直线上两个端点之间进行插值, 很容易就跟面部三角形穿插了. 所以这里的插值路径只有最大值和最小值已经满足不了需求了, 而是需要按照曲线进行位置插值, 并且配合旋转插值贴合面部的法线方向. 也就是说, 一个调节项的插值可能是基于曲线(或多个关键帧), 而且可以同时影响骨骼Transform的多个分量.

眼睛的大小调节是最复杂的, 一共影响6根骨骼. 也就是说, 一个调节项是可以对应多根骨骼的.

我们总结一下, 脸型(或体型)调整原理就是:

  • 本质上修改的是骨骼的Local Transform(Translation, Rotation, Scale)
  • 一次只修改Local Transform的某个分量(或多个):Tx/Ty/Tz/Rx/Ry/Rz/Sx/Sy/Sz
  • 使用滑杆在预设的调节范围之间进行插值
  • 插值不一定是线性的, 可能是有多个关键帧
  • 每个调节项可能对应不只一根骨骼

对衣服的资源进行分析可以发现两点值得学习的地方:

  • 每件衣服都配有一个剔除掉被遮住的三角形的裸模, 一方面可以提升绘制性能, 一方面能避免衣服和皮肤两层三角形的穿插
  • 裙摆/披风/长衫等都是共用同样的8条物理骨骼, 算是比较传统的布料模拟做法

基于MESH差值

捏脸主要区域集中在正面,如果美术有能力统一脸部拓扑,使用Mesh差值通过顶点动画实现预设头更好的解决办法。取得新的顶点位置后,要重新计算Mesh法线。

实际脸部形变区域

即使头发使用mesh,头皮依然需要相应颜色的贴图映衬。对于短发寸头,通过对贴图改变发际线的形状也可以取得更多头发的多样性。

捏脸到底使用Mesh差值还是使用骨骼调整,具体还是要看引擎的要求。有的引擎对于Morph Targets优化更好,有的对于骨骼动画优化更好。同时这也取决于你的脸部动画是通过骨骼驱动的还是Morph驱动的。

如果没有性能要求,谁的效果好? 答案肯定是基于Mesh差值的结果。

关于楚留香传的捏脸系统,在额头附近同时调节两个控制器会造成异常的凸起。 这是因为两个控制器同时对相同一部分顶点造成位移而形成的的Double Transformation。可以在美术上normalize两个控制器的影响,或者在程序上限值两个控制器的调整范围。

捏脸是个系统工程,需要美术,程序,设计师多方面的讨论。

说一个不容易发现的细节:有的预设头如果眼睛的距离本身很近,那么控制器是在默认状态就会锁住你的范围,避免你捏出一个眼距过大过小的怪物。