[GTL] Epic (07/20) - Effector Parameterizing 2

brights2ella·2025년 8월 2일
0

정글게임테크랩1기

목록 보기
13/14

Ray Tracing 과정을 GPU로 옮기고 나며 더 많은 Ray 갯수를 계산할 수 있게 되면서 파라미터 추출 과정에도 큰 부하가 걸리게 되었다. 때문에 덜 자연스럽게 들리더라도 간소화할 방법이 필요했다.

스레드 분리

우선 게임 스레드에서 진행되던 파라미터 추출 과정을 별도의 스레드에서 진행하기로 하였다. 여기서 사용하는 데이터는 Game 스레드에서 Audio 스레드로, Audio 스레드에서 Audio Mixer 스레드로 이동한다.

위 사진에서 Ray Tracing 결과로 반환된 Traced Data가 Audio 스레드로 전달되고 여기서 바로 파라미터 추출이 일어나던 것을, Parameterize 스레드를 별도로 두어 진행하게 되었다.
이 스레드를 통해 Parameter 데이터가 생성되고 그대로 Audio Mixer 스레드로 전달되며 오디오 신호로 변환된다.

좀 더 구체적인 흐름은 아래와 같다.

직접 만든 Subsystem의 PollRenderThreadResults()를 통해 Render Thread에서 Ray Tracing 결과를 받아온다. 이 함수 내부의 콜백으로 받아온 데이터를 Source Data Override로 전달하고, 전달된 데이터는 다시 다른 스레드로 전달되기 위해 Packet으로 감싸져 큐에 들어간다.

Audio Thread에서, Source Data Override의 GetSourceDataOverride(...)가 호출되면 Packet을 Dequeue하여 Parameterize Packet으로 감싸져 Parameterize Thread로 전달된다.

별도로 생성한 Parameterize Thread는 FRunnable로 실행되며 내부에서 다시 ParallelFor(...)로 병렬로 파라미터 추출이 이루어진다. 위 그림의 Algorithm::Process()가 파라미터 추출을 실행하는 함수이다.
이 결과는 다시 Audio Thread로 전달되기 위해 큐에 들어간다.

다시 GetSourceDataOverride(...)에서 추출된 파라미터들을 큐에서 뽑아 Spatializer와 Reverb로 전달하게 된다.

함수 하나에서 입력과 출력을 둘 다 받아 처리하고 있는 게 맘에 안 들긴 하지만... 이렇게 스레드를 분리하여, 게임 스레드의 프레임 드랍이 일어나지 않을 뿐만 아니라 파라미터 추출 실행 주기도 설정할 수 있게 되었다.

새 파라미터 추출 과정

1. Devide Phase

RayTracing 방식이 Russian Roulette으로 바뀌었다.

Russian Roulette 기법이란, Ray가 벽에 부딪혔을 때 흡수율의 확률만큼 Ray가 반사되지 않고 사라지게 만드는 기법을 말한다. 그러나 이렇게만 하면 이미지가 전체적으로 어두워지기 때문에 (사운드라면 전체적으로 볼륨이 작아지기 때문에) 살아남은 Ray의 에너지를 보상한다.

덕분에 RayTracing 속도는 빨라졌지만 Traced Data의 분포가 조금 달라졌다. 아래 산점도처럼 Delay가 커질수록, 점들이 모여있는 정도는 줄어들지만 각 점의 Volume 값이 커지게 된다.

다행히 Volume을 더한 전체적인 분포는 크게 달라지지 않는다.

이 산점도를 Weight Histogram으로 그렸을 때의 분포가 y=aty={a\over t}를 따른다고 가정한다.
(실제로는 y=atk,2k1y=at^{k},-2 \le k \le -1 로 추정된다)

이 모델을 기준으로 초기반사음 구간과 잔향 구간을 임의로 나눈다. 처음으로 들어오는 Ray Data의 볼륨이 1이라 가정하고, 이에 대응하는 Delay 값을 통해 aa를 구할 수 있다.

Model:y=ata=y0t0\text{Model:} \quad y = {a\over t} \\ a = y_0 \cdot t_0
// PredictModel: Volume = Coef / Delay, Coef = DirectRayVolume * DirectRayDelay
// Model Predict Histogram Weighted by Volume, bin = 1.f / RayCount
// Model follows Calculating Reflection Volume Formular
const float FirstRayVolume = 1.f;   // assumed
const float FirstRayDelay = AvailableTracedDataArray[0].Distance / InSettings.SoundSpeed;
float ModelCoef = FirstRayVolume * FirstRayDelay;   // by Model

그리고 이 식으로 특정 볼륨이 되는 시점을 예측한다.

// EarlyReflection / Reverb Threshold is assumed by ratio with Volume of Direct Sound (1.f)
const float EREndVolumedB = -20.f;
const float EREndThreshold = FATUtils::DecibelToRatio(EREndVolumedB);
const float EREnd = ModelCoef / EREndThreshold; // by Model

const float ReverbStartVolumedB = -5.f;
const float ReverbStartThreshold = FATUtils::DecibelToRatio(ReverbStartVolumedB);
const float ReverbStart = ModelCoef / ReverbStartThreshold; // by Model

