TECH.ART.FLOW.IO

[번역] 미호요 스타레일 바텐딩 효과(레이어드 액체 병) 복제 시도

jplee 2024. 5. 2. 18:09

역자의 말.
역시 지후 에는 참 다양한 개발자들이 많아요. 중국은 한국에 비해서 개발자 인력 레인지나 수가 압도적으로 많고 개인 개발이나 개인 취미 연구가들도 무척 많습니다. 암튼... 재미있는 복원 연구 주제가 있으니 시간이 있다면 시도 해 보는 것도 좋을 듯 하여 공유 해 봅니다.


저자

개요

——맞아요, 바로 이 이벤트입니다.

—— 불과 한 달 전 이 이벤트를 시작했는데, 이 액체층 효과를 보고 너무 재미있어서 혼자 해보고 싶은 충동이 생겼습니다.(마침 일파만파로 보던 액체병 효과를 실제로 해보기 시작했습니다)

—— 하지만 생각보다 더 많은 기술적인 부분이 있다는 것을 알게 되었고, 이 기사는 파이프라인 버전을 변경하지 않았습니다(투명 정렬 및 굴절에 대한 RenderFeature 완벽한 솔루션이 없음). 그리고 나서 다른 기사를 훑어보고 프로젝트 파일과 코드를 넣었습니다.

——지금까지 구현된 방식에 비추어 볼 때, 이번 업데이트의 포괄성은 1~2년 전의 프로토-고드 사막을 쉽게 뛰어넘습니다. 사용된 기술 포인트에는 분리 및 부착, 지그재그 구멍, 반투명도, 커스텀 ZWrite, 스텐실, 컴퓨트 셰이더, 텍스처 배열, 게임 플레이 레이어의 일부 애니메이션 커브, 블릿 및 부력 시뮬레이션을 위한 로직 등이 포함되지만 이에 국한되지 않습니다. (랭구스 이펙트는 매우 전문화되어 있어 결합도가 매우 높으며 많은 기술 포인트는 다른 이펙트를 위한 아이디어로만 사용할 수 있습니다.)

关于原神沙漠痕迹效果的踩坑记录

——因为个人之前活动的平台缺乏有营养的可以促进双方思考与进步的评论(毕竟平台的主打内容和用户群体不同),所以思考(与自己的懒惰做斗争)过后,技术相关的文章以后还是来知乎或C

zhuanlan.zhihu.com

——현재 결과는 다음과 같습니다.

——참고로 컵이 기울어졌을 때 액체 수위 보정도 해봤습니다.

효과 분해

——여기서는 (막힐까 봐) 프레임을 잡아서 보지 않고, 예술적 직관에 기반한 육안 분석으로 진행 --- ---.
1、직관적으로 이 효과는 액체, 얼음, 유리의 세 가지 레이어로 구성됩니다. 유리의 벽이 얇고 안팎의 모양이 동일한 경우 액체와 유리는 단일 재료 구현으로 병합된 것으로 간주할 수 있습니다(이 문서에서는 분리되어 있음).
2、안경 및 얼음 조각은 모두 간단한 매트캡 BlendAD(또는 AB)로 구현할 수 있으며, 얼음 조각은 GPU 인스턴스로 이동하여 drawCall을 합성할 수도 있습니다.
3、가장 중요한 액체 효과인 각 액체는 색상(투명도 포함), 정적인 액체 디테일(내벽의 기포, 입자 등), 동적인 부유 기포의 세 가지 레이어로 구성됩니다.

4、액체의 다른 장식으로는 테두리 조명, 서로 다른 액체 층 사이의 부드러운 전환(섭동 포함), 액체 표면의 변동하는 변형이 있습니다.

5、실제로 원래 효과에는 따르거나 저을 때 약간의 왜곡을 만드는 정적 마스크와 같은 터무니없는 세부 사항이 있지만 지금은 여기서는 구현되지 않습니다.

렌더링 시퀀스 분석

기본 파이프라인의 한계

——컵은 기본적으로 최종 렌더링에서 확정됩니다.
——액체와 얼음이 섞이기 전과 후를 구분하기는 쉽지 않습니다.

