所有由Meecarlo发布的文章

Unity中的移动平台RVT实现

谁在用Runtime Virtual Texture这个技术

· UBI在2016年公布了在Far Cry 4中使用的Adaptive Virtual Texture其中就包含Runtime virtual texture.

·UE在4.25版本发布了Runtime Virtual Texture

·鹅厂大佬公布天涯明月刀中使用了Runtime Virtual Texture,表示2层以上的混合就能表现出明显的性能优势

为什么需要Runtime Virtual Texture

  • 你要做任何的大型的景观,就绕不开使用Unity TerrainUE4 Landscape这样的多层混合材质巨型动态Mesh。
  • 从早年的world machine到现在流行的Houdini在地形制作这个部分都在疯狂地丰富一个东西—图层,8层,堪堪够用而已。

8层在十年前你可以认为是8张颜色贴图加两张控制图的混合,十张,好像是有点多,但是现在是PBR的时代,以Unity的Terrain材质为例8层意味着

(1 Albeto + 1 Normal + 1 Detail[AO R + Metal G + Roughness A])* 8 + 2 Control Map = 26次贴图采样

即每个像素,每帧都会有26次贴图采样。这就是为什么Unity官方推荐移动平台不要超过4层的原因。你以为4层就没事了吗?这个标准只能保证中端设备能跑30帧且烫不烫完全不care。

原理

Runtime Virtual Texture做的事情非常简单,把Terrain Mesh分块,把每块的26张贴图合并成2张render target,这样我们渲染的时候不管有多少图层到了渲染的时候都只有2个贴图采样,但是显存必然是有更大的开销的,空间换时间不多废话。

这里必须提出来一个问题,如果我们完全用静态资源,我们的资源到底有多大?以git工程的示例场景为例,一个传统的RPG视角,我们的LOD 0最小切割到8m * 8m,这样的一块在视角内要满足2k分辨率,需要至少1024分辨率的贴图,当然测试下来512也是可以忍的。对于一块512的Terrain,我们的贴图需求是 4096套1024贴图(albeto + normal + detail),这个资源量,再加上mipmap的开销

一、RVT核心原理

  • 虚拟纹理机制

RVT通过逻辑上的无限大纹理动态加载局部所需纹理,类似内存分页管理:

  1. Feedback阶段:低分辨率渲染获取当前视野所需纹理的mipmap等级和位置信息(RGB存储格子坐标与mip等级)
  2. PageTable管理:维护动态更新的页表,记录纹理加载状态和物理存储位置(LRU算法优化Tile复用)
  3. TileTexture烘焙:通过MRT(多渲染目标)实时烘焙Diffuse、法线等数据到物理贴图集
  • 移动端简化方案
  1. 四叉树动态分块:根据相机距离划分地形区块,每块分配相同尺寸贴图,用Texture2DArray存储(索引即数组下标),避免传统RVT的复杂空间管理
  2. 分帧加载:限制每帧细分/合并操作次数(如每帧最大细分4次),避免卡顿

二、关键实现步骤

  • 四叉树动态LOD管理
  1. 数据结构:节点记录坐标(x,z)、尺寸(size)、物理贴图索引(physicTexIndex)及父子节点引用。
  2. 合并与细分合并:当节点远离相机时,回收子节点贴图索引,用父节点低精度贴图覆盖。细分:靠近相机时分裂为4个子节点,分配新索引加载高精度贴图。
  3. 帧更新优化:每帧遍历叶节点队列,按LOD变化决策合并/细分,交换双队列避免GC
  • 物理贴图生成
  1. Blit替代相机渲染:避免传统相机渲染地形Mesh的开销,直接通过Shader一次性采样所有地表纹理(如16层)输出到Texture2DArray
  2. 索引贴图映射:根据地形UV生成索引贴图,存储节点起始坐标、尺寸、贴图数组索引,Shader采样时直接计算实际UV

三、移动端专项优化

1、纹理与内存管理

  • AlphaMap分组:每4种纹理共用一张AlphaMap,减少贴图切换(如12种纹理需3张图)
  • 异步加载:通过AsyncGPUReadback异步回读Feedback数据,延迟一帧使用避免阻塞