const float ReverbEndVolumedB = -25.f;
const float ReverbEndThreshold = FATUtils::DecibelToRatio(ReverbEndVolumedB);
const float ReverbEnd = FMath::Min(ModelCoef / ReverbEndThreshold, InSettings.MaxTraceTime); // by Model

이 시점들로 구간을 나눈다.

2. Greedy Clustering

Delay 기준 오름차순으로 정렬된 Ray Data들을 순회하면서, 가까운 각도에 있는 것들을 군집화한다. 군집화 방식은 단순하게, 군집 중 첫 번째 각도와의 차이를 threshold와 비교하여 군집에 포함시킬지 말지 결정한다.

Delay를 반지름으로 하여 각 방향에 표시하면 방사형처럼 그려질 텐데, 아래처럼 표시할 수 있을 것이다.

이 중 가장 가까운 Ray Data를 첫 군집의 방향으로 삼는다.

다음으로 가까운 Ray Data를 보고, 가장 가까운 방향의 군집을 찾는다. 만약 각도 차이가 threshold보다 작다면 그 군집에 포함시킨다.

만약 threshold보다 크다면 새로운 군집으로 만든다.

이를 반복하여 모든 Ray를 군집으로 분류한다.
threshold가 15도로 설정했을 때 군집은 약 110개 정도가 발생하게 된다.

이렇게 형성한 군집별로, 직접음이 도달하는 시점부터 EREnd까지의 Ray를 수집하여 Weight Histogram을 그린다.
그리고 가장 높은 볼륨 구간 N개를 뽑아 파라미터로 추출한다. 이때 N개는 설정에서 정할 수 있도록 하였다.

// Processing Early Reflections
TArray<FEarlyReflectionInfo> EarlyReflectionInfos;
{
	// construct histogram by clusters
    const float EarlyReflectionVolumeThreshold = FATUtils::DecibelToRatio(-20.f);
    for ( FClusterInfo& ClusterInfo : Clusters )
    {
        TArray<float> VolumeHist;
        VolumeHist.AddZeroed((ReverbStart - FirstRayDelay) / TimeBin + 1);

        for (int32 i = 0; i < ClusterInfo.EarlyReflectionDelays.Num(); ++i)
        {
            int32 HistIndex = (ClusterInfo.EarlyReflectionDelays[i] - FirstRayDelay) / TimeBin;
            if (HistIndex < 0 || VolumeHist.Num() <= HistIndex)
                continue;
            VolumeHist[HistIndex] += ClusterInfo.EarlyReflectionVolumes[i];
        }
        
        // pick high volume section
        for (int32 i = 0; i < VolumeHist.Num(); ++i)
        {
            if (VolumeHist[i] > EarlyReflectionVolumeThreshold)
            {
                FEarlyReflectionInfo EarlyReflectionInfo;
                EarlyReflectionInfo.Delay = FirstRayDelay + i * TimeBin;
                EarlyReflectionInfo.Volume = VolumeHist[i];
                EarlyReflectionInfo.DirIdx = ClusterInfo.FirstRayDirectionIdx;
                EarlyReflectionInfos.Add(EarlyReflectionInfo);
            }
        }
    }

	// select highest volume sections
    Algo::Sort(EarlyReflectionInfos, [](const FEarlyReflectionInfo& LHS, const FEarlyReflectionInfo& RHS)
    {
        return LHS.Volume > RHS.Volume;
    });
    
    const int32 MaxNumEarlyReflection = InSettings.MaxEarlyReflectionCount;
    if (EarlyReflectionInfos.Num() > MaxNumEarlyReflection )
    {
        EarlyReflectionInfos.RemoveAt(MaxNumEarlyReflection, EarlyReflectionInfos.Num() - MaxNumEarlyReflection);
    }
    
    // normalize volume
    const float TargetEarlyReflectionVolume = 1.f;
    float EarlyReflectionVolumeSum = 0.f;
    for (const FEarlyReflectionInfo& EarlyReflectionInfo :EarlyReflectionInfos)
    {
        EarlyReflectionVolumeSum += EarlyReflectionInfo.Volume;
    }
        
    if (EarlyReflectionVolumeSum >= TargetEarlyReflectionVolume )
    {
        for ( FEarlyReflectionInfo& EarlyReflectionInfo : EarlyReflectionInfos )
        {
            EarlyReflectionInfo.Volume /= EarlyReflectionVolumeSum;
            EarlyReflectionInfo.Volume *= TargetEarlyReflectionVolume;
        }
    }
}

3. Reverb

Reverb는 기존처럼 Energy Decay Curve를 활용한다.

결과

기존 방식으로 스트레스 테스트를 했을 때 파라미터 과정만 50ms 넘게 나오던 것을 볼 수 있다.

개선 후에는 10ms 안쪽으로 줄어들었다. 엄밀한 측정은 아니지만 유의미한 감소를 확인할 수 있다.

소리 질은 크게 다르진 않은 듯하다.

0개의 댓글