feat(aklive2d): add protrait images and live2d
This commit is contained in:
12
aklive2d.js
12
aklive2d.js
@@ -119,9 +119,12 @@ async function main() {
|
|||||||
write(JSON.stringify(content, null, 2), path.join(OPERATOR_RELEASE_FOLDER, 'project.json'))
|
write(JSON.stringify(content, null, 2), path.join(OPERATOR_RELEASE_FOLDER, 'project.json'))
|
||||||
})
|
})
|
||||||
|
|
||||||
const assetsProcessor = new AssetsProcessor(OPERATOR_NAME)
|
const assetsProcessor = new AssetsProcessor(OPERATOR_NAME, OPERATOR_SHARE_FOLDER)
|
||||||
assetsProcessor.process(EXTRACTED_FOLDER).then((content) => {
|
assetsProcessor.process(EXTRACTED_FOLDER).then((content) => {
|
||||||
write(JSON.stringify(content.assetsJson, null), path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, `assets.json`))
|
write(JSON.stringify(content.landscape.assetsJson, null), path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, `assets.json`))
|
||||||
|
if (__config.operators[OPERATOR_NAME].portrait) {
|
||||||
|
write(JSON.stringify(content.portrait.assetsJson, null), path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME, `assets_portrait.json`))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const filesToCopy = [
|
const filesToCopy = [
|
||||||
@@ -146,6 +149,11 @@ async function main() {
|
|||||||
source: path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME),
|
source: path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME),
|
||||||
target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER)
|
target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
filename: `${__config.operators[OPERATOR_NAME].fallback_name}_portrait.png`,
|
||||||
|
source: path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME),
|
||||||
|
target: path.join(SHOWCASE_PUBLIC_ASSSETS_FOLDER)
|
||||||
|
}
|
||||||
]
|
]
|
||||||
filesToCopy.forEach((file) => {
|
filesToCopy.forEach((file) => {
|
||||||
copy(path.join(file.source, file.filename), path.join(file.target, file.filename))
|
copy(path.join(file.source, file.filename), path.join(file.target, file.filename))
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ invert_filter: false
|
|||||||
color: rgba(14, 126, 239, 0.85)
|
color: rgba(14, 126, 239, 0.85)
|
||||||
codename:
|
codename:
|
||||||
zh-CN: 假日威龙陈
|
zh-CN: 假日威龙陈
|
||||||
en-US: Ch'en/Chen the Holungday
|
en-US: Ch'en/Chen the Holungday
|
||||||
|
portrait: null
|
||||||
@@ -10,4 +10,5 @@ invert_filter: true
|
|||||||
color: rgb(78, 201, 187)
|
color: rgb(78, 201, 187)
|
||||||
codename:
|
codename:
|
||||||
zh-CN: 染尘烟 · 夕
|
zh-CN: 染尘烟 · 夕
|
||||||
en-US: Everything is a Miracle / Dusk
|
en-US: Everything is a Miracle / Dusk
|
||||||
|
portrait: dyn_portrait_char_2015_dusk_nian#7
|
||||||
@@ -10,4 +10,5 @@ invert_filter: true
|
|||||||
color: rgb(206, 0, 0)
|
color: rgb(206, 0, 0)
|
||||||
codename:
|
codename:
|
||||||
zh-CN: 手到牌来 · 老鲤
|
zh-CN: 手到牌来 · 老鲤
|
||||||
en-US: Trust Your Eyes / Lee
|
en-US: Trust Your Eyes / Lee
|
||||||
|
portrait: dyn_portrait_char_322_lmlee_witch#3
|
||||||
@@ -10,4 +10,5 @@ invert_filter: true
|
|||||||
color: rgb(37, 148, 197)
|
color: rgb(37, 148, 197)
|
||||||
codename:
|
codename:
|
||||||
zh-CN: 濯缨 · 令
|
zh-CN: 濯缨 · 令
|
||||||
en-US: It Does Wash the Strings / Ling
|
en-US: It Does Wash the Strings / Ling
|
||||||
|
portrait: dyn_portrait_char_2023_ling_nian#9
|
||||||
@@ -10,4 +10,5 @@ invert_filter: true
|
|||||||
color: rgb(141, 213, 228)
|
color: rgb(141, 213, 228)
|
||||||
codename:
|
codename:
|
||||||
zh-CN: 复现荣光 · 耀骑士临光
|
zh-CN: 复现荣光 · 耀骑士临光
|
||||||
en-US: Relight / Nearl
|
en-US: Relight / Nearl
|
||||||
|
portrait: dyn_portrait_char_1014_nearl2_epoque#17
|
||||||
@@ -10,4 +10,5 @@ invert_filter: true
|
|||||||
color: rgb(187, 163, 106)
|
color: rgb(187, 163, 106)
|
||||||
codename:
|
codename:
|
||||||
zh-CN: 乐逍遥 · 年
|
zh-CN: 乐逍遥 · 年
|
||||||
en-US: Unfettered Freedom / Nian
|
en-US: Unfettered Freedom / Nian
|
||||||
|
portrait: dyn_portrait_char_2014_nian_nian#4
|
||||||
@@ -10,4 +10,5 @@ invert_filter: true
|
|||||||
color: rgb(231, 166, 144)
|
color: rgb(231, 166, 144)
|
||||||
codename:
|
codename:
|
||||||
zh-CN: 今昔须臾之梦 · 异客
|
zh-CN: 今昔须臾之梦 · 异客
|
||||||
en-US: Dream in a Moment / Passager
|
en-US: Dream in a Moment / Passager
|
||||||
|
portrait: dyn_portrait_char_472_pasngr_epoque#17
|
||||||
@@ -9,4 +9,5 @@ invert_filter: false
|
|||||||
color: rgb(145, 220, 253)
|
color: rgb(145, 220, 253)
|
||||||
codename:
|
codename:
|
||||||
zh-CN: 字句中的雪原 · 鸿雪
|
zh-CN: 字句中的雪原 · 鸿雪
|
||||||
en-US: Snowy Plains in Words / Позёмка
|
en-US: Snowy Plains in Words / Позёмка
|
||||||
|
portrait: dyn_portrait_char_4055_bgsnow_wild#7
|
||||||
@@ -10,4 +10,5 @@ invert_filter: true
|
|||||||
color: rgb(116, 177, 222)
|
color: rgb(116, 177, 222)
|
||||||
codename:
|
codename:
|
||||||
zh-CN: 拥抱新生 · 迷迭香
|
zh-CN: 拥抱新生 · 迷迭香
|
||||||
en-US: Become Anew / Rosmontis
|
en-US: Become Anew / Rosmontis
|
||||||
|
portrait: dyn_portrait_char_391_rosmon_epoque#17
|
||||||
@@ -10,4 +10,5 @@ invert_filter: true
|
|||||||
color: rgba(95, 116, 187, 0.74)
|
color: rgba(95, 116, 187, 0.74)
|
||||||
codename:
|
codename:
|
||||||
zh-CN: 升华 · 浊心斯卡蒂
|
zh-CN: 升华 · 浊心斯卡蒂
|
||||||
en-US: Sublimation / Skadi the Corrupting Heart
|
en-US: Sublimation / Skadi the Corrupting Heart
|
||||||
|
portrait: dyn_portrait_char_1012_skadi2_boc#4
|
||||||
@@ -10,4 +10,5 @@ invert_filter: false
|
|||||||
color: rgb(177, 226, 249)
|
color: rgb(177, 226, 249)
|
||||||
codename:
|
codename:
|
||||||
zh-CN: 缤纷奇境 CW03 · 史尔特尔
|
zh-CN: 缤纷奇境 CW03 · 史尔特尔
|
||||||
en-US: Colorful Wonderland CW03 / Surtr
|
en-US: Colorful Wonderland CW03 / Surtr
|
||||||
|
portrait: dyn_portrait_char_350_surtr_summer#9
|
||||||
@@ -11,4 +11,5 @@ invert_filter: true
|
|||||||
color: rgba(0, 0, 0, 0.83)
|
color: rgba(0, 0, 0, 0.83)
|
||||||
codename:
|
codename:
|
||||||
zh-CN: 恍惚 · W
|
zh-CN: 恍惚 · W
|
||||||
en-US: Wonder
|
en-US: Wonder / W
|
||||||
|
portrait: dyn_portrait_char_113_cqbw_epoque#7
|
||||||
@@ -3,12 +3,12 @@ import path from "path";
|
|||||||
|
|
||||||
export default class AlphaComposite {
|
export default class AlphaComposite {
|
||||||
|
|
||||||
async process(filename, extractedDir) {
|
async process(filename, maskFilename, extractedDir) {
|
||||||
const image = sharp(path.join(extractedDir, filename))
|
const image = sharp(path.join(extractedDir, filename))
|
||||||
.removeAlpha()
|
.removeAlpha()
|
||||||
const imageMeta = await image.metadata()
|
const imageMeta = await image.metadata()
|
||||||
const imageBuffer = await image.toBuffer()
|
const imageBuffer = await image.toBuffer()
|
||||||
const mask = await sharp(path.join(extractedDir, `${path.parse(filename).name}[alpha].png`))
|
const mask = await sharp(path.join(extractedDir, maskFilename))
|
||||||
.extractChannel("blue")
|
.extractChannel("blue")
|
||||||
.resize(imageMeta.width, imageMeta.height)
|
.resize(imageMeta.width, imageMeta.height)
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
@@ -16,7 +16,16 @@ export default class AlphaComposite {
|
|||||||
return sharp(imageBuffer)
|
return sharp(imageBuffer)
|
||||||
.joinChannel(mask)
|
.joinChannel(mask)
|
||||||
.toBuffer()
|
.toBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
async crop(buffer, rect) {
|
||||||
|
const left = rect.y
|
||||||
|
const top = rect.x
|
||||||
|
const width = rect.h
|
||||||
|
const height = rect.w
|
||||||
|
const rotate = rect.rotate === 0 ? -90 : 0
|
||||||
|
const newImage = await sharp(buffer).rotate(90).extract({ left: left, top: top, width: width, height: height }).resize(width, height).extract({ left: 0, top: 0, width: width, height: height }).toBuffer()
|
||||||
|
return await sharp(newImage).rotate(rotate).toBuffer()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,38 +1,63 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { copy, read, write } from './file.js'
|
import { read, write, readSync } from './file.js'
|
||||||
import AlphaComposite from './alpha_composite.js'
|
import AlphaComposite from './alpha_composite.js'
|
||||||
|
|
||||||
export default class AssetsProcessor {
|
export default class AssetsProcessor {
|
||||||
#operatorSourceFolder
|
#operatorSourceFolder
|
||||||
#alphaCompositer
|
#alphaCompositer
|
||||||
#operatorName
|
#operatorName
|
||||||
|
#shareFolder
|
||||||
|
|
||||||
constructor(operatorName) {
|
constructor(operatorName, shareFolder) {
|
||||||
this.#operatorSourceFolder = path.join(__projetRoot, __config.folder.operator)
|
this.#operatorSourceFolder = path.join(__projetRoot, __config.folder.operator)
|
||||||
this.#alphaCompositer = new AlphaComposite()
|
this.#alphaCompositer = new AlphaComposite()
|
||||||
this.#operatorName = operatorName
|
this.#operatorName = operatorName
|
||||||
|
this.#shareFolder = shareFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
async process(extractedDir) {
|
async process(extractedDir) {
|
||||||
|
const fallback_name = __config.operators[this.#operatorName].fallback_name
|
||||||
|
const fallbackFilename = `${fallback_name}.png`
|
||||||
|
const fallbackBuffer = await this.#alphaCompositer.process(fallbackFilename, `${path.parse(fallbackFilename).name}[alpha].png`, extractedDir)
|
||||||
|
await write(fallbackBuffer, path.join(this.#operatorSourceFolder, this.#operatorName, fallbackFilename))
|
||||||
|
|
||||||
|
// generate portrait
|
||||||
|
const portraitDir = path.join(this.#shareFolder, "portraits")
|
||||||
|
const portraitHub = JSON.parse(readSync(path.join(portraitDir, "MonoBehaviour", "portrait_hub.json")))
|
||||||
|
const portraitAtlas = portraitHub._sprites.find((item) => item.name === fallback_name).atlas
|
||||||
|
const portraitJson = JSON.parse(readSync(path.join(portraitDir, "MonoBehaviour", `portraits#${portraitAtlas}.json`)))
|
||||||
|
const item = portraitJson._sprites.find((item) => item.name === fallback_name)
|
||||||
|
const rect = {
|
||||||
|
...item.rect,
|
||||||
|
rotate: item.rotate
|
||||||
|
}
|
||||||
|
const protraitFilename = `portraits#${portraitAtlas}.png`
|
||||||
|
const portraitBuffer = await this.#alphaCompositer.process(protraitFilename, `${path.parse(protraitFilename).name}a.png`, path.join(portraitDir, "Texture2D"))
|
||||||
|
const croppedBuffer = await this.#alphaCompositer.crop(portraitBuffer, rect)
|
||||||
|
await write(croppedBuffer, path.join(this.#operatorSourceFolder, this.#operatorName, `${fallback_name}_portrait.png`))
|
||||||
|
|
||||||
|
return {
|
||||||
|
landscape: await this.#generateAssets(__config.operators[this.#operatorName].filename, extractedDir),
|
||||||
|
portrait: __config.operators[this.#operatorName].portrait ? await this.#generateAssets(__config.operators[this.#operatorName].portrait, extractedDir) : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #generateAssets(filename, extractedDir) {
|
||||||
const BASE64_BINARY_PREFIX = 'data:application/octet-stream;base64,'
|
const BASE64_BINARY_PREFIX = 'data:application/octet-stream;base64,'
|
||||||
const BASE64_PNG_PREFIX = 'data:image/png;base64,'
|
const BASE64_PNG_PREFIX = 'data:image/png;base64,'
|
||||||
const assetsJson = {}
|
const assetsJson = {}
|
||||||
const skelFilename = `${__config.operators[this.#operatorName].filename}.skel`
|
const skelFilename = `${filename}.skel`
|
||||||
const skel = await read(path.join(extractedDir, skelFilename), null)
|
const skel = await read(path.join(extractedDir, skelFilename), null)
|
||||||
const atlasFilename = `${__config.operators[this.#operatorName].filename}.atlas`
|
const atlasFilename = `${filename}.atlas`
|
||||||
const atlas = await read(path.join(extractedDir, atlasFilename))
|
const atlas = await read(path.join(extractedDir, atlasFilename))
|
||||||
const dimensions = atlas.match(new RegExp(/^size:(.*),(.*)/gm))[0].replace('size: ', '').split(',')
|
const dimensions = atlas.match(new RegExp(/^size:(.*),(.*)/gm))[0].replace('size: ', '').split(',')
|
||||||
const matches = atlas.match(new RegExp(/(.*).png/g))
|
const matches = atlas.match(new RegExp(/(.*).png/g))
|
||||||
for (const item of matches) {
|
for (const item of matches) {
|
||||||
const buffer = await this.#alphaCompositer.process(item, extractedDir)
|
const buffer = await this.#alphaCompositer.process(item, `${path.parse(item).name}[alpha].png`, extractedDir)
|
||||||
assetsJson[`./assets/${item}`] = BASE64_PNG_PREFIX + buffer.toString('base64')
|
assetsJson[`./assets/${item}`] = BASE64_PNG_PREFIX + buffer.toString('base64')
|
||||||
}
|
}
|
||||||
assetsJson[`./assets/${skelFilename.replace('#', '%23')}`] = BASE64_BINARY_PREFIX + skel.toString('base64')
|
assetsJson[`./assets/${skelFilename.replace('#', '%23')}`] = BASE64_BINARY_PREFIX + skel.toString('base64')
|
||||||
assetsJson[`./assets/${atlasFilename.replace('#', '%23')}`] = BASE64_BINARY_PREFIX + Buffer.from(atlas).toString('base64')
|
assetsJson[`./assets/${atlasFilename.replace('#', '%23')}`] = BASE64_BINARY_PREFIX + Buffer.from(atlas).toString('base64')
|
||||||
|
|
||||||
const fallbackFilename = `${__config.operators[this.#operatorName].fallback_name}.png`
|
|
||||||
const fallbackBuffer = await this.#alphaCompositer.process(fallbackFilename, extractedDir)
|
|
||||||
await write(fallbackBuffer, path.join(this.#operatorSourceFolder, this.#operatorName, fallbackFilename))
|
|
||||||
return {
|
return {
|
||||||
dimensions,
|
dimensions,
|
||||||
assetsJson
|
assetsJson
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function process(config) {
|
|||||||
// add title
|
// add title
|
||||||
operator.title = `${config.share.title["en-US"]}${operator.codename["en-US"]} - ${config.share.title["zh-CN"]}${operator.codename["zh-CN"]}`
|
operator.title = `${config.share.title["en-US"]}${operator.codename["en-US"]} - ${config.share.title["zh-CN"]}${operator.codename["zh-CN"]}`
|
||||||
// add type
|
// add type
|
||||||
operator.type = operator.codename["zh-CN"].includes('·') ? 'operator' : 'skin'
|
operator.type = operator.codename["zh-CN"].includes('·') ? 'skin' : 'operator'
|
||||||
|
|
||||||
// add link
|
// add link
|
||||||
operator.link = operatorName
|
operator.link = operatorName
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { writeSync, copy, rmdir } from './file.js'
|
import { writeSync, copy, rmdir } from './file.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO:
|
||||||
|
* 1. add voice config -> look up charword table
|
||||||
|
*/
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const targetFolder = path.join(__projetRoot, __config.folder.release, __config.folder.directory);
|
const targetFolder = path.join(__projetRoot, __config.folder.release, __config.folder.directory);
|
||||||
const sourceFolder = path.join(__projetRoot, __config.folder.operator);
|
const sourceFolder = path.join(__projetRoot, __config.folder.operator);
|
||||||
@@ -13,7 +18,9 @@ export default function () {
|
|||||||
}
|
}
|
||||||
writeSync(JSON.stringify(directoryJson, null), path.join(targetFolder, "directory.json"))
|
writeSync(JSON.stringify(directoryJson, null), path.join(targetFolder, "directory.json"))
|
||||||
filesToCopy.forEach((key) => {
|
filesToCopy.forEach((key) => {
|
||||||
const filename = `${__config.operators[key].filename}.json`;
|
copy(path.join(sourceFolder, key, 'assets.json'), path.join(targetFolder, `${__config.operators[key].filename}.json`))
|
||||||
copy(path.join(sourceFolder, key, 'assets.json'), path.join(targetFolder, filename))
|
if (__config.operators[key].portrait) {
|
||||||
|
copy(path.join(sourceFolder, key, 'assets_portrait.json'), path.join(targetFolder, `${__config.operators[key].portrait}.json`))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user