2、海洋主题适配

  • 岛屿独立节点:将岛屿抽象为独立四叉树节点,支持随机分布与紧密数据排列
  • 棋盘格烘焙:以512×512米为单位烘焙地形数据,适配航海类大世界

四、工程实践建议

1、开发管线集成

Houdini流程:用Houdini生成地形/道路/桥梁,导出Unity后通过HLOD System预处理(减面+合批),减少70%+ DrawCall

光照方案

  • 近距离:级联阴影(CSM)
  • 远距离:体素化阴影(VSM)+ LightProbe流式加载

2、调式工具

  • 可视化Feedback贴图与PageTable状态,实时监控Tile加载情况
  • 负反馈性能调节系统:动态限制高负载操作(如分帧细分)

其实吧RVT的核心架构一张图就能说清楚

与传统的贴图方案对比优势

优势场景:

  • 超大规模地形(如飞行模拟中的全球地表)
  • 程序化生成世界(无限地形动态烘焙)
  • 高清材质统一管理(避免重复资源)

下面我一步一步从前往后梳理整个过程,以及在Unity的实现技术细节。

格子

我们会根据可视范围把范围内的所有地块划分成格子,格子越小,分到固定分辨率大小的贴图清晰度就越高,当然性能也就越差。UE是固定格子,直接是以地形为单位划分的。但是对于一个超大世界来讲,这个范围是变化的,所以格子也是动态变化的。这里只交待一个概念,后面在各个部分的时候再详细讲。

FeedBack

feedback有点类似于遮挡剔除,会预先烘一个低分辨率的贴图信息,rgb分别表示格子坐标,mipmap等级。格子坐标通过世界坐标在可视区域,以及格子的大小这些参数就能算出来。mipmap等级则是通过偏导求出来的。



feed_v2f VTVertFeedback(feed_attr v)
{
    feed_v2f o;
    UNITY_SETUP_INSTANCE_ID(v);
    
#if defined(UNITY_INSTANCING_ENABLED)
    float2 patchVertex = v.vertex.xy;
    float4 instanceData = UNITY_ACCESS_INSTANCED_PROP(Terrain, _TerrainPatchInstanceData);

    float2 sampleCoords = (patchVertex.xy + instanceData.xy) * instanceData.z; // (xy + float2(xBase,yBase)) * skipScale
    float height = UnpackHeightmap(_TerrainHeightmapTexture.Load(int3(sampleCoords, 0)));

    v.vertex.xz = sampleCoords * _TerrainHeightmapScale.xz;
    v.vertex.y = height * _TerrainHeightmapScale.y;
    
    v.texcoord = sampleCoords * _TerrainHeightmapRecipSize.zw;
#endif
    
    VertexPositionInputs Attributes = GetVertexPositionInputs(v.vertex.xyz);
    
    o.pos = Attributes.positionCS;
    float2 posWS = Attributes.positionWS.xz;
    o.uv = (posWS - _VTRealRect.xy) / _VTRealRect.zw;
    
    return o;
}

float4 VTFragFeedback(feed_v2f i) : SV_Target
{
	float2 page = floor(i.uv * _VTFeedbackParam.x);

	float2 uv = i.uv * _VTFeedbackParam.y;
	float2 dx = ddx(uv);
	float2 dy = ddy(uv);
	int mip = clamp(int(0.5 * log2(max(dot(dx, dx), dot(dy, dy))) + 0.5 + _VTFeedbackParam.w), 0, _VTFeedbackParam.z);

	return float4(page / 255.0, mip / 255.0, 1);
}

在unity urp配置渲染管线,只需要添加一个renderdata,配置好参数

然后在地形里边添加一个feedback的pass

然后在feedback相机上选择这个render

feedback的相机要保证跟场景相机参数一样,可以写个脚本复制参数,然后把这个相机挂在场景相机,transform归零就ok了。这里我把camera的enable去掉了,我自己来调用render,因为这里可以自己控制更新频率。

