본문 바로가기

Dev./React

도스 삼국지2 - 에디터 (#3) / 장수 조합형 얼굴 그리기

 

장수 데이터의 offset 27과 28을 각각 faceMainId와 faceSubId로 지정한 바 있다.

이때 faceSubId 값이 있는(0보다 큼) 경우 조합형 얼굴이 되겠다.

 

if (faceSubId > 0) {

  // 조합형 얼굴 그림.

} else {

  // 완성형 얼굴 그림 (https://brokenpc.tistory.com/4)

}

 

완성형 얼굴의 경우 KAODATA.DAT에 그 재료가 있고

조합형 얼굴의 경우 MONTAGE.DAT에 그 재료가 있다.

 

MONTAGE.DAT를 살펴보자.

이 파일의 크기는 50,688 Bytes이며, 총 8가지 세트가 있다.

 

한 세트의 구성은 다음과 같다.

Upper * 4개 + Lower*4개 > Eye*4개 > Nose*4개 > Mouth*4개

 

한 세트는 같은 컬러 계열의 테마라고 생각하면 쉽다.

비슷한 느낌의 이미지들이

얼굴 이미지의 윗부분이 4장 그리고 아랫부분이 4장이며 (위+아래만 해도 64*80이 완성된다.)

비어 있는 프레임에 눈 4장, 코 4장, 그리고 입 4장의 조합이다.

 

50,688 Bytes / 8(세트) = 6,336 Bytes

 

한 세트당 6,336 Bytes이며 이것을 모두 비트로 환산하면 50,688임을 알 수 있다. (도로 50,688)

다시 50,688 Bits는 3 Bits당 1Color를 표현하므로 50,688 / 3 = 16,896 pixels가 된다.

 

이제 얼굴 데이터를 각 타입(위,아래,눈,코,입)별로 분류하면

 

먼저 주어진 faceMainId와 faceSubId를 통해서 fullFace 정보를 얻는다.

const fullFace = faceMainId | faceSubId << 8

 

맨 뒤에서 부터

처음 3bit : 세트번호 8개 (0~7번)

다음 2bit : 윗부분 타입 4개 (0~3번)

다음 2bit : 아랫부분 타입 4개 (0~3번)

다음 2bit : 눈 타입 4개 (0~3번)

다음 2bit : 코 타입 4개 (0~3번)

다음 2bit : 입 타입 4개 (0~3번) 로 구성된다.

 

이를 코드로 보면,

8가지 세트중 어느 세트인지...

const group = fullFace&0x07

 

윗 부분 (한 부분에 4개씩이므로 0x03으로 연산 해줌)

const upperType = (fullFace>>3)&0x03

아래

const lowerType = (fullFace>>5)&0x03

const eyeType = (fullFace>>7)&0x03

const noseType = (fullFace>>9)&0x03

const mouthType = (fullFace>>11)&0x03

BG (일거라 추측이 되는데 그리는데 없어도 상관 없어서 안썼음.. 나중에라도 밝혀지면 수정할 예정임)

const bgType = (fullFace>>13)&0x07

 

여기서 한개의 세트는 6,336 Bytes.

const setLength = 6336

 

const montageData = file.load(MONTAGE.DAT를 읽어온 버퍼)

 

let start = group*setLength

let offset = start + setLength

 

// get set data

const sd = montageData.slice(start, offset)

 

let byteOffset = 0

 

const widtgh = 64

const height = 40

 

// 윗면 64*18 = 1152 pixel

const upperPixel = width*18

// 1 세트에 윗면이 4개 이므로 4608 pixel

const sUpperPixel = upperPixle * 4

// 4608 pixel = 4608*3 bits = 13824 bits = 1728 Bytes

const sUpperBytes = sUpperPixel * 3 / 8

// 윗면 4개의 이미지 데이터를 가져옴

const upperBuffers = sd.slice(byteOffset, byteOffset+sUpperBytes)

byteOffset += sUpperBytes

 

// 여기에 윗면 타입을 가지고 있으므로 타입에 따른 데이터 추출이 가능하다

const upperByteLength = sUpperBytes/4

const upperBytes = upperBuffers.slice(upperType*upperByteLength, upperType*upperByteLength+upperByteLength)

 

// 이렇게 위면 이미지 upperBytes를 가져왔다.

// 같은 방법으로 lowerBytes를 추출 할 수 있다.

 

이렇게 얻어온 upperBytes, lowerBytes를 가지고

얼굴 데이터를 1차 생성 한다. (완성형 얼굴에 비하면 눈,코,입 부분이 사격형으로 뻥 뚫려 있는 이미지이다.)

 

// 가져온 이미지 데이터 버퍼

const srcBytes = [...upperBytes, ...lowerBytes]

 

// 내보낼 이미지 데이터 버퍼

const out = new Array(width*height*4*2)

 

let pixelIndex = 0

 

const lineBytes = width*4 // 256

let lineCount = 1

 

이제 완성형 얼굴과 같은 방법으로 이미지 픽셀 데이터를 채운다

 

for (let i=0; i<srcBytes.length; i+=3) {

  const greenArray = byteTo8BitArray(srcByte[i])

  const blueArray = byteTo8BitArray(srcByte[i+1])

  const redArray = byteTo8BitArray(srcByte[i+2])

 

  for (let j=0; j<8; j++) [

    const rgb = colorToArray(redArray[j], greenArray[j], blueArray[j])

    out[pixelIndex] = rgb[0]

    out[pixelIndex+lineBytes] = rgb[0]

    pixelIndex++

    out[pixelIndex] = rgb[1]

    out[pixelIndex+lineBytes] = rgb[1]

    pixelIndex++   

    out[pixelIndex] = rgb[2]

    out[pixelIndex+lineBytes] = rgb[2]

    pixelIndex++

  }

 

  const max = (3*width/8)

  if (i<0 && i%max===0) {

    pixelIndex += lineBytes

    lineCount++

  }

}

 

// 여기까지가 윗면과 아랫면으로 만든 네모난 구멍뚫린 얼굴 이미지이다.

 

이제 눈, 코, 입을 붙여보자. 위의 윗면과 아랫면을 가져오는 방법으로..

 

// 눈의 높이는 8pixel

const eyePixel = width*8

const sEyePixel = eyePixel * 4

const seyeBytes = sEyePixel * 3 / 8

const eyeBuffers = sd.slice(byteOffsetm byteOffset+sEyeBytes)

const eyeByteLength = sEyeBytes/4

const eyeBytes = eyeBuffers.slice(eyeType*eyeByteLength, eyeType*eyeByteLength+eyeByteLength)

 

// 눈의 y position은 20이다.

let posY = 20

 

pixelIndex = lineBytes*posY

 

for (let i=0; i<srcBytes.length; i+=3) {

  const greenArray = byteTo8BitArray(eyeBytes[i])

  const blueArray = byteTo8BitArray(eyeBytes[i+1])

  const redArray = byteTo8BitArray(eyeBytes[i+2])

 

  for (let j=0; j<8; j++) [

    const rgb = colorToArray(redArray[j], greenArray[j], blueArray[j])

 

    if (rgb[0]===0&&rgb[1]===0&&rgb[2]===0) {

      // 모든 값이 비어 있으면 transparent로 생각하고 넘어간다. (rgb 4byte)

      // 어차피 0 base라 안그리면 검은색으로 표현된다.

      pixelIndex+=4

    } else {

      // 값이 있으면

      out[pixelIndex] = rgb[0]

      out[pixelIndex+lineBytes] = rgb[0]

      pixelIndex++

      out[pixelIndex] = rgb[1]

      out[pixelIndex+lineBytes] = rgb[1]

      pixelIndex++    

      out[pixelIndex] = rgb[2]

      out[pixelIndex+lineBytes] = rgb[2]

      pixelIndex++

    }

  }

 

  const max = (3*width/8)

  if (i<0 && i%max===0) {

    pixelIndex += lineBytes

    lineCount++

  }

}

 

같은 방법으로 코와 입을 그린다.

코는 8pixel의 높이를 가지고 32의 y position부터 그린다

입은 10pixel의 높이를 가지고 44의 y position부터 그린다.

 

 

이 전체 로직은 getGenericFaceData라는 function으로 정리했다.

 

const getGenericFaceData = (width, height, data) => {
 
  const {idx, name, fullFace} = data

  // 8 가지 sets의 pointer (50688 / 8 = 6336 bytes)
  const group = fullFace.value&0x07
  let setLength = 6336;

  let start = group*setLength;
  let offset = start + setLength;

  const sd = montageData.slice(start, offset);

  // upper*4 > lower*4 > eye*4 > mouth*4 > nose*4

  // 6336 bytes = 50688 bits
  // 50688 bits = 1set에 16896 pixels 을 표현가능
  const upperType = (fullFace.value>>3)&0x03
  const lowerType = (fullFace.value>>5)&0x03
  const eyeType = (fullFace.value>>7)&0x03
  const noseType = (fullFace.value>>9)&0x03
  const mouthType = (fullFace.value>>11)&0x03
  const faceType = (fullFace.value>>13)&0x07
 

  let byteOffset = 0
  // sd(set data)는 윗면 4개 + 아랫면 4개 + 눈 4개 + 입 4개 + 코 4개의 이미지를 표현할 수 있다

  // 윗면 64*18=1152 pixel
  const upperPixel = width*18
  // 1 set에 윗면 4개이므로 1152*4 = 4608 pixel
  const sUpperPixel = upperPixel * 4
  // 4608 pixel = 4608 * 3 bits = 13824 bits = 1728 bytes
  const sUpperBytes = sUpperPixel * 3 / 8
  // 1728 bytes가 1개 세트의 4개 윗면 픽셀 데이터임.

  const upperBuffers = sd.slice(byteOffset, byteOffset+sUpperBytes);
  byteOffset += sUpperBytes

  // 1개 type의 윗면 데이터는 432 bytes
  const upperByteLength = sUpperBytes/4
  const upperBytes = upperBuffers.slice(upperType*upperByteLength, upperType*upperByteLength+upperByteLength)
 
  // 아랫면 64*22=1408 pixel
  const lowerPixel = width*22
  // 1 set에 아랫면 4개이므로 1408*4 = 5632 pixel
  const sLowerPixel = lowerPixel * 4
  // 5632 pixel = 5632 * 3 bits = 16896 bits = 2112 bytes
  const sLowerBytes = sLowerPixel * 3 / 8
  // 2112 bytes가 1개 세트의 4개 아랫면 픽셀 데이터임

  const lowerBuffers = sd.slice(byteOffset, byteOffset+sLowerBytes)
  byteOffset += sLowerBytes

  // 1개 type의 아랫면 데이터는 528 bytes
  const lowerByteLength = sLowerBytes/4
  const lowerBytes = lowerBuffers.slice(lowerType*lowerByteLength, lowerType*lowerByteLength+lowerByteLength)


  const srcByte = [...upperBytes, ...lowerBytes]

  const out = new Array(width*height*4*2);

  let pixelIndex = 0;

  const lineBytes = width*4

  let lineCount = 1

  for (let i=0; i<srcByte.length; i+=3) {

    const greenArray = byteTo8BitArray(srcByte[i]);
    const blueArray = byteTo8BitArray(srcByte[i+1]);
    const redArray = byteTo8BitArray(srcByte[i+2]);

    for (let j=0; j<8; j++) {
      // 2줄씩이라. 다음줄도 똑같이 그린다.
      const rgb = colorToArray(redArray[j], greenArray[j], blueArray[j]);
      out[pixelIndex] = rgb[0];
      out[pixelIndex+lineBytes] = rgb[0];
      pixelIndex++;
      out[pixelIndex] = rgb[1];
      out[pixelIndex+lineBytes] = rgb[1];
      pixelIndex++;
      out[pixelIndex] = rgb[2];
      out[pixelIndex+lineBytes] = rgb[2];
      pixelIndex++;
      out[pixelIndex] = 0xff; // alpha
      out[pixelIndex+lineBytes] = 0xff; // alpha
      pixelIndex++;
    }
 
    const max = (3*width/8)
    // i가 max mod로 0이면 새로운 줄이다.
    if (i>0 && i%max===0) {
      // new line
      pixelIndex += lineBytes;
      lineCount++
    }
  }


  const eyePixel = width*8
  const sEyePixel = eyePixel * 4
  const sEyeBytes = sEyePixel * 3 / 8
  const eyeBuffers = sd.slice(byteOffset, byteOffset+sEyeBytes)
  byteOffset += sEyeBytes

  // 1개 type의 눈 데이터는 192 bytes
  const eyeByteLength = sEyeBytes/4
  const eyeBytes = eyeBuffers.slice(eyeType*eyeByteLength, eyeType*eyeByteLength+eyeByteLength)

  let posY = 20
  pixelIndex = lineBytes*posY

  for (let i=0; i<eyeBytes.length; i+=3) {

    const greenArray = byteTo8BitArray(eyeBytes[i]);
    const blueArray = byteTo8BitArray(eyeBytes[i+1]);
    const redArray = byteTo8BitArray(eyeBytes[i+2]);

    for (let j=0; j<8; j++) {
      // 2줄씩이라. 다음줄도 똑같이 그린다.
      const rgb = colorToArray(redArray[j], greenArray[j], blueArray[j]);
      if (rgb[0]===0&&rgb[1]===0&&rgb[2]===0) {
        pixelIndex += 4
      } else {
        out[pixelIndex] = rgb[0];
        out[pixelIndex+lineBytes] = rgb[0];
        pixelIndex++;
        out[pixelIndex] = rgb[1];
        out[pixelIndex+lineBytes] = rgb[1];
        pixelIndex++;
        out[pixelIndex] = rgb[2];
        out[pixelIndex+lineBytes] = rgb[2];
        pixelIndex++;
        out[pixelIndex] = 0xff; // alpha
        out[pixelIndex+lineBytes] = 0xff; // alpha
        pixelIndex++;
      }
    }
 
    const max = (3*width/8)
    // i가 max mod로 0이면 새로운 줄이다.
    if (i>0 && i%max===0) {
      // new line
      pixelIndex += lineBytes;
      lineCount++
    }
  }

  const mouthPixel = width*10
  const sMouthPixel = mouthPixel * 4
  const sMouthBytes = sMouthPixel * 3 / 8
  const mouthBuffers = sd.slice(byteOffset, byteOffset+sMouthBytes)
  byteOffset += sMouthBytes

  // 1개 type의 코 데이터는 192 bytes
  const mouthByteLength = sMouthBytes/4
  const mouthBytes = mouthBuffers.slice(mouthType*mouthByteLengthmouthType*mouthByteLength+mouthByteLength)

  const nosePixel = width*8
  const sNosePixel = nosePixel * 4
  const sNoseBytes = sNosePixel * 3 / 8
  const noseBuffers = sd.slice(byteOffset, byteOffset+sNoseBytes)
  byteOffset += sNoseBytes

  // 1개 type의 코 데이터는 192 bytes
  const noseByteLength = sNoseBytes/4
  const noseBytes = noseBuffers.slice(noseType*noseByteLength, noseType*noseByteLength+noseByteLength)

  posY = 20+12
  pixelIndex = lineBytes*posY


  for (let i=0; i<noseBytes.length; i+=3) {

    const greenArray = byteTo8BitArray(noseBytes[i]);
    const blueArray = byteTo8BitArray(noseBytes[i+1]);
    const redArray = byteTo8BitArray(noseBytes[i+2]);

    for (let j=0; j<8; j++) {
      // 2줄씩이라. 다음줄도 똑같이 그린다.
      const rgb = colorToArray(redArray[j], greenArray[j], blueArray[j]);
      if (rgb[0]===0||rgb[1]===0||rgb[2]===0) {
        pixelIndex += 4
      } else {
        out[pixelIndex] = rgb[0];
        out[pixelIndex+lineBytes] = rgb[0];
        pixelIndex++;
        out[pixelIndex] = rgb[1];
        out[pixelIndex+lineBytes] = rgb[1];
        pixelIndex++;
        out[pixelIndex] = rgb[2];
        out[pixelIndex+lineBytes] = rgb[2];
        pixelIndex++;
        out[pixelIndex] = 0xff; // alpha
        out[pixelIndex+lineBytes] = 0xff; // alpha
        pixelIndex++;
      }
    }
 
    const max = (3*width/8)
    // i가 max mod로 0이면 새로운 줄이다.
    if (i>0 && i%max===0) {
      // new line
      pixelIndex += lineBytes;
      lineCount++
    }
  }

  posY = 20+12+12
  pixelIndex = lineBytes*posY


  for (let i=0; i<mouthBytes.length; i+=3) {

    const greenArray = byteTo8BitArray(mouthBytes[i]);
    const blueArray = byteTo8BitArray(mouthBytes[i+1]);
    const redArray = byteTo8BitArray(mouthBytes[i+2]);

    for (let j=0; j<8; j++) {
      // 2줄씩이라. 다음줄도 똑같이 그린다.
      const rgb = colorToArray(redArray[j], greenArray[j], blueArray[j]);
      if (rgb[0]===0&&rgb[1]===0&&rgb[2]===0) {
        pixelIndex += 4
      } else {
        out[pixelIndex] = rgb[0];
        out[pixelIndex+lineBytes] = rgb[0];
        pixelIndex++;
        out[pixelIndex] = rgb[1];
        out[pixelIndex+lineBytes] = rgb[1];
        pixelIndex++;
        out[pixelIndex] = rgb[2];
        out[pixelIndex+lineBytes] = rgb[2];
        pixelIndex++;
        out[pixelIndex] = 0xff; // alpha
        out[pixelIndex+lineBytes] = 0xff; // alpha
        pixelIndex++;
      }
    }
 
    const max = (3*width/8)
    // i가 max mod로 0이면 새로운 줄이다.
    if (i>0 && i%max===0) {
      // new line
      pixelIndex += lineBytes;
      lineCount++
      }
    }


  return out;
}

이 방법을 이용하여

장수 데이터 중 조합형 얼굴을 그릴 수 있게 되었다.

 

 

삼국지 시나리오 초반 군주중 하나인 유요이다.

시작시 태사자를 거느리고 있다.

다만 삼국지 연의에서 유명하지 않은 것인지 군주중에 완성형 얼굴이 아닌 조합형 얼굴이다.

 

 

다음은 각 땅의 정보에 대해서 알아보도록 하자.