——파이프라인을 변경하지 않고 철로를 바라보는 효과를 생각해 보세요. 수면 위의 얼음은 배경과 액체에 대해 굴절되어 액체가 얼음보다 먼저 렌더링되고 액체가 얼음에 컬러 버퍼를 캐시하여 샘플을 생성하지만(U3D의 _카메라오피크텍스처에 해당), 수면 아래의 얼음은 다시 다른 레이어 투명도로 액체에 의해 가려지고 혼합되어 얼음이 있음을 나타냅니다. 액체 앞에 렌더링
——기본 파이프라인으로 렌더링하면 위의 모순된 결과, 즉 두 개의 반투명 표면이 교차하는 경우가 발생하여 메시를 가장 작은 정렬 단위로 사용하는 페인터의 알고리즘이 올바른 투명 블렌딩 결과를 얻지 못할 수밖에 없습니다.
——투명 패스 전에 리퀴드를 렌더링하여 컬러 버퍼와 뎁스 버퍼가 _CameraOpaqueTexture 와 _CameraDepthTexture 에 들어가도록 한 다음, 아이스 큐브의 PS 에서 샘플링하고 수작업으로 깊이를 비교하여 투명 블렌딩]의 픽셀 세분성을 고려하더라도, 이 픽셀 세분성을 고려하더라도 정렬은 하늘 구체가 _카메라오파크 텍스처를 렌더링하는 마지막 패스이므로, 그 후에 액체 패스를 추가해도 두 글로벌 텍스처에 포함되지 않는 방식으로 이루어집니다.
—— 직접 스카이 스피어를 만들어 포장한 다음 스카이 스피어 머티리얼의 대기열 값을 전진시키지 않는 한 말이죠. 하지만 그렇게 해도 이론적으로 [물리적으로] 혼합이 제대로 되지 않습니다. 액체와 스카이 스피어 사이에 아이스 큐브 레이어가 끼어 있지만, 위의 레이어 순서는 액체 - 스카이 스피어 - 아이스 큐브가 될 뿐이기 때문입니다.
—— 따라서 이 효과를 내기 위해 SRP도 작성했어야 합니다.

피어싱 및 반투명

—— 이 글에서는 파이프라인을 변경하지 않고 효과를 비교한 후 [16차 타이홀 반투명]을 사용하여 올바른 투명도 효과를 근사화했습니다. (덤불 속을 걷거나 페이퍼보이의 치마 속을 들여다보려고 할 때 얻을 수 있는 효과입니다 - -).

Unity Shader 点阵像素剔除半透(Stipple Transparency )

最近一段时间一直不停的刷只狼(打铁真好玩.......),无意中发现主角被场景物体遮挡的时候,半透效果比较有意思,是像马赛克一样被剔除掉了。 上图是我在游戏里截的,在上传以后被压缩

zhuanlan.zhihu.com

—— 픽셀의 공간밀도(발생확률)로 가중치를 근사화하는 방식으로 부피 알고리즘, 광추적 등 응용에 효과적입니다.-

——(사실, 무방비 상태인 것이 더 나을 수도 있습니다...)

结论

—— 이 기사의 구현에서는 얼음과 액체의 굴절과 배경은 무시하고 얼음 뒤의 액체만 얼음을 통해 보이는 것으로 간주하고 얼음 앞의 다른 투명도 액체를 투명하게 혼합하는 것을 고려합니다. 전자는 전체 얼음 조각에 균일한 지그재그 반투명도를 적용하여 시뮬레이션하고, 후자는 얼음 조각을 액체 앞에 렌더링하고 액체의 알파블렌드 모드를 사용하여 실제 투명도 블렌딩을 수행합니다.
—— 최종 렌더링 순서: 얼음 - 액체 - 유리

리퀴드 리소스

—— 리소스 구성을 위한 아트 제작을 용이하게 하기 위해 각 액체 레이어의 직렬화 가능한 추상화는 스크립터블 오브젝트를 사용하여 수행할 수 있습니다. 이 문서에서는 액체를 다음 네 가지 프로퍼티로 추상화합니다.

using UnityEngine;