这里渲染的时候分辨率降低一半,如果觉得还不够可以再降一些,这里为啥不一开始就用低分辨率呢,是因为,先以一个差不多的分辨率精度比较高,再降是取最大的mipmap等级,这样会保证正确性。

float4 GetMaxFeedback(float2 uv, int count)
{
	float4 col = float4(1, 1, 1, 1);
	for (int y = 0; y < count; y++)
	{
		for (int x = 0; x < count; x++)
		{
			float4 col1 = tex2D(_MainTex, uv + float2(_MainTex_TexelSize.x * x, _MainTex_TexelSize.y * y));
			col = lerp(col, col1, step(col1.b, col.b));
		}
	}
	return col;
}

这里之所以对分辨率要求这么高,是因为我们需要把贴图数据从rendertarget里取出到cpu来读取数据。

// 发起异步回读请求
m_ReadbackRequest = AsyncGPUReadback.Request(texture);

这是一个异步请求,我们可能有一个延迟,也就是说一般是这一帧使用的是上一帧的feedback

PageTable

页表是一个mipmap层级结构的表,比如说我们的格子是一个256×256的矩阵,那么mipmap为0的等级就是256×256的cell,mipmap为1的就是128×128的cell,依次类推。cell的上面除了存放mipmap等级,占据的rect,还有page的加载情况。

public class TableNodeCell
{
        public RectInt Rect { get; set; }

        public PagePayload Payload { get; set; }

        public int MipLevel { get; }

        public TableNodeCell(int x, int y, int width, int height,int mip)
        {
            Rect = new RectInt(x, y, width, height);
            MipLevel = mip;
            Payload = new PagePayload();
        }
}


这里要注意的是我们这的pagetable是个可变化的,针对移动是调整后面单独拎出来说。

上面feedback处理完得到一张feedback的贴图,上面各个像素表示需要显示的cell以及mipmap等级。我们可以通过这个信息,去触发加载TileTexture。

/// <summary>
/// 激活页表
/// </summary>
private TableNodeCell ActivatePage(int x, int y, int mip)
{
            if (mip > MaxMipLevel || mip < 0 || x < 0 || y < 0 || x >= TableSize || y >= TableSize)
                return null;
            // 找到当前页表
            var page = m_PageTable[mip].Get(x, y);
            if(page == null)
            {
                return null;
            }
            if(!page.Payload.IsReady)
            {
                LoadPage(x, y, page);

                //向上找到最近的父节点
                while(mip < MaxMipLevel &amp;&amp; !page.Payload.IsReady)
                {
                    mip++;
                    page = m_PageTable[mip].Get(x, y);
                }
            }

            if (page.Payload.IsReady)
            {
                // 激活对应的平铺贴图块
                m_TileTexture.SetActive(page.Payload.TileIndex);
                page.Payload.ActiveFrame = Time.frameCount;
                return page;
            }

            return null;
}
/// <summary>
/// 加载页表
/// </summary>
private void LoadPage(int x, int y, TableNodeCell node)
{
   if (node == null)
     return;

   // 正在加载中,不需要重复请求
   if(node.Payload.LoadRequest != null)
       return;

   // 新建加载请求
   node.Payload.LoadRequest = m_RenderTextureJob.Request(x, y, node.MipLevel);
}

TileTexture

tileTexture会设置tile的size,每个size的分辨率大小,以及padding像素

在这里我们有一个渲染队列,由于同一帧可能请求比较多,我们可以从mipmap等级高到底排序,然后控制每帧渲染数量来进行。

public void Update()
{
            if (m_PendingRequests.Count <= 0)
                return;

            // 优先处理mipmap等级高的请求
            m_PendingRequests.Sort((x, y) => { return x.MipLevel.CompareTo(y.MipLevel); });

            int count = m_Limit;
            while (count > 0 &amp;&amp; m_PendingRequests.Count > 0)
            {
                count--;
                // 将第一个请求从等待队列移到运行队列
                var req = m_PendingRequests[m_PendingRequests.Count - 1];
                m_PendingRequests.RemoveAt(m_PendingRequests.Count - 1);

                // 开始渲染
                StartRenderJob?.Invoke(req);
            }
}

