xnb.js 개발일지(2)

Lybell·2022년 6월 23일
0

xnb.js 개발일지

목록 보기
2/6

이 시리즈는 제가 개인 프로젝트 등을 제작하면서 배운 프로그래밍/CS 지식을 정리해둔 시리즈입니다.

개발 상황

현재 xnb.js의 1.2버전을 개발하고 있다. xnb.js는 다음과 같은 기능이 추가되며, 이제 기능 구현과 디버깅이 완료되었고, 빌드와 퍼블리싱을 남겨두고 있다.

  • png 파일을 단독으로 xnb 파일로 변환하는 기능
  • Texture2D 언패킹 시 format:2를 압축 해제하는 기능

개발 도중 발견한 버그들

32bit rgba가 아닌 png 파일 패킹 버그

xnb.js 개발일지(1)를 쓰다가, 다음의 문장에 의문점이 들었다.

모든 png 파일은 32bit RGBA 포맷을 사용한다.

format이 0인 xnb 파일의 png 텍스처는 32bit RGBA로 간주되지만, 모든 png 파일이 32bit RGBA 데이터를 저장한다고 보장할 수 없다. png 파일은 색상 채널 수와 색상 처리 방식 등으로 인해 여러가지로 나뉜다.

  • PNG-8 : 픽셀 하나에 8bit를 사용. 컬러 팔레트를 이용하여, 각 픽셀 데이터는 컬러 팔레트의 인덱스만 저장한다.
  • PNG-24 : 픽셀 하나에 24bit를 사용. RGB 각각 8bit씩 사용해서 저장한다.
  • PNG-32 : 픽셀 하나에 32bit를 사용. RGBA 각각 8bit씩 사용해서 저장한다.
  • 그 외에도 각 채널당 16비트를 사용해서 색상을 저장하는 변종도 있다.

그래서 사용자가 PNG-8 혹은 PNG-24를 xnb 파일로 패킹하려고 하면, 패킹 자체는 가능하지만, 언팩 시 xnb.js는 32bit RGBA를 사용하므로 png 파일을 제대로 언팩하지 못할 것이다.

해당 잠재적인 문제를 해결하고자, channels가 4가 아니거나 depth가 8이 아니거나 palette가 존재하는 경우 32bit RGBA로 변환시키는 코드를 추가하였다.

function fixPNG(pngdata)
{
  const { width, height, channels, depth } = pngdata;
  let {data} = pngdata;

  if(pngdata.palette) return applyPalette(data, depth, pngdata.palette);

  if(depth === 16) data = png16to8(data);
  if(channels < 4) data = addChannels(data, channels);
  return data;
}