[CreateAssetMenu(menuName = "自定义资源/Liquid")]
public class Liquid : ScriptableObject
{
    public Color RGBA;                          // 颜色 & 不透明度
    public float bubbleIntensity;               // 泡沫强度
    public Texture2D maskTex;                   // 静态细节遮罩纹理
    [Range(0, 0.5f)] public float lerpRange;    // 向下层的过渡宽度

    public Liquid(Liquid liquidIN)
    {
        RGBA = liquidIN.RGBA;
        maskTex = liquidIN.maskTex;
        bubbleIntensity = liquidIN.bubbleIntensity;
        lerpRange = liquidIN.lerpRange;
    }
}

높은 액체 처리 능력

인클로저의 상대적 높이

—— 물론 DCC에서 모델의 바닥을 원점으로 이동하고 로컬 좌표를 사용하여 액체의 상대적 높이를 계산할 수 있습니다. 하지만 이 문서에서는 컵이 기울어졌을 때 액체 레벨 보정을 구현하므로 다음과 같이 약간 수정했습니다.

// 둘러싸는 상자 기반 mesh의 로컬 좌표계 원점 획득
float3 GetRelativeOrigin(float3 posWS)
{
    float3 posWS_Origin = (unity_RendererBounds_Min + unity_RendererBounds_Max) * 0.5;
    posWS_Origin.y = unity_RendererBounds_Min.y;
    return posWS_Origin;
}

—— 오브젝트의 월드 둘러싸기 상자 아래쪽 표면의 중심을 로컬 좌표의 원점으로 삼습니다. 이 로컬 공간의 세 기본 벡터는 월드 공간과 평행하고 스케일링되지 않으므로 이 상대 좌표를 얻으려면 posWS - posWS_Origin만 필요합니다.

클립 레벨

—— 물을 추가하거나 교반할 때 액체 수위 변동의 효과를 얻기 위해 여기서는 상대 좌표 .xz를 사용하여 높이 섭동 맵을 클립으로 샘플링합니다.

// 가장자리 높이
float2 uv_Edge = _HeightMap_SV.xy * pos_Relative.xz + _HeightMap_SV.zw * _Time.x;
half h_Edge = _HeightWarpInt * (tex2D(_HeightMap, uv_Edge).r - 0.5);

// clip
half liquidHeightOS = _MaxLiquidHeightOS * _LiquidHeight01;
half clipH = liquidHeightOS - pos_Relative.y + h_Edge;
clip(clipH);

—— 여기서 pos_Relative는 둘러싸는 상자의 이전에 계산된 상대 좌표이고, _MaxLiquidHeightOS는 액체가 도달할 수 있는 최대 상대 높이입니다.
—— 그러면 액체 레벨의 높이를 외부에서 제어할 수 있는 _LiquidHeight01이 노출됩니다.
—— 수평면 양쪽에서 섭동이 발생하도록 샘플링 결과가 -0.5(일종의 나쁜 연산 - -)가 되어야 섭동 강도가 변할 때 효과가 더 자연스럽다는 점에 유의해야 합니다.
직접 섭동(물리학)

-0.5 포스트 스크램블링

리퀴드 레이어 구현

계층적 배열 ID

—— 가능한 한 적은 샘플로 drawCall에서 다른 마스크로 리퀴드 레이어링 효과를 얻으려면 플립북, Texture2DArray, Texture3D 등을 선택할 수 있습니다. 플립북은 단일 드로잉의 해상도를 제한하고 UV 매핑 및 타일링 계산이 번거롭기 때문입니다. 플립북은 단일 이미지의 해상도를 제한하고 UV 매핑 및 타일링 계산이 더 번거롭고 Texture3D 샘플링과 삼선 보간의 간섭, 효과와 성능 (코드 작성의 우아함 ---)을 종합적으로 고려하기 때문에 여기서는 여전히 Texture2DArray를 사용하고 있습니다.
—— 마찬가지로, 위의 RGBA, bubbleIntensity 및 lerpRange의 경우 고정 길이 배열을 사용하여 셰이더에서 직접 구현할 수 있습니다.

#define MAX_LAYER_NUM 5
half4 liquidRGBA[MAX_LAYER_NUM];
half liquidBubbleInt[MAX_LAYER_NUM];
half liquidLerpRange[MAX_LAYER_NUM];