在处理一个具体的tile时,由于我们需要烘焙diffuse,normal,Mask(暂时没处理),我们这里使用的MRT。

然后就转化到要把page上的某个rect,画到TileTexture的某个tile上了,这里还得考虑padding这些,涉及到非常繁琐的tileoffset的计算,这里花了我不少时间。除了渲染地形,以后如果有些需要贴到地形上的贴花也在这个过程中进行。

drawtexture的shader也就是常规的blend混合,这里要注意的是,对于地形超出四层的情况,第一个四层正常渲染,第二个四层以及后面的贴花可以采用Blend One One的形式

PixelOutput frag(v2f_drawTex i) : SV_Target
{
    float4 blend = tex2D(_Blend, i.uv * _BlendTile.xy + _BlendTile.zw);
    
#ifdef TERRAIN_SPLAT_ADDPASS
    clip(blend.x + blend.y + blend.z + blend.w <= 0.005h ? -1.0h : 1.0h);
#endif
    
    float2 transUv = i.uv * _TileOffset1.xy + _TileOffset1.zw;
    float4 Diffuse1 = tex2D(_Diffuse1, transUv);
    float4 Normal1 = tex2D(_Normal1, transUv);
    transUv = i.uv * _TileOffset2.xy + _TileOffset2.zw;
    float4 Diffuse2 = tex2D(_Diffuse2, transUv);
    float4 Normal2 = tex2D(_Normal2, transUv);
    transUv = i.uv * _TileOffset3.xy + _TileOffset3.zw;
    float4 Diffuse3 = tex2D(_Diffuse3, transUv);
    float4 Normal3 = tex2D(_Normal3, transUv);
    transUv = i.uv * _TileOffset4.xy + _TileOffset4.zw;
    float4 Diffuse4 = tex2D(_Diffuse4, transUv);
    float4 Normal4 = tex2D(_Normal4, transUv);

    PixelOutput o;
    o.col0 = blend.r * Diffuse1 + blend.g * Diffuse2 + blend.b * Diffuse3 + blend.a * Diffuse4;
    o.col1 = blend.r * Normal1 + blend.g * Normal2 + blend.b * Normal3 + blend.a * Normal4;
    return o;
}

(这里注意采样纹理一定要自己指定mipmap等级为0,不然显卡会降mipmap等级导致变模糊,这也是我后面发现的,文章和demo都没修改)

画完后就要通知pagetable我加载完毕,并且更新cell上的加载信息。

LookUp
在pagetable每帧更新中,会将TileTexture各个Tile激活下的数据写入lookup贴图,这个lookup的尺寸跟pagetable的cell格子数一样大小,rgb分别表示,cell坐标,mipmap等级。

这里要注意几个地方,第一,Active的page只需取存在feedback上的activepage,而不需取整个TileTexture上的。第二,active的page可能会有重合的地方,这里以mipmap等级低的覆盖等级高的。由于有这两点,我起初是直接在CPU创建一张Texture2D,往里填数据,后来测试发现这样会非常慢,特别是Texture2D的Apply。后来参考UE的代码采用GPUInstance直接画上去,这里由于需要使用mipmap等级低的覆盖高的,可以使用画家算法,按mipmap排序。ue还使用了莫顿码,我这里直接使用InstanceData传入进去整个数据

var mats = new Matrix4x4[drawList.Count];
var pageInfos = new Vector4[drawList.Count];
for(int i=0;i<drawList.Count;i++){
    float size = drawList[i].rect.width / TableSize;
    mats[i] = Matrix4x4.TRS(
                    new Vector3(drawList[i].rect.x / TableSize, drawList[i].rect.y / TableSize), 
                    Quaternion.identity,
                    new Vector3(size, size, size));
    pageInfos[i] = new Vector4(drawList[i].drawPos.x, drawList[i].drawPos.y, drawList[i].mip / 255f,0);
}
Graphics.SetRenderTarget(m_LookupTexture);
var tempCB = new CommandBuffer();
var block = new MaterialPropertyBlock();
block.SetVectorArray("_PageInfo", pageInfos);
block.SetMatrixArray("_ImageMVP", mats);
tempCB.DrawMeshInstanced(mQuad, 0, drawLookupMat,0, mats, mats.Length, block);
Graphics.ExecuteCommandBuffer(tempCB);

