bink video라는 것이 있었습니다.

제가 속해있는 프로젝트가 워낙 고대 때부터 개발해 왔었던지라 게임에 쓰이는 영상을 Bink Video 라이브러리를 사용하고 있습니다. Bink Video는 영상에 알파 채널을 포함 시킬수 있어서 게임 UI와 혼합 사용하여 다양한 효과를 낼수 있죠. 이 덕분에 가챠폰을 깔때 특수 효과라든가, 게임 승리 효과 UI등을 다이나믹 하게 표현할수 있습니다. 그런데 문제는 이 라이브러리가 워낙 고대의 유물이다 보니 압축률이 낮아 영상의 퀄리티가 높거나 해상도가 높으면 용량이 기하급수적으로 늘어나는 단점이 있습니다. 영상 용량이 늘어난다는 것은 패치 용량이 늘어난다는 말이고 그 것은 곧 CDN 비용 상승으로 이어집니다.

webm을 찾다

bink video를 대체할 포맷이 없을까 찾아보았습니다. 찾아보니 bink video도 bink hdr 이라는 후속 라이브러리도 나와있는 상태였습니다. 그러나 비용 문제가 있어, 우선 순위에서는 밀어두었습니다. 요즘 영상 주력 포맷인 h.264도 알아봤지만 이는 라이센스 관련 문제로 활용하기 힘들고, 무엇보다 알파 채널을 활용할수가 없어 기존에 사용하던 bink video를 대체할수가 없었습니다.


그러다 구글이 지원하고 있다는 Webm Project를 발견했습니다. 라이센스도 BSD 라이센스로 활용도 자유롭고, 무엇보다 알파 채널을 지원하는 것이 딱 적합했습니다. bink로 제작했던 영상 파일을 webm으로 인코딩을 해보니 VP8 코덱의 고효율 압축률 덕분에 bink로 제작했던 30여메가 용량의 영상이 1-2메가의 용량으로 줄어드는 극적인 효과를 내주었습니다.


webm에서 알파 지원다하며?

하지만 webm을 사용하기에 전혀 문제가 없는 것이 아니었습니다. 첫 번째 문제가 webm의 알파 채널 지원이 기본 기능이 아니었다는 것입니다. 무슨 말인고 하니 webm project에서 제공 하는 libvpx / libwebm 라이브러리에서는 알파 채널을 디코딩하는 기능이 없었습니다. 즉, 그냥 사용해서는 영상 내에 있는 알파 채널 정보를 얻어올수 없다는 것이죠.


일단, 사양 명세를 살펴보았습니다. webm development wiki를 살펴 보니 알파 채널 인코딩/디코딩에 관한 명세가 있었습니다. 이를 기반으로 libvpx / libwebm 소스를 분석해보니 인코딩 관련 코드를 확인할수 있었습니다. 반대로 디코딩에 관한 코드는 왜인지 없었습니다. 포럼을 검색해 봐도 알파 채널 디코딩을 공식적으로는 지원하지 않고 있다는 답변만이 있었습니다.




구글링을 해보면 알파 채널이 포함된 webm을 재생할 수 있는 크롬에 대한 관련 개발 글이 있는데, 이 글을 봐도 크롬 내에서 해당 기능을 자체적으로 개발했다는 이야기가 있습니다.


결국 해당 기능을 만들어야만 하는 것이었습니다.


만들어야지 어쩌겠어

먼저 libwebm에서 webm 인코딩시 알파 정보를 어떻게 저장하는지 분석을 하였습니다. webm은 Matroka 컨테이너 포맷을 이용해 정보를 저장합니다. cluster 안에 block group이 있고, 각 block에 frame 정보가 포함되어 있습니다. libwebm 소스를 보면 인코딩 할 때, 알파 채널 정보가 존재하는 경우 mkvmuxerutil를 이용해 block에 추가 정보(block additional)를 저장하고 있는 것을 확인할 수 있습니다.