—— 그렇다면 핵심은 실제로 각 레이어에 대한 배열 ID를 얻는 방법입니다.

—— 위의 두 층은 높이가 같으며 빨간색 영역은 두 액체의 혼합이 발생하는 영역입니다. 혼합 영역의 상하 경계는 파란색 선(즉, 레이어의 중심선)을 초과하지 않습니다.
—— 앞서 계산한 상대 높이를 사용하여 [0, n]으로 리매핑하고(n은 현재 액체의 최대 레이어 수이며 Texture2DArray의 레이어 수와 같음) 바닥을 현재 픽셀이 위치한 레이어의 ID로 취합니다. 그러나 현재 픽셀의 블렌딩에 관련된 상하 레이어의 ID를 얻으려면 0.5만큼 오프셋해야 합니다.

half liquidHeight01_Current = saturate(posRelative_Y / _MaxLiquidHeightOS);    // 输入高度对应的归一化高度
uint3 size;
_MaskTex2DArr.GetDimensions(size.x, size.y, size.z);
half liquidHeight_MulLayerNum = liquidHeight01_Current * size.z;
uint id_Floor = floor(max(0, liquidHeight_MulLayerNum - 0.5)); // 当前区间下界的层ID
uint id_Ceil = id_Floor + 1;

계층적 혼합 보간 계수

—— 위의 id_Ceil에 해당하는 값으로 믹싱 간격의 중간점을 계산하여 시작할 수 있습니다.
—— 그런 다음 이를 기반으로 현재 lerpRange를 더하고 빼면 보간을 위한 상한과 하한이 나옵니다(파란색 선을 초과할 수 없으므로 lerpRange 최대값은 0.5가 되어야 합니다).

half mixH = id_Ceil;      // 현재 혼합 구간의 시작 위치
half lerpRange = liquidLerpRange[id_Ceil];

// 混合分界插值系数扰动
float2 uv_Warp = _LerpWarpTex_ST.xy * uv_Mask + _LerpWarpTex_ST.zw;
half lerpRangeWarpMask = _LerpWarpInt * (tex2D(_LerpWarpTex, uv_Warp).r - 0.5);
half lerp01 = smoothstep(mixH-lerpRange, mixH+lerpRange, liquidHeight_MulLayerNum);
lerp01 = saturate(lerp01 + (1 - abs(lerp01 - 0.5) * 2) * lerpRangeWarpMask);

—— 위의 보간 계수에 몇 가지 퍼트레이션이 추가되었습니다.
——위 코드의 마지막 줄은 중심선의 상단과 하단에 대칭인 보간 계수를 기반으로 감쇠 간격을 계산하여 섭동 마스크의 샘플링 결과를 곱하여 섭동이 원래의 상한과 하한을 초과하지 않도록 합니다.
—— 마지막으로, 계산된 ID에 따라 Tex2DArray와 배열의 해당 요소를 취하고 보간 계수를 사용하여 항목을 보간합니다.

동적 버블

—— 이펙트에 내부 구조가 있다는 착각을 불러일으키기 위해 이중 시차를 간단히 적용한 것입니다. 확장에 대해서는 이전에 작성한 이 글을 참조할 수 있습니다.
 

关于原神冰元素分层错位效果的实现

前言         本文承接上回关于视差算法的原理讲解。这次要实现的效果从原理上讲属于是视差算法的超级低配版,但在伪造冰块、宝石等有一定通透感的材质等方面有着不错的效果。    

www.bilibili.com

—— 이 논문에서는 약간의 최적화를 통해 교차점을 찾기 위한 VS 스테이지의 접선 공간 평면 오프셋은 다음 계산을 사용하여 동일합니다.

half3 tDirWS = TransformObjectToWorldDir(i.tDirOS.xyz);
half3 bDirWS = normalize(cross(o.nDirWS, tDirWS.xyz) * i.tDirOS.w);
half3x3 TBN = half3x3(tDirWS, bDirWS, o.nDirWS);
half3 vDirVS = TransformWorldToTangent(GetCameraPositionWS() - o.posWS, TBN);
o.uv_Mask_Bubble.zw = i.uv - vDirVS.xy * _BubblePlxBias / vDirVS.z;
o.uv_Mask_Bubble.zw = _Bubble_SV.xy * o.uv_Mask_Bubble.zw - _Bubble_SV.zw * _Time.x;