shader代码

UNITY_INSTANCING_BUFFER_START(InstanceProp)
UNITY_DEFINE_INSTANCED_PROP(float4, _PageInfo)
UNITY_DEFINE_INSTANCED_PROP(float4x4, _ImageMVP)
UNITY_INSTANCING_BUFFER_END(InstanceProp)
Varyings vert(Attributes IN)
{
	Varyings OUT ;
	UNITY_SETUP_INSTANCE_ID(IN);
	float4x4 mat = UNITY_MATRIX_M;
	mat = UNITY_ACCESS_INSTANCED_PROP(InstanceProp, _ImageMVP);
	float2 pos = saturate(mul(mat, IN.positionOS).xy);
	pos.y = 1 - pos.y;
	OUT.positionHCS = float4(2.0 * pos  - 1,0.5,1);
	OUT.color = UNITY_ACCESS_INSTANCED_PROP(InstanceProp, _PageInfo);
	return OUT;
}

half4 frag(Varyings IN) : SV_Target
{
	return IN.color;
}

这里Unity有几个坑逼的地方,首先unity调用drawMeshInstanced传入的那个矩阵他会修改里边的数据,导致我不能直接使用那个数据,因为我这里不是标准的MVP矩阵,他可能帮我转换了,我这里重新弄了一个矩阵instanceData。还有一点就是如果你在shader里不使用UNITY_MATRIX_M这个变量,unity就认为你这个不是drawInstance。所以我在shader里加了一句无意义的代码float4x4 mat = UNITY_MATRIX_M;

DrawVT

所有数据准备完毕后,现在就比较简单了。直接使用RVT贴图里的diffuse和Normal,然后参与光照计算就OK了。

half4 GetRVTColor(Varyings IN)
{
    float2 uv = (IN.positionWS.xz - _VTRealRect.xy) / _VTRealRect.zw;
    float2 uvInt = uv - frac(uv * _VTPageParam.x) * _VTPageParam.y;
	float4 page = tex2D(_VTLookupTex, uvInt) * 255;
#ifdef _SHOWRVTMIPMAP
    return float4(clamp(1 - page.b * 0.1 , 0, 1), 0, 0, 1);
#endif
	float2 inPageOffset = frac(uv * exp2(_VTPageParam.z - page.b));
    uv = (page.rg * (_VTTileParam.y + _VTTileParam.x * 2) + inPageOffset * _VTTileParam.y + _VTTileParam.x) / _VTTileParam.zw;
    half3 albedo = tex2D(_VTDiffuse, uv);
    half3 normalTS = UnpackNormalScale(tex2D(_VTNormal, uv), 1);
    InputData inputData;
    InitializeInputData(IN, normalTS, inputData);
    half metallic = 0;
    half smoothness = 0.1;
    half occlusion = 1;
    half alpha = 1;
    half4 color = UniversalFragmentPBR(inputData, albedo, metallic, /* specular */ half3(0.0h, 0.0h, 0.0h), smoothness, occlusion, /* emission */ half3(0, 0, 0), alpha);
    SplatmapFinalColor(color, inputData.fogCoord);

    return half4(color.rgb, 1.0h);
}

动态更新
以上基本就能把地形通过rvt画出来了,但是这样有个基本限制,那就是我们移动的时候,如果是超大世界,那么这个pagetable对应的位置就有限。ue的解决办法是每个地形一套rvt,这样会造成,在地形边界可能出现四套rvt,这基本不能接受。天刀采用的是动态更新pagetable,也就是说pagetable对应的cell表示的范围会动态变化。

