본문 바로가기

Unreal Engine

언리얼 엔진에서는 노멀맵을 어떻게 구분할까?

Normal 텍스처의 Compression Setting

언리얼에서 텍스처 임포트 하다 보면, 노멀 텍스처일 경우 알아서 Compression setting이 노멀 맵 압축 셋팅으로 잡히는 것을 볼 수 있다.

BC5 압축 포맷은 R 채널과 G 채널을 각각 8비트 채널에 저장하고, 나머지 B채널과 A채널 내용을 날린다. 따라서 파일 용량이 압축되기 전보다는 줄어들게 된다. 사실상 노멀 맵이 아니면 쓸 일이 없는 압축 방식이다.

BC5를 사용할 수 없을 경우에는 fallback으로 DXT5 방식을 를 사용한다.

이후 B채널 내용은 R채널과 G채널 내용을 기반으로 계산해 채워지게 된다.

임포트 된 텍스처가 Normal 텍스처인지 엔진이 어떻게 알까?

처음에는 파일 명 규칙을 보고 판별하는 줄로만 알았는데, 엔진 코드를 살펴보니 보다 심오한 내용이 숨어 있었다.

EditorFactories.cpp

EditorFactories 에서는 텍스처를 비롯한 여러 에셋을 엔진으로 가지고 올 때의 해석과 처리 내용이 들어 있는 것 같다.

그 중 텍스처 부분만 살펴보면, 이 부분에 앞서 ImportTexture() 함수 안의 ImportImage() 함수에서 이미지의 확장자와 BitDepth 를 살펴보고, Compression Setting이 Grayscale 또는 HDR 인지 먼저 판별해 준다. 노멀 맵인지 아닌지는 그 다음에 체크하게 된다.

ImportImage()

 

NormalmapIdentification.cpp

실제 텍스처가 노멀맵인지 판별하는 코드는 여기에 있다.

/**
	 * DoesTextureLookLikelyToBeANormalMap
	 *
	 * Makes a best guess as to whether a texture represents a normal map or not.
	 * Will not be 100% accurate, but aims to be as good as it can without usage
	 * information or relying on naming conventions.
	 *
	 * The heuristic takes samples in small blocks across the texture (if the texture
	 * is large enough). The assumption is that if the texture represents a normal map
	 * then the average direction of the resulting vector should be somewhere near {0,0,1}.
	 * It samples in a number of blocks spread out to decrease the chance of hitting a
	 * single unused/blank area of texture, which could happen depending on uv layout.
	 *
	 * Any pixels that are black, mid-gray or have a red or green value resulting in X or Y
	 * being -1 or +1 are ignored on the grounds that they are invalid values. Artists
	 * sometimes fill the unused areas of normal maps with color being the {0,0,1} vector,
	 * but that cannot be relied on - those areas are often black or gray instead.
	 *
	 * If the heuristic manages to sample enough valid pixels, the threshold being based
	 * on the total number of samples it will be looking at, then it takes the average
	 * vector of all the sampled pixels and checks to see if the length and direction are
	 * within a specific tolerance. See the namespace at the top of the file for tolerance
	 * value specifications. If the vector satisfies those tolerances then the texture is
	 * considered to be a normal map.
	 */
	bool DoesTextureLookLikelyToBeANormalMap( UTexture* Texture )
	{
		int32 TextureSizeX = Texture->Source.GetSizeX();
		int32 TextureSizeY = Texture->Source.GetSizeY();

		// Calculate the number of tiles in each axis, but limit the number
		// we interact with to a maximum of 16 tiles (4x4)
		int32 NumTilesX = FMath::Min( TextureSizeX / SampleTileEdgeLength, MaxTilesPerAxis );
		int32 NumTilesY = FMath::Min( TextureSizeY / SampleTileEdgeLength, MaxTilesPerAxis );

		Sampler.SetSourceTexture( Texture );

		if (( NumTilesX > 0 ) &&
			( NumTilesY > 0 ))
		{
			// If texture is large enough then take samples spread out across the image
			NumSamplesThreshold = (NumTilesX * NumTilesY) * 4; // on average 4 samples per tile need to be valid...

			for ( int32 TileY = 0; TileY < NumTilesY; TileY++ )
			{
				int Top = (TextureSizeY / NumTilesY) * TileY;

				for ( int32 TileX = 0; TileX < NumTilesX; TileX++ )
				{
					int Left = (TextureSizeX / NumTilesX) * TileX;

					EvaluateSubBlock( Left, Top, SampleTileEdgeLength, SampleTileEdgeLength );
				}
			}
		}
		else
		{
			NumSamplesThreshold = (TextureSizeX * TextureSizeY) / 4;

			// Texture is small enough to sample all texels
			EvaluateSubBlock( 0, 0, TextureSizeX, TextureSizeY );
		}

		// if we managed to take a reasonable number of samples then we can evaluate the result
		if ( NumSamplesTaken >= NumSamplesThreshold )
		{
			const float RejectedToTakenRatio = static_cast<float>(NumSamplesRejected) / static_cast<float>(NumSamplesTaken);
			if ( RejectedToTakenRatio >= RejectedToTakenRatioThreshold )
			{
				// Too many invalid samples, probably not a normal map
				return false;
			}

			AverageColor /= (float)NumSamplesTaken;

			// See if the resulting vector lies anywhere near the {0,0,1} vector
			float Vx = Sampler.ScaleAndBiasComponent( AverageColor.R );
			float Vy = Sampler.ScaleAndBiasComponent( AverageColor.G );
			float Vz = Sampler.ScaleAndBiasComponent( AverageColor.B );

			float Magnitude = FMath::Sqrt( Vx*Vx + Vy*Vy + Vz*Vz );

			// The normalized value of the Z component tells us how close to {0,0,1} the average vector is
			float NormalizedZ = Vz / Magnitude;

			// if the average vector is longer than or equal to the min length, shorter than the max length
			// and the normalized Z value means that the vector is close enough to {0,0,1} then we consider
			// this a normal map
			return ((Magnitude >= NormalMapMinLengthConfidenceThreshold) &&
					(Magnitude < NormalMapMaxLengthConfidenceThreshold) &&
					(NormalizedZ >= NormalMapDeviationThreshold));
		}

		// Not enough samples, don't trust the result at all
		return false;
	}