액체 표면 구현

가상 높이 평면(VHP)

—— 이 글에서는 액체 표면에 실제 메시를 사용하는 대신(스텐실로 자르면 작동하겠지만) 메시 레이어를 복사하고 면을 평평하게 뒤집은 다음 액체 모델의 [내부]로 사용했습니다.
—— 이 안쪽 면을 사용하여 뷰 벡터와 함께 가상 평면을 계산합니다. 시선과 가상 높이 평면의 교점을 계산하고, 교차점의 상대 좌표의 xz 성분을 샘플링을 위한 액체 표면의 자외선으로 사용합니다.

—— 이 방법을 사용하면 드로를 두 번 분할할 필요가 없으며, 액체 표면의 렌더링은 [Liquid Side Layering]과 머티리얼과 패스를 공유하고 내부와 외부를 판단하여 UV 계산의 분기 작업을 수행할 수 있습니다(내부와 외부를 버텍스 컬러 등으로 식별할 수 있음).
—— 하지만 이 접근 방식에는 몇 가지 단점이 있습니다:
① 액체 모형의 바닥에 [빈] 구멍이 있으면 마모됩니다(이론적으로는 구멍이 있어도 물을 담을 수 없습니다 - -).

② 깊이 테스트를 수행할 때 잘못 체크됩니다(따라서 후속 투명 믹싱이 불가능합니다).

사용자 지정 깊이 쓰기

—— 맞습니다. 액체가 반투명하게 렌더링되더라도 여기에 ZWrite를 사용해야 합니다.
—— 위의 문제 2를 해결하기 위해 PS에서 SV_Depth 시맨틱을 사용하는 덜 일반적인 작업을 사용하여 ZWrite를 사용자 지정할 수 있습니다.
—— 사용 방법은 간단합니다. PS 함수에 SV_Depth라는 이름의 출력 매개변수를 추가한 다음 PS에서 depthOUT을 변경하면 됩니다.

half4 frag(v2f i, out float depthOUT : SV_Depth) : SV_Target

——一 일반적으로 각 drawCall은 [컬러 버퍼(SV_Target)]와 [깊이 버퍼(SV_Depth)]를 한 번씩 새로 고치고, 우리가 작성한 PS의 반환값은 컬러 버퍼에 브러시된 준비된 픽셀 컬러의 값입니다. 이후 뎁스 테스트 및 템플릿 테스트를 통과하면 원본 버퍼를 덮어쓰는 블렌드 모드가 됩니다.
—— 쓰기 없이 기본적으로 SV_Depth는 SV_Position.z(즉, 하드웨어 원근 분할 후 NDC.z)를 사용합니다[일 1D: 자르기 공간, 래스터에서 (보간, 원근 보정 및 Z파이팅 수학 프로세스의 유도]). 하지만 출력 매개변수에 SV_Depth를 추가하는 한, 값을 수동으로 지정해야 합니다.
—— 이제 월드 좌표를 기준으로 가상 평면의 깊이 값을 반전하여 SV_Depth에 기록해야 합니다. U3D가 정규화된 깊이에서 뷰스페이스의 실제 깊이로 제공하는 LinearEyeDepth를 참조하여 역변환을 수행할 수 있습니다.

// 공간깊이별 정규화깊이
float EyeDepthToLinear01(float eyeDepth)
{
    return (rcp(eyeDepth) - _ZBufferParams.w) / _ZBufferParams.z;
}

—— 그런 다음 세계 좌표에서 시각 공간 좌표 .z를 계산하는 것으로 충분합니다(다음 계산은 행렬 변환의 결과와 동일하지만 하나의 구성 요소에 대해서만 해당).

float eyeDepth = dot(posWS_Plane - GetCameraPositionWS(), -UNITY_MATRIX_V[2].xyz);

Detach&Attach

—— 위의 조합에는 실제로 문제가 있습니다.
—— 액체에 ZWrite가 켜져 있어도 내부에서 틱이 발생하지 않을 수 있으며, 이로 인해 다음과 같은 문제가 발생할 수 있습니다.
 