在changerect的时候,我们需要最大限度地复用TileTexture,不然需要重绘所有的TileTexture,这样会造成比较大的卡顿。这里对于changrect的rect的规范就比较有讲究了,我们需要fixed这个rect,让他尽可能地多复用pagetable。我们这是通过设置更新距离为整个可视距离的四分之一或者八分之一这种,然后通过这个四分之一去fixed可视范围center

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通过程序加载和销毁。

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中添加以上代码以及修改以下代码

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

动态平面阴影

Shader "fake_shadow"
{
    Properties
    {
        //材质属性面板
        _MainTex ("主贴图",2D) = "white"{}

        _GroundY ("地面Y高度 (外部传入)",float) = 0
        _Shadow_Color("影子颜色",Color) = (1,1,1,1)
        _Shadow_Length("影子长度",float) = 0
        _Shadow_Rotated("影子旋转角度",float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue" = "Transparent+5" /*"RenderType"="Transparence"  "RenderPipeline" = "UniversalPipeline" *///注意这里很重要,因为影子是要绘制在地面上,所以地面必须应该先绘制,否则blend混合的时候就是和背后的skybox进行混合了
        }

        pass
        {
            Stencil{

                Ref 1
                //Comp取值依次为  0:Disabled  1:Never  2:Less  3:Equal  4:LessEqual  5:Greater  6:NotEqual  7:GreaterEqual  8:Always
                Comp NotEqual //或者改成NotEqual
                //Pass取值依次为  0:Keep  1:Zero  2:Replace  3:IncrementSaturate  4:DecrementSaturate  5:Invert  6:IncrementWrap  7:DecrementWrap
                Pass Replace
            }

            Blend SrcAlpha oneMinusSrcAlpha   

            //因为和地面重叠所以做个偏移
            //也可以不做偏移,将传入的地面高度抬高一点即可
            Offset -2,-2

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                //这里worldPos一定是float4,因为vert()中实际是手动两次空间变换如果是float3会导致w分量丢失,透视除法会出错
                //如果不参与变换,只是传到frag()中使用的话,比如进行Blinn-Phong光照计算V向量那么float3就够了
                float4 worldPos : TEXCOORD0;
                //做阴影插值和Clip地面以下阴影用
                float cacheWorldY : TEXCOORD1;
                float worldPosY : TEXCOORD2;
};
 //           CBUFFER_START
CBUFFER_START(UnityPerMaterial)
            half _GroundY;
            half4 _Shadow_Color;   
            half _Shadow_Length;     
            half _Shadow_Rotated;
CBUFFER_END
//            CBUFFER_END
            
            v2f vert(appdata v)
            {
                v2f o = (v2f)0;

                //获取世界空间的位置
                o.worldPos = mul(unity_ObjectToWorld,v.vertex);
                //缓存世界空间下的y分量,后续两点作用
                //第一点 : 做插值用做计算xz的偏移量的多少
                //第二点 : 防止在地面以下
                o.cacheWorldY = o.worldPos.y;
    
                o.worldPosY = UnityObjectToWorldNormal(v.vertex).y;

                //设置世界空间下y的值全部都设置为传入的地面高度值
                o.worldPos.y = _GroundY;

                //根据世界空间下模型y值减去传入的地面高度值_GroundY
                //以这个值为传入 lerp(0,_Shadow_Length) 进行线性插值
                //最后获取到模型y值由低到高的插值lerpVal
                //这个max()函数 假设腿部在地面以下则裁切掉腿部阴影,后续使用clip后无需Max
                //half lerpVal = lerp(0,_Shadow_Length,max(0,o.cacheWorldY-_GroundY));
                half lerpVal = lerp(0,_Shadow_Length,o.cacheWorldY-_GroundY);
                

                //常量PI
                //const float PI = 3.14159265;
                //角度转换成弧度
                half radian = _Shadow_Rotated / 180.0 * UNITY_PI;

                //旋转矩阵,对(0,1)向量进行旋转,计算旋转后的向量,该向量就是阴影方向
                //2D旋转矩阵如下
                // [x]        [ cosθ , -sinθ ]
                // [ ]  乘以  
                // [y]        [ sinθ , cosθ  ]
                // x' = xcosθ - ysinθ
                // y' = xsinθ + ycosθ
                half2 ratatedAngle = half2((0*cos(radian)-1*sin(radian)),(0*sin(radian)+1*cos(radian)));
                
                //用以y轴高度为参考计算的插值 lerpVal 去 乘以一个旋转后的方向向量,作为阴影的方向
                //最终得到偏移后的阴影位置
                o.worldPos.xz += lerpVal * ratatedAngle;
                
                //变换到裁剪空间
                o.pos = mul(UNITY_MATRIX_VP,o.worldPos);

                return o;
            }

            fixed4 frag(v2f i) : SV_TARGET
            {
                //剔除低于地面部分的片段
                clip(i.cacheWorldY - _GroundY);
                //用作阴影的Pass直接输出颜色即可
                return _Shadow_Color;
            }

            ENDCG
        }
    }
}
using UnityEngine;

