저자 : 일출(破晓) - 인터넷 이름
오랫동안 무언가를 쓰지 않으면서 재활의 생활을 했다. 사실 무엇을 쓸 생각은 없었지만 최근에는 더스트 화이트 플레이에서 필요한 텍스처의 이진화 된 이미지에서 UDF 및 얼굴 SDF 그림자를 생성하는 데 사용되는 SDF 생성 도구에서 Unity를 자연스럽게 잭으로 만들기로 결정했습니다. 얼굴 SDF 섀도우 매핑은 [나는 매일 보지만 개인적인 일과 관심에 의해 제한되어 마침내 만지지 않은 것들을 만지고, 그 느낌을 적는 것은 괜찮습니다.]
역자 주 : UDF : Unsigned Distance Field / SDF : Signed Distance Field 로 보면 됩니다. 궁금하시다면 예전 역자의 번역글 중 아래 내용을 참조.
이론
사이토 알고리즘
유클리드 거리를 수평 및 수직 방향의 값으로 분해한 데서 유래했습니다. 참조:유클리드 거리 변환(EDT) 알고리즘
즉, 첫 번째 단계에서는 각 픽셀 포인트에 대해 수평 방향에서 가장자리와 가장 가까운 거리의 제곱을 계산합니다.
두 번째 단계에서는 각 픽셀에 대해 각 픽셀에서 이 픽셀까지의 수직 방향 거리의 제곱을 계산하여 첫 번째 단계의 값에 더합니다. 이 열의 결과의 최소값은 거리의 제곱입니다.
얼굴 SDF 음영 계산
얼굴 SDF 셰이딩은 미리 그려진 일련의 그림자 맵에 의존하며, 작은 이미지는 정면에서 들어오는 빛에 해당하고 큰 이미지는 측면에서 들어오는 빛에 해당한다고 가정하고, 음영 영역은 하나씩 증가하며 반복되는 영역은 없을 수 있습니다.
이러한 이미지를 더하면 사다리 스타일 이미지가 얻어지고 단면은 대략 다음과 같이 보입니다:
다음 "단계"에서 이 픽셀의 거리와 이전 "단계"에서의 거리는 해당 이미지의 SDF를 사용하여 찾을 수 있으며(SDF를 만들려면 그 중 하나를 반전해야 함), 그라데이션 값도 찾을 수 있습니다.
구현
전처리 이진화
물론 이미지가 깨끗한 가장자리를 갖도록 작업하기 전에 이미지를 이진화해야 합니다.
UDF생성
ComputeShader 코드:
[numthreads(8,8,1)]
void SaitoHorizon(uint3 id : SV_DispatchThreadID)
{
// Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);
float4 color = Source[id.xy];
if (color.r == 0.0)
{
Result[id.xy] = float4(0.0, 0.0, 0.0, 1.0);
return;
}
int dist = 0;
while (dist < width)
{
dist++;
if ((int)id.x + dist < width && Source[uint2(id.x + dist, id.y)].r == 0.0)
break;
if ((int)id.x - dist >= 0 && Source[uint2(id.x - dist, id.y)].r == 0.0)
break;
if ((int) id.x + dist >= width &&
(int) id.x - dist < 0)
break;
}
dist = dist * dist;
Result[id.xy] = float4(dist, dist, dist, 1.0);
}
[numthreads(8,8,1)]
void SaitoVertical(uint3 id : SV_DispatchThreadID)
{
float4 color = Source[id.xy];
if (color.r == 0.0)
{
Result[id.xy] = float4(0.0, 0.0, 0.0, 1.0);
return;
}
float minDist = Source[uint2(id.x, 0)].r + (float)(id.y * id.y);
for (int i = 1; i < height; i++)
{
float a = Source[uint2(id.x, i)].r;
float b = i - (int)id.y;
minDist = min(minDist, a + b * b);
}
float sdf = sqrt(minDist) * pixelSize;
Result[id.xy] = float4(sdf, sdf, sdf, 1.0);
}
호출 메서드:
public void GenerateUDF(ref RenderTexture sdf, int width, int height, int pixelCountToArriveOne)
{
RenderTexture result = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGBFloat);
result.enableRandomWrite = true;
result.useMipMap = false;
result.Create();
uint groupX, groupY, groupZ;
SDFUtils.SDFCompute.GetKernelThreadGroupSizes(0, out groupX, out groupY, out groupZ);
SDFUtils.SDFCompute.SetTexture(0, "Source", sdf);
SDFUtils.SDFCompute.SetTexture(0, "Result", result);
SDFUtils.SDFCompute.SetInt("width", width);
SDFUtils.SDFCompute.SetInt("height", height);
SDFUtils.SDFCompute.SetFloat("pixelSize", 1.0f / (float)pixelCountToArriveOne);
SDFUtils.SDFCompute.Dispatch(0, width / (int)groupX, height / (int)groupY, 1);
SDFUtils.SDFCompute.SetTexture(1, "Source", result);
SDFUtils.SDFCompute.SetTexture(1, "Result", sdf);
SDFUtils.SDFCompute.Dispatch(1, width / (int)groupX, height / (int)groupY, 1);
RenderTexture.ReleaseTemporary(result);
}
여기서 픽셀 카운트 투 어라이브 원은 이미지에서 거리 1이 몇 픽셀에 해당하는지를 나타내며, exr 형식이 아닌 이미지를 사용하는 경우 그라데이션 거리를 나타내는 데 사용할 수 있습니다.
얼굴 그림자 그리기
저는 서브스턴스 페인터를 사용하여 페인팅합니다. 모델을 로드한 후 [텍스처 세트 설정]으로 이동하여 채널은 하나만 남기고 깨끗하게 삭제합니다. 여기서는 BaseColor를 사용하거나 사용자 정의 채널 User0 또는 이와 유사한 것을 사용할 수 있습니다.
그런 다음 자료 보기에서 보기 모드를 해당 채널로 전환합니다.
3D 창에서 보이는 브러시 영역과 UV의 브러시 영역이 반드시 동일하지는 않으므로(예: 아래 이미지에서 그려진 획의 끝은 3D 창에서 볼 때는 폭이 같지만 UV 관점에서 볼 때는 두껍습니다) 그리기 전에 브러시의 보정, 간격 및 디더링을 신중하게 고려해야 합니다.
마지막으로 나중에 그리는 그림자가 항상 앞의 그림자 위로 바뀌도록 하려면 한 레이어씩 진행하면서 서서히 그리는 것이 가장 좋습니다:
얼굴 SDF 맵 생성
컴퓨트쉐이더 코드:
[numthreads(8,8,1)]
void Interpolation(uint3 id : SV_DispatchThreadID)
{
float dist0 = Shadow0[id.xy].x;
float dist1 = Shadow1[id.xy].x;
float r = dist0 / (dist0 + dist1);
Result[id.xy] = float4(r, r, r, 1);
}
[numthreads(8, 8, 1)]
void Sum(uint3 id : SV_DispatchThreadID)
{
float dist = Source[id.xy].x;
float r = dist / sumTime;
Result[id.xy] += float4(r, r, r, 1);
}
调用方法:
// 逐个读入文件
int sumTime = shadowPathList.Count - 1;
List<RenderTexture> sdfList = new List<RenderTexture>();
for (int index = 0; index < shadowPathList.Count - 1; index++)
{
Texture2D shadow0 = new Texture2D(2, 2);
shadow0.LoadImage(System.IO.File.ReadAllBytes(shadowPathList[index]));
shadow0.Apply();
Texture2D shadow1 = new Texture2D(2, 2);
shadow1.LoadImage(System.IO.File.ReadAllBytes(shadowPathList[index + 1]));
shadow1.Apply();
// 注意,其中一个阴影贴图在预处理时必须反向
RenderTexture rt0 = sdfGenerator.PreprocessImage(shadow0, channel, 0.5f, false, sdfWidth, sdfHeight, RenderTextureFormat.ARGBFloat);
RenderTexture rt1 = sdfGenerator.PreprocessImage(shadow1, channel, 0.5f, true, sdfWidth, sdfHeight, RenderTextureFormat.ARGBFloat);
var sdfBetweenTwo = sdfGenerator.GenerateFaceSDFBetweenTwo(ref rt0, ref rt1, sdfWidth, sdfHeight, 0, 0);
sdfList.Add(sdfBetweenTwo);
}
originalSdfTexture = sdfGenerator.SumFaceSDF(ref sdfList);
// 先在相邻两个阴影贴图里渐变
public RenderTexture GenerateFaceSDFBetweenTwo(ref RenderTexture shadow0, ref RenderTexture shadow1, int width, int height, int lower, int upper)
{
GenerateUDF(ref shadow0, width, height, 1);
GenerateUDF(ref shadow1, width, height, 1);
RenderTexture result = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGBFloat);
result.enableRandomWrite = true;
result.useMipMap = false;
result.Create();
uint groupX, groupY, groupZ;
SDFUtils.SDFCompute.GetKernelThreadGroupSizes(2, out groupX, out groupY, out groupZ);
SDFUtils.SDFCompute.SetTexture(2, "Shadow0", shadow0);
SDFUtils.SDFCompute.SetTexture(2, "Shadow1", shadow1);
SDFUtils.SDFCompute.SetTexture(2, "Result", result);
SDFUtils.SDFCompute.SetInt("width", width);
SDFUtils.SDFCompute.SetInt("height", height);
SDFUtils.SDFCompute.SetInt("lower", lower);
SDFUtils.SDFCompute.SetInt("upper", upper);
SDFUtils.SDFCompute.Dispatch(2, width / (int)groupX, height / (int)groupY, 1);
return result;
}
// 把渐变结果累加起来
public RenderTexture SumFaceSDF(ref List<RenderTexture> sdfList)
{
RenderTexture result = RenderTexture.GetTemporary(sdfList[0].width, sdfList[0].height, 0, RenderTextureFormat.ARGBFloat);
result.enableRandomWrite = true;
result.useMipMap = false;
result.Create();
uint groupX, groupY, groupZ;
SDFUtils.SDFCompute.GetKernelThreadGroupSizes(3, out groupX, out groupY, out groupZ);
for (int i = 0; i < sdfList.Count; i++)
{
string path = "D:/" + i + ".exr";
Texture2D tex = SDFUtils.GetTexture2DFromRenderTexture(sdfList[i]);
byte[] bytes = tex.EncodeToEXR();
System.IO.File.WriteAllBytes(path, bytes);
SDFUtils.SDFCompute.SetTexture(3, "Source", sdfList[i]);
SDFUtils.SDFCompute.SetTexture(3, "Result", result);
SDFUtils.SDFCompute.SetInt("sumTime", sdfList.Count);
SDFUtils.SDFCompute.Dispatch(3, sdfList[i].width / (int)groupX, sdfList[i].height / (int)groupY, 1);
}
return result;
}
효과 및 주소
UDF 생성
얼굴 SDF 효과
최종 결과물:
섀도우 매핑의 두 프레임마다 계산되는 그라데이션 맵의 조합으로 구성됩니다:
여기에서는 얼굴의 반쪽(즉, 광원이 90° 회전) 총 6개의 섀도우 맵을 테스트적으로 만들었을 뿐이며 실제 생산은 180°로 완료되어야 하며 수량은 이렇게 적을 수 없습니다.
프로젝트 주소
제가 글을 쓰는 대신 깃허브 코파일럿이 작성하는 주석을 쓰고, C#을 작성하는 데 정말 오랜 시간이 걸리고, 무의식적으로 디프를 작성하여 혼자서 자소 할 뻔 했습니다.
원문
https://zhuanlan.zhihu.com/p/705226178?utm_psn=1788900461492973570&utm_id=0
'TECH.ART.FLOW.IO' 카테고리의 다른 글
[X][공유] 파이선으로 만든 Face SDF 생성기. (0) | 2024.06.28 |
---|---|
[번역] 플루이드 플럭스 2.0 근해 파도 (0) | 2024.06.28 |
[번역] 【UE5.4】Slate Post-buffer를 사용해보자. (0) | 2024.06.25 |
[번역] 언리얼 엔진 RDG 소스 코드 분석 (0) | 2024.06.10 |
[번역]언리얼 엔진 초실감 인간을 해부하는 렌더링 기술 Part 2 - 눈동자 렌더링 (1) | 2024.06.10 |