—— 내부 가상 평면의 계산이 모든 액체가 있는 영역을 완전히 덮는 것처럼 보이는데, 이는 내부를 먼저 렌더링하고 외부를 나중에 렌더링하기 때문에 발생하는 현상입니다.
—— 깊이 테스트는 현재 패스 이후의 그리기에만 영향을 미치기 때문에 (현재 패스가 ZWrite를 켠 경우) 이전에 컬러 버퍼에 쓰여진 결과를 체크할 수 없습니다 (불투명에는 이 문제가 없는데, 투명 블렌딩이 포함되지 않아 그려져도 볼 수 없기 때문이죠). (하지만 이것은 또한 불투명에는 실제로 오버드로-v-가 있다는 것을 의미합니다.)
—— 하지만 현재 시나리오에 따르면 액체의 안쪽과 바깥쪽이 한 패스로 그려지는데, 그려지는 순서를 어떻게 제어할 수 있을까요? 여기서 우리는 동일한 패스에서 서로 다른 삼각형 면이 그려지는 순서를 명확히 해야 합니다. 이는 GPU에서 대부분 병렬로 이루어지지만 결과를 보면 삼각형 얼굴 ID와 관련이 있습니다. ID가 작을수록 더 앞에 렌더링됩니다].
—— 따라서 액체의 바깥쪽이 먼저 렌더링되고 안쪽이 나중에 렌더링되도록(즉, 바깥쪽의 ID가 더 작음) 바깥쪽이 먼저 깊이를 작성하여 깊이 테스트의 안쪽에 작용하도록 하여 액체 레벨 아래의 안쪽이 렌더링되지 않도록 해야 합니다.
—— DCC에서는 모델 얼굴의 일부를 떼어냈다가 다시 채우는 작업을 통해 삼각형의 얼굴 ID가 바뀌고, 먼저 선택된 얼굴이 결합될 때 더 작은 ID를 가지게 되며, Max에서는 분리&붙이기, Maya에서는 [Extract Face] 및 [Combine]이라고 부릅니다.
—— 그러나 또 다른 작업은 호랑이이며 250의 결과를 살펴보면 이전 문제가 여전히 존재하며 다음을 액체 부분으로 덮어야합니다!

—— 다시 생각해보니 앞서 SV_Depth를 변경한 결과 내부 깊이가 모두 가상 평면으로 작성되었습니다. 그래서 여전히 바깥쪽 앞이나 다른 오브젝트 앞을 막고 있는 것은 이해할 수 있습니다...…

Stencil

—— 모든 딥 테스트 문제를 해결하기 위해 파이프라인을 변경하지 않고도 최후의 수단으로 이 방법을 사용할 수 있습니다. 다음 템플릿 테스트 구성을 액체 패스에 추가합니다.

Stencil
{
	Ref 10          // 템플릿 값 기록
    Comp NotEqual   // 템플릿 값 비교 조건
	Pass Replace    // 조건을 비교하는 작업을 통해
    ZFail Replace   // 조건을 비교하되 심도 있는 테스트를 통과하지 못한 경우의 조작
}

—— 좋아요, 완벽합니다. (이런 세트를 사용하면 성능에 대한 부담도 커졌을 겁니다 ---)

GamePlay레이어 로직

아이스 큐브 부력 시뮬레이션

—— U3D에 사용할 수 있는 공식 3D 부력 구성 요소는 아직 없는 것 같습니다. 그러니 직접 제작하세요.
—— 부력 계산 방법 - - 중학교 물리학을 떠올리면 [배수법]이라는 것이 기억나는데, F = ρ_액체 gV_discharge, 그게 다인 것 같습니다!
—— 아이스 큐브는 측면 길이가 1인 정사각형으로 모델링되므로 엔진의 눈금은 실제 측면 길이와 같습니다. 이 측면 길이를 사용하여 y = _LiquidHeightWS_GLB를 사용하여 잠긴 액체의 부피 V를 계산하면 다음과 같이 계산할 수 있습니다.