public class SetShadowRotationAndHeight : MonoBehaviour
{

    public int materialIndex = 2; // 设置材质索引

    [SerializeField]
    private Transform rootTransform = null;          //创建模型时设置

    private Renderer rend = null;
    private bool isFindRend = false;
    private float extend = 30f;


    private Transform lightTrans=null;

    void Awake()
    {
        if (GetComponent<MeshFilter>() != null)
        {
            Mesh mesh = GetComponent<MeshFilter>().mesh;
            Bounds bd = mesh.bounds;
            bd.Expand(extend);
            mesh.bounds = bd;
        }
        else if(GetComponent<SkinnedMeshRenderer>() != null)
        {
            //SkinnedMeshRenderer skinnedMesh = GetComponent<SkinnedMeshRenderer>();
            //Bounds bd = skinnedMesh.bounds;
            //bd.Expand(extend);
            //skinnedMesh.bounds = bd;
        }
    }

    public void SetRootTransFrom(Transform root)
    {
        rootTransform = root;

        //GameObject mainLightObj = GameObject.Find("MainLight_Day");
        //if(mainLightObj==null)
        //{
        //    mainLightObj = GameObject.Find("FillLight_Day");
        //}
        //if (mainLightObj != null)
        //    lightTrans = mainLightObj.transform;
    }

    public Renderer GetTransformRenderer()
    {
        if (rend != null)
            return rend;
        if (isFindRend)
            return null;
        rend= GetComponent<Renderer>();
        isFindRend = true;
        return rend;
    }

    bool isDebug = false;

    void Update()
    {
        //if (lightTrans == null)
        //{
        //    if(!isDebug)
        //    {
        //        Debug.Log("找不到光源");
        //        isDebug = true;
        //    }
        //    return;
        //}
            

        if (rootTransform == null)
        {
            rootTransform = transform;
        }
        Renderer tempRend = GetTransformRenderer();

        if (tempRend == null)
            return;

        // 确保指定的材质索引有效
        if (materialIndex < 0 || materialIndex >= tempRend.materials.Length)
        {
            Debug.LogError(transform.name + "Invalid material index!");
            return;
        }

        Material mat = tempRend.materials[materialIndex]; // 获取指定索引的材质

        // 获取父节点的 Y 坐标
        float parentY = rootTransform.position.y + 0.2f;

        // 将父节点的 Y 坐标传递给指定索引的材质的 _GroundY 属性
        mat.SetFloat("_GroundY", parentY);

        // 获取主光源的方向

        if (RenderSettings.sun == null)
            return;

        Vector3 lightDirection = -RenderSettings.sun.transform.forward;
        //Vector3 lightDirection = -lightTrans.forward;

        // 计算主光源的旋转角度的 Y 值(使用 Atan2 函数)
        float shadowRotationY = Mathf.Atan2(lightDirection.x, lightDirection.z) * Mathf.Rad2Deg;

        // 将主光源的旋转角度的 Y 值传递给指定索引的材质的 _Shadow_Rotated 属性
        mat.SetFloat("_Shadow_Rotated", shadowRotationY);
    }
}

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++标准库