장수 데이터의 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*mouthByteLength, mouthType*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;
}
이 방법을 이용하여
장수 데이터 중 조합형 얼굴을 그릴 수 있게 되었다.
삼국지 시나리오 초반 군주중 하나인 유요이다.
시작시 태사자를 거느리고 있다.
다만 삼국지 연의에서 유명하지 않은 것인지 군주중에 완성형 얼굴이 아닌 조합형 얼굴이다.
다음은 각 땅의 정보에 대해서 알아보도록 하자.