// 부력 계산
private void IceFakeBuoyancy(Rigidbody rigidbody)
{
    float currentH = rigidbody.transform.position.y;
    float halfSize = 0.5f * iceSize;
    float bottomWS = currentH - halfSize;
    if (bottomWS < _LiquidHeightWS_GLB)
    {
        float V = iceSize * iceSize * Mathf.Min(iceSize, _LiquidHeightWS_GLB - bottomWS);
        Vector3 drag = -rigidbody.velocity * dragScale * V;
        rigidbody.AddForce(-Physics.gravity * V + drag);
    }
}

—— 액체 저항과 같은 것을 시뮬레이션하기 위해 나중에 드래그를 추가했습니다. 추가하지 않으면 다음과 같은 현상이 나타나기 때문입니다(가장 사실적인 물리 엔진이 있습니다)(Q-bomb이고 안정적이지 않습니다).

마스크 배열 동적 수정

—— C#에서 Texture2DArray를 선언하고 레이어(슬라이스) 중 하나에 대해 런타임에 수정할 수 있어야 합니다(따라서 정적 Texture2DArray 유형이 아닌 RenderTexture여야 합니다).
—— 예를 들어 액체를 추가할 때는 이전 Liquid.maskTex를 채워야 하며, 혼합하려면 매 프레임마다 모든 슬라이스에 대해 lerp(slice[n], averageMask, t) 연산이 필요합니다.
—— 런타임에 Texture2DArray를 생성하고 수정하기 위해 다음과 같이 RenderTexture를 선언합니다(지금은 밉 없이). 그렇게 하면 CS에서 해당 밉을 읽고 쓸 수 있습니다), 볼륨 깊이는 현재 최대 액체 레이어 수(즉, 배열의 슬라이스 수)로 지정해야 합니다.

// 표준 생성 Mask2DArr
private static RenderTexture CreateMask2DArr()
{
    RenderTexture outRT = new RenderTexture(
        maskSize, maskSize, 0, 
        RenderTextureFormat.R8, RenderTextureReadWrite.Linear
    );
    outRT.dimension = TextureDimension.Tex2DArray;
    outRT.wrapMode = TextureWrapMode.Repeat;
    outRT.useMipMap = false;
    outRT.enableRandomWrite = true;
    return outRT;
}

// 최대 액체 층 수 설정 (캐시 마스크의 tex2DArray 재작성)
public void ResetLayerNum(int layerNum)
{
    if (layerNum > 0)
    {
        if (buffer_Mask2DArr && layerNum != buffer_Mask2DArr.volumeDepth)
        {
            buffer_Mask2DArr.Release();
        }
        buffer_Mask2DArr = CreateMask2DArr();
        buffer_Mask2DArr.volumeDepth = layerNum;
        mat_Liquid.SetTexture(id_MaskTex2DArr, buffer_Mask2DArr);
    }
}

—— enableRandomWrite는 여기서 true로 설정해야 합니다. 나중에 CS를 사용하여 Texture2DArray의 모든 레이어를 혼합하고 보간할 것이므로 UAV로 만들 예정이기 때문입니다.
—— 결과 측면에서 Graphics.Blit의 Slice로 오버로딩을 구현할 수 있지만 여러 번 실행해야하므로 모든 것을 변경하려면 다음과 같이 직접 CS에 문의하십시오.

CS 평균 마스크

—— 【믹스】를 클릭하는 순간, CS로 averageMask를 계산하여 현재 Texture2Darray의 유효층마다 평균을 구하면, 뒤의 프레임마다 평균을 구하지 않아도 됩니다.
C#

private static readonly ComputeShader cs_Static = Resources.Load<ComputeShader>("CS_LiquidBottle");
private static readonly int kID_AverageMask = cs_Static.FindKernel("AverageMask");
private static readonly int kID_LerpMask = cs_Static.FindKernel("LerpMask");

private static readonly int id_SrcMaskTex2DArr = Shader.PropertyToID("_SrcMaskTex2DArr");
private static readonly int id_DstMaskTex2D = Shader.PropertyToID("_DstMaskTex2D");
private static readonly int id_OutMaskTex2DArr = Shader.PropertyToID("_OutMaskTex2DArr");
private static readonly int id_OutTex2D = Shader.PropertyToID("_OutTex2D"); 
private static readonly int id_LayerNum = Shader.PropertyToID("_LayerNum");
private static readonly int id_Lerp01 = Shader.PropertyToID("_Lerp01");