async function readExternFiles(extension, files)
{
  // Texture2D to PNG
  if(extension === "png")
  {
    // get binary file
    const rawPng = await readBlobasArrayBuffer(files.png);

    // get the png data
    let png = fromPNG(new Uint8Array(rawPng) );

    // if the png data is not 32bit-rgba, fix it
    if(png.channel !== 4 || png.depth !== 8 || png.palette !== undefined) {
      png.data = fixPNG(png);
    }

    return {
      type: "Texture2D",
      data: png.data,
      width: png.width,
      height: png.height
    };
  }
  // ...

패킹과 언팩을 반복했을 때 텍스처 파일의 투명 부분이 어두워지는 버그

이 버그는 Texture2DReader 클래스 코드를 분석하다가 우연히 발견한 버그다.

read(buffer) {
  // ...
  let data = buffer.read(dataSize);
  // ...

  // add the alpha channel into the image
  for(let i = 0; i < data.length; i += 4) {
    let inverseAlpha = 255 / data[i + 3];
    data[i    ] = Math.min(Math.ceil(data[i    ] * inverseAlpha), 255);
    data[i + 1] = Math.min(Math.ceil(data[i + 1] * inverseAlpha), 255);
    data[i + 2] = Math.min(Math.ceil(data[i + 2] * inverseAlpha), 255);
  }
  // ...
}

해당 부분에서 buffer.read()는 ArrayBuffer 오브젝트를 반환한다. 하지만, ArrayBuffer 오브젝트엔 length가 존재하지 않고, 배열처럼 인덱싱으로 접근할 수도 없다. 따라서, data가 중간에 Uint8Array 등으로 바뀌지 않는 한 해당 코드는 실행되지 않는다.
단, write 메소드의 비슷한 부분은 Uint8Array를 조작하므로 실행된다. 그래서 패킹과 언팩을 반복했을 때 알파값이 존재하는 텍스처 파일의 RGB 값이 손실될 수 있다.

해당 부분은 data를 Uint8Array로 형변환해서 해결했다.

오늘의 학습

Big Endian & Little Endian

xnb.js는 바이너리 파일을 다루는 라이브러리이기 때문에, 바이너리 파일에 대해 많이 알아야 할 수밖에 없다. 그 중 오늘 알아본 부분은 빅 엔디언과 리틀 엔디언이다.
메모리에 1바이트 데이터를 저장할 때는 괜찮지만, 2바이트 이상의 데이터를 저장할 때 어떤 것을 먼저 저장하는지가 문제가 된다. 이 중 자릿수가 큰 것을 앞에 저장하는 것이 빅 엔디언, 자리수가 작은 것을 앞에 저장하는 것이 리틀 엔디언이다.

4바이트 데이터인 0x12345678을 메모리에 저장한다고 생각해 보자. 이 데이터는 0x12, 0x34, 0x56, 0x78로 나뉘는데, 빅 엔디언은 작은 메모리 순으로 0x12, 0x34, 0x56, 0x78로 저장하는 반면, 리틀 엔디언은 작은 메모리 순으로 0x78, 0x56, 0x34, 0x12 순으로 저장하게 된다.

컴퓨터가 무슨 엔디언을 사용하는지는 CPU마다 다른데, 예를 들어 내가 사용하는 인텔 CPU는 리틀 엔디언을 사용하고 있다. 반면, 모토로라 프로세서나 네트워크에서는 빅 엔디언을 사용하고 있다.

파일의 데이터를 저장하는 데 리틀 엔디언을 사용하는지 빅 엔디언을 사용하는지는 파일마다도 차이가 있는데, xnb 파일은 리틀 엔디언을 사용해 데이터를 저장하고 있다. 반면, 빅 엔디언을 사용하는 대표적인 파일 포맷은 png이다. 바이너리 파일에서 데이터를 추출할 때, 파일이 어떤 엔디언을 사용하는지는 중요하다고 할 수 있다. 데이터를 추출할 때 반대 엔디언을 사용하면 값이 뒤집혀서 엉뚱한 값을 추출하기 때문이다.

premultiplied alpha

for(let i = 0; i < data.length; i += 4) {
  let inverseAlpha = 255 / data[i + 3];
  data[i    ] = Math.min(Math.ceil(data[i    ] * inverseAlpha), 255);
  data[i + 1] = Math.min(Math.ceil(data[i + 1] * inverseAlpha), 255);
  data[i + 2] = Math.min(Math.ceil(data[i + 2] * inverseAlpha), 255);
}

xnb 언패커 프로그램의 텍스처 언팩 부분 중, 위의 코드가 왜 쓰이는 건지 궁금해졌다. 자료를 조사하다 premultiplied alpha라는 것을 알게 되었다.

RGBA 데이터는 서로 다른 2가지 형식으로 나뉘는데, 하나는 우리가 그래픽 작업을 할 때 매우 친숙한 straight alpha이고, 다른 하나는 RGB값에 알파를 가공한 premultiplied alpha이다.
straight alpha는 우리가 평소에 쓰던 RGBA 형식으로, 여기에서 알파 값은 픽셀의 투명도를 의미하고, RGB값은 빨강, 녹색, 파란색의 순수한 강도를 의미한다. 반면, premultiplied alpha는 straight alpha의 RGB값에 알파 값을 추가로 곱한 값을 RGB값으로 가진다. rgb값만 가지고도 투명도가 적용된 색상을 알 수 있어서 이미지 필터링, 혼합 등에 쓰인다고 한다.

xnb 파일에 저장되는 RGBA 값은 premultiplied alpha 포맷을 사용한다. 반면, 우리가 사용하는 png 파일 등은 straight alpha를 사용하기 때문에, 포맷을 상호 변환해주기 위해 위의 코드가 사용된 것이다.

profile
홍익인간이 되고 싶은 꿈꾸는 방랑자

0개의 댓글