Imported from Medium story (Sep 4, 2022)
최근 대부분의 PFP NFT는 이미지를 구성할 각 파츠 이미지들을 만들어 두고, 프로그래밍을 통해 이를 랜덤으로 조합하여 최종 이미지를 생성하는 방식을 사용합니다.

예를 들어 BAYC #3419의 경우 그림의 배경, 옷, 눈, 털, 모자, 입을 구성하는 각각의 파츠 이미지들이 합성되어 하나의 최종 이미지를 만들어 냅니다. OpenSea와 같은 NFT 거래소나 NFT 뷰어 앱 등에서 이미지를 구성하는 파츠들을 볼 수 있습니다.

그리고 BAYC Gallery에 가면 전체적으로 어떤 파츠들이 있고, 각 파츠들을 어떻게 구성했는지도 볼 수 있습니다.
이렇게 하나의 최종 이미지를 만들기 위해서는 보통 배경이 투명한 파츠 이미지들을 만들어 이를 합성합니다. 파츠 이미지들이 준비되었다면, imagemagick 라이브러리와 Javascript를 이용하여 Generative NFT 이미지를 생성할 수 있습니다.
먼저 합성할 imageList를 받아 imageName으로 저장해주는 compositeImage함수를 만듭니다. 넘겨받은 imageList에 있는 파일들을 순서대로 imagemagick 명령어에 맞게 겹겹이 쌓아올리고 이미지 합성을 실행합니다.
const imagemagick = require('imagemagick'); | |
const rootPath = require('app-root-path'); | |
const compositeImage = async (imageName, imageList) => new Promise((resolve, reject) => { | |
try { | |
const commands = [imageList[0]]; | |
for (let i = 1; i < imageList.length; i += 1) { | |
commands.push('-coalesce', 'null:', imageList[i], '-layers', 'composite'); | |
} | |
commands.push(`${rootPath}/images/${imageName}.png`); | |
imagemagick.convert(commands, (error, result) => { | |
if (error) { | |
console.error(error); | |
reject(error); | |
} else { | |
console.log(result); | |
resolve(`${rootPath}/images/${imageName}.png`); | |
} | |
}); | |
} catch (error) { | |
reject(error); | |
} | |
}); |
그리고 앞서 만든 compositeImage함수에 imageName, imageList를 건내주며 생성하고자 하는 이미지 수 만큼 호출합니다.
const fs = require('fs'); | |
const rootPath = require('app-root-path'); | |
const composite = async () => { | |
const total = compositeInfo.length; | |
let current = 0; | |
for (const { background, hair, earrings, mouth, eyes, clothes } of compositeInfo) { | |
if (!fs.existsSync(`${rootPath}/images/${current}.png`)) { | |
const result = await compositeImage( | |
current, | |
[ | |
`${rootPath}/background/${background}`, | |
`${rootPath}/face/default_face.png`, | |
`${rootPath}/hair/${hair}`, | |
`${rootPath}/earrings/${earrings}`, | |
`${rootPath}/mouth/${mouth}`, | |
`${rootPath}/eyes/${eyes}`, | |
`${rootPath}/clothes/${clothes}`, | |
], | |
); | |
console.log(`[${current + 1}/${total}] ${result}`); | |
} | |
current += 1; | |
} | |
}; |
BAYC의 경우 각 파츠의 생김새는 다양하지만, 원숭이 얼굴의 형태를 가진 베이스 이미지는 항상 동일하게 들어갑니다. 이러한 베이스 이미지도 이미지 합성시 빠지지 않도록 순서에 맞게 넣어주어야 합니다. 위 코드의 경우 imageList의 두 번째에 들어가는 face attribute입니다.
그리고 각 파츠 이미지들을 어떤 방식으로 준비했는지에 따라 합성 순서가 중요해질 수 있으므로 imageList생성시 이미지 파일명을 넣는 순서에 신경써야 합니다.

HAPEBEAST NFT의 위 이미지처럼 원숭이의 귀가 모자를 뚫고 나오는 경우의 수를 생각하지 못하고 이미지를 합성하여 퀄리티 논란이 생겼던 적도 있었습니다. 파츠 제작이나 합성시 이런 경우의 수를 잘 따져봐야 합니다.
[
{
"background": "background_3.png",
"hair": "hair_8.png",
"earrings": "earrings_1.png",
"mouth": "mouth_9.png",
"eyes": "eyes_2.png",
"clothes": "clothes_5.png"
},
{
"background": "background_1.png",
"hair": "hair_1.png",
"earrings": "earrings_5.png",
"mouth": "mouth_3.png",
"eyes": "eyes_6.png",
"clothes": "clothes_2.png"
},
...
]
예시 코드에서 사용되는 compositeInfo의 형태는 위와 같습니다. 각 파츠 이미지 파일명의 prefix를 통일하고 뒤에 붙는 숫자로 이미지를 구분되게 하면, 랜덤 함수로 숫자만 붙여주는 간단한 스크립트로 JSON 파일을 생성하여 사용할 수 있습니다.
랜덤 함수를 통해 위와 같은 이미지 리스트를 생성하면서 Metadata JSON 파일 생성을 위한 스크립트도 동시에 생성하면 편리하게 Generative NFT를 만들어볼 수 있을 것입니다.