// 구평균
public static void AverageMask(RenderTexture out2D, RenderTexture src2DArr, int layerNum)
{
    ComputeShader cs = ComputeShader.Instantiate(cs_Static);
    
    uint tSize_X, tSize_Y, tSize_Z;
    cs.GetKernelThreadGroupSizes(kID_AverageMask, out tSize_X, out tSize_Y, out tSize_Z);
    Vector3Int gSize = new Vector3Int(
        Mathf.CeilToInt(out2D.width / (float) tSize_X),
        Mathf.CeilToInt(out2D.height / (float) tSize_Y),
        1
    );
    
    cs.SetInt(id_LayerNum, layerNum);
    cs.SetTexture(kID_AverageMask, id_OutTex2D, out2D);
    cs.SetTexture(kID_AverageMask, id_SrcMaskTex2DArr, src2DArr);
    cs.Dispatch(kID_AverageMask, gSize.x, gSize.y, gSize.z);
    
    ComputeShader.Destroy(cs);
}

CS

Texture2DArray<half> _SrcMaskTex2DArr;
Texture2D<half> _DstMaskTex2D;
RWTexture2DArray<half> _OutMaskTex2DArr;
RWTexture2D<half> _OutTex2D;

// 구평균
uint _LayerNum;
#pragma kernel AverageMask
[numthreads(32, 32, 1)]
void AverageMask(uint2 id : SV_DispatchThreadID)
{
	half temp = 0;
	[loop]
	for (uint i = 0; i < _LayerNum; i++)
	{
		temp += _SrcMaskTex2DArr[uint3(id, i)];
	}
	_OutTex2D[id] = temp / _LayerNum;
}

CS 혼합 마스크

C#

// 보간
public static void LerpMask(RenderTexture out2DArr, RenderTexture src2DArr, Texture dst2D, float lerp01)
{
    ComputeShader cs = ComputeShader.Instantiate(cs_Static);
    
    uint tSize_X, tSize_Y, tSize_Z;
    cs_Static.GetKernelThreadGroupSizes(kID_LerpMask, out tSize_X, out tSize_Y, out tSize_Z);
    Vector3Int gSize = new Vector3Int(
        Mathf.CeilToInt(out2DArr.width / (float) tSize_X),
        Mathf.CeilToInt(out2DArr.height / (float) tSize_Y),
        Mathf.CeilToInt(out2DArr.volumeDepth / (float) tSize_Z)
    );
    
    cs.SetTexture(kID_LerpMask, id_OutMaskTex2DArr, out2DArr);
    cs.SetTexture(kID_LerpMask, id_SrcMaskTex2DArr, src2DArr);
    cs.SetTexture(kID_LerpMask, id_DstMaskTex2D, dst2D);
    cs.SetFloat(id_Lerp01, lerp01);
    cs.Dispatch(kID_LerpMask, gSize.x, gSize.y, gSize.z);
    
    ComputeShader.Destroy(cs);
}

CS

// 插值
half _Lerp01;
#pragma kernel LerpMask
[numthreads(32, 32, 1)]
void LerpMask(uint3 id : SV_DispatchThreadID)
{
	_OutMaskTex2DArr[id] = lerp(_SrcMaskTex2DArr[id], _DstMaskTex2D[id.xy], _Lerp01);
}

—— 마지막으로, 드로잉 속도로 수동으로 이동해야 하는 프로퍼티의 보간 계수를 애니메이션 커브로 노출하여 원하는 대로 할 수 있습니다.


원문
https://zhuanlan.zhihu.com/p/694949392?utm_psn=1769385139132788736

关于星穹铁道调酒效果(分层液体瓶)的复刻尝试

前言——没错,就是这个活动 ——小一个月前活动刚开,看到这种液体分层的效果,觉得很有意思,于是自己搞一手的冲动就来了(正好实践一波之前看到过但没做过的液体瓶效果) ——但走

zhuanlan.zhihu.com