설명에 따르면, 이미지를 몇 개의 블럭으로 나눈 후, 각 블럭의 RGB 값이 0, 0, 1에 가까운 경우가 많으면 노멀 맵이라고 추정하는 것 같다.

UV 레이아웃에 따라 사용하지 않는 구역이 있는 경우가 있으므로, 이 구역을 샘플링하는 경우가 없도록 이미지의 여러 영역에 퍼지게끔 배치된다. 이때, 완전히 블랙, 중간 회색이거나(0.5, 0.5, 0.5를 뜻하는 건가?) R 또는 G 가 -1 또는 +1 이라면 유효하지 않은 값으로 보고 무시한다.

샘플링된 픽셀들의 평균 벡터값을 구하고, threshold를 만족할 경우 노멀 맵으로 취급한다.

소스 포맷이 아래와 같을 경우에만 위 판별 알고리즘으로 노멀맵인지 검사하게 된다.

결론

주석에도 쓰여 있지만, 100퍼센트 정확한 판별법이 아니기 때문에, 이미지 전체적으로 RGB값이 0, 0, 1에 가까운 값이 많은 경우에는 종종 노멀맵으로 인식되어버리는 경우가 있다. 이때는 수동으로 압축 방식을 바꿔주면 된다.

정상적으로 만들어진 PBR 기반 텍스처라면 거의 생기지 않을 경우이지만, 스타일라이즈드 된 프로젝트에서는 종종 나올 수 있는 경우일 것 같다. 실제로 파란 계열 픽셀이 대부분인 텍스처를 베이스 컬러로 임포트 해서 사용할 때, 뭔가 이상하다는 제보가 들어오는 경우가 몇번 있었는데, 이 문제였던 경우에는 텍스처 압축 셋팅을 Default로 변경하여 간단히 해결하였다.

 

참고 링크 : https://www.techarthub.com/your-guide-to-texture-compression-in-unreal-engine/