if (frame->additional())
{
	if (!WriteEbmlMasterElement(writer, libwebm::kMkvBlockAdditions,
		block_additions_payload_size))
	{
		return 0;
	}

	if (!WriteEbmlMasterElement(writer, libwebm::kMkvBlockMore,
		block_more_payload_size))
		return 0;

	if (!WriteEbmlElement(writer, libwebm::kMkvBlockAddID,
		static_cast<uint64>(frame->add_id())))
		return 0;

	if (!WriteEbmlElement(writer, libwebm::kMkvBlockAdditional,
		frame->additional(), frame->additional_length()))
	{
		return 0;
	}
}

어떻게 저장하는지 알았으니 디코딩 할 때, 이걸 읽어내면 되겠습니다. mkvparser에서 block을 읽을 때, libwebm::kMkvBlockAdditions가 있으면 위의 정보를 추가적으로 읽게 수정해줍니다. 그리고 block에 추가 정보의 frame 위치와 크기를 저장하도록 수정해줍니다. 그렇게 수정 후 libwebm의 webm_info를 통해 해당 정보를 제대로 파싱하고 있는지 확인해줍니다.



frame_addition 위치와 크기가 제대로 표시되는군요.


YUVA to RGBA

알파 채널 디코딩 문제는 해결했고, 본격적으로 게임에서 webm 영상을 재생해보려고 합니다. 그런데, webm 영상은 RGB가 아닌 YUV 포맷으로 이미지를 저장합니다. 게임에 이미지를 띄어보기 위해서는 이를 RGB로 변환해주어야 하죠. YUV와 RGB간의 변환 샘플은 구글링하면 많이 나옵니다. 이를 참고하여, 영상 이미지를 RGBA로 변환 후 OpenGL에서 텍스쳐 생성 후 띄어봤습니다. 그런데 너무 느립니다. 픽셀 하나하나를 일일히 변환 후에, 텍스쳐를 생성하여 띄우다 보니 영상 해상도가 크면 클수록 성능이 기하급수적으로 떨어지는 것입니다.
// 기존 변환 코드. 모든 픽셀을 일일히 변환 하고 있다.
for (unsigned long int i = 0; i < height; ++i)
{
	for (unsigned long int j = 0; j < width; ++j)
	{
		int t_y = y[((i * strideY) + j)];
		int t_u = u[(((i >> 1) * strideU) + (j >> 1))];
		int t_v = v[(((i >> 1) * strideV) + (j >> 1))];
		int t_a = a[((i * strideA) + j)];
		t_y = t_y < 16 ? 16 : t_y;

		int r = (298 * (t_y - 16) + 409 * (t_v - 128) + 128) >> 8;
		int g = (298 * (t_y - 16) - 100 * (t_u - 128) - 208 * (t_v - 128) + 128) >> 8;
		int b = (298 * (t_y - 16) + 516 * (t_u - 128) + 128) >> 8;

		mCTX.pixels[pixels++] = r > 255 ? 255 : r < 0 ? 0 : r;
		mCTX.pixels[pixels++] = g > 255 ? 255 : g < 0 ? 0 : g;
		mCTX.pixels[pixels++] = b > 255 ? 255 : b < 0 ? 0 : b;
		mCTX.pixels[pixels++] = t_a;
	}
}

최적화가 필요합니다. 구글링을 좀 해보니 어느 능력자분께서 yuv to rgb 변환에 SIMD 명령어 셋을 이용하여 최적화를 해놓은 프로젝트가 있더군요. 한번의 루프에 64개의 픽셀 정보를 변환할수 있어, 이전 보다 수 배 빠른 변환 속도를 보여주었습니다. 다만, 이 프로젝트도 알파 정보가 포함되지 않은 YUV to RGB 변환이라 약간의 코드 수정을 해주어야 했습니다.
for (uint32_t h = 0; h < (height - 1); h += 2)
{
	const uint8_t *y_ptr1 = Y + h * Y_stride;
	const uint8_t *y_ptr2 = Y + (h + 1) * Y_stride;
	const uint8_t *u_ptr = U + (h / 2) * U_stride;
	const uint8_t *v_ptr = V + (h / 2) * V_stride;
	const uint8_t *a_ptr1 = (A) ? A + h * A_stride : nullptr;
	const uint8_t *a_ptr2 = (A) ? A + (h + 1) * A_stride : nullptr;

	uint8_t *rgba_ptr1 = RGBA + (h * RGBA_stride);
	uint8_t *rgba_ptr2 = RGBA + ((h + 1) * RGBA_stride);

	for (uint32_t w = 0; w < (width - 31); w += 32)
	{
		__m128i u = LOAD_SI128((const __m128i*)(u_ptr));
		__m128i v = LOAD_SI128((const __m128i*)(v_ptr));

		__m128i r_tmp, g_tmp, b_tmp;
		__m128i r_16_1, g_16_1, b_16_1, r_16_2, g_16_2, b_16_2;
		__m128i r_uv_16_1, g_uv_16_1, b_uv_16_1, r_uv_16_2, g_uv_16_2, b_uv_16_2;
		__m128i y_16_1, y_16_2;

		u = _mm_add_epi8(u, _mm_set1_epi8(-128));
		v = _mm_add_epi8(v, _mm_set1_epi8(-128));

		/* process first 16 pixels of first line */
		__m128i u_16 = _mm_srai_epi16(_mm_unpacklo_epi8(u, u), 8);
		__m128i v_16 = _mm_srai_epi16(_mm_unpacklo_epi8(v, v), 8);

		UV2RGB_16(u_16, v_16, r_uv_16_1, g_uv_16_1, b_uv_16_1, r_uv_16_2, g_uv_16_2, b_uv_16_2)
			r_16_1 = r_uv_16_1; g_16_1 = g_uv_16_1; b_16_1 = b_uv_16_1;
		r_16_2 = r_uv_16_2; g_16_2 = g_uv_16_2; b_16_2 = b_uv_16_2;

		__m128i y = LOAD_SI128((const __m128i*)(y_ptr1));
		y = _mm_sub_epi8(y, _mm_set1_epi8(param->y_offset));
		y_16_1 = _mm_unpacklo_epi8(y, _mm_setzero_si128());
		y_16_2 = _mm_unpackhi_epi8(y, _mm_setzero_si128());

		ADD_Y2RGB_16(y_16_1, y_16_2, r_16_1, g_16_1, b_16_1, r_16_2, g_16_2, b_16_2);

		__m128i r_8_11 = _mm_packus_epi16(r_16_1, r_16_2);
		__m128i g_8_11 = _mm_packus_epi16(g_16_1, g_16_2);
		__m128i b_8_11 = _mm_packus_epi16(b_16_1, b_16_2);

		/* process first 16 pixels of second line */
		r_16_1 = r_uv_16_1; g_16_1 = g_uv_16_1; b_16_1 = b_uv_16_1;
		r_16_2 = r_uv_16_2; g_16_2 = g_uv_16_2; b_16_2 = b_uv_16_2;

		y = LOAD_SI128((const __m128i*)(y_ptr2));
		y = _mm_sub_epi8(y, _mm_set1_epi8(param->y_offset));
		y_16_1 = _mm_unpacklo_epi8(y, _mm_setzero_si128());
		y_16_2 = _mm_unpackhi_epi8(y, _mm_setzero_si128());

		ADD_Y2RGB_16(y_16_1, y_16_2, r_16_1, g_16_1, b_16_1, r_16_2, g_16_2, b_16_2);

		__m128i r_8_21 = _mm_packus_epi16(r_16_1, r_16_2);
		__m128i g_8_21 = _mm_packus_epi16(g_16_1, g_16_2);
		__m128i b_8_21 = _mm_packus_epi16(b_16_1, b_16_2);

		/* process last 16 pixels of first line */
		u_16 = _mm_srai_epi16(_mm_unpackhi_epi8(u, u), 8);
		v_16 = _mm_srai_epi16(_mm_unpackhi_epi8(v, v), 8);

		UV2RGB_16(u_16, v_16, r_uv_16_1, g_uv_16_1, b_uv_16_1, r_uv_16_2, g_uv_16_2, b_uv_16_2);
		r_16_1 = r_uv_16_1; g_16_1 = g_uv_16_1; b_16_1 = b_uv_16_1;
		r_16_2 = r_uv_16_2; g_16_2 = g_uv_16_2; b_16_2 = b_uv_16_2;

		y = LOAD_SI128((const __m128i*)(y_ptr1 + 16));
		y = _mm_sub_epi8(y, _mm_set1_epi8(param->y_offset));
		y_16_1 = _mm_unpacklo_epi8(y, _mm_setzero_si128());
		y_16_2 = _mm_unpackhi_epi8(y, _mm_setzero_si128());

		ADD_Y2RGB_16(y_16_1, y_16_2, r_16_1, g_16_1, b_16_1, r_16_2, g_16_2, b_16_2);

		__m128i r_8_12 = _mm_packus_epi16(r_16_1, r_16_2);
		__m128i g_8_12 = _mm_packus_epi16(g_16_1, g_16_2);
		__m128i b_8_12 = _mm_packus_epi16(b_16_1, b_16_2);

		/* process last 16 pixels of second line */
		r_16_1 = r_uv_16_1; g_16_1 = g_uv_16_1; b_16_1 = b_uv_16_1;
		r_16_2 = r_uv_16_2; g_16_2 = g_uv_16_2; b_16_2 = b_uv_16_2;

		y = LOAD_SI128((const __m128i*)(y_ptr2 + 16));
		y = _mm_sub_epi8(y, _mm_set1_epi8(param->y_offset));
		y_16_1 = _mm_unpacklo_epi8(y, _mm_setzero_si128());
		y_16_2 = _mm_unpackhi_epi8(y, _mm_setzero_si128());

		ADD_Y2RGB_16(y_16_1, y_16_2, r_16_1, g_16_1, b_16_1, r_16_2, g_16_2, b_16_2);

		__m128i r_8_22 = _mm_packus_epi16(r_16_1, r_16_2);
		__m128i g_8_22 = _mm_packus_epi16(g_16_1, g_16_2);
		__m128i b_8_22 = _mm_packus_epi16(b_16_1, b_16_2);

		__m128i rgb[6];

		PACK_RGB24_32(r_8_11, r_8_12, g_8_11, g_8_12, b_8_11, b_8_12, rgb[0], rgb[1], rgb[2], rgb[3], rgb[4], rgb[5]);

		uint8_t *rgb_src = rgb[0].m128i_u8;
		for (int i = 0; i < 32; ++i)
		{
			memcpy(rgba_ptr1, rgb_src, sizeof(uint8_t) * 3);
			rgba_ptr1 += 3; rgb_src += 3;
			*(rgba_ptr1++) = (a_ptr1) ? *(a_ptr1++) : 255;
		}

		PACK_RGB24_32(r_8_21, r_8_22, g_8_21, g_8_22, b_8_21, b_8_22, rgb[0], rgb[1], rgb[2], rgb[3], rgb[4], rgb[5]);

		rgb_src = rgb[0].m128i_u8;
		for (int i = 0; i < 32; ++i)
		{
			memcpy(rgba_ptr2, rgb_src, sizeof(uint8_t) * 3);
			rgba_ptr2 += 3; rgb_src += 3;
			*(rgba_ptr2++) = (a_ptr2) ? *(a_ptr2++) : 255;
		}

		y_ptr1 += 32;
		y_ptr2 += 32;
		u_ptr += 16;
		v_ptr += 16;
	}
}

결론

webm 알파 정보 파싱도 완료 했고, yuva to rgba 변환 최적화도 해결했으니, 이제 OpenGL을 이용해, 텍스쳐에 webm 영상을 재생해보았습니다. https://simpl.info/videoalpha/ 의 알파가 포함된 webm 샘플 영상을 띄어봤습니다. 뒤의 푸른 배경색이 제대로 보이며, 영상이 재생되는 것을 볼수 있습니다.



소스 코드

https://github.com/KindTis/Webm2RGBA

https://github.com/KindTis/libwebm


참고

https://www.webmproject.org/

https://github.com/descampsa/yuv2rgb

https://developers.google.com/web/updates/2013/07/Alpha-transparency-in-Chrome-video

https://github.com/tomdalling/opengl-series


+ Recent posts