From 3eed10772cfdd40c607bee6207420a2a2b572a0f Mon Sep 17 00:00:00 2001 From: Haoyu Xu Date: Sat, 25 Feb 2023 23:58:09 -0500 Subject: [PATCH] feat(aklive2d): add protrait images and live2d --- aklive2d.js | 12 +++++- config/_template.yaml | 3 +- config/dusk_everything_is_a_miracle.yaml | 3 +- config/lee_trust_your_eyes.yaml | 3 +- config/ling_it_does_wash_the_strings.yaml | 3 +- config/nearl_relight.yaml | 3 +- config/nian_unfettered_freedom.yaml | 3 +- config/passager_dream_in_a_moment.yaml | 3 +- config/pozemka_snowy_plains_in_words.yaml | 3 +- config/rosmontis_become_anew.yaml | 3 +- config/skadi_sublimation.yaml | 3 +- config/surtr_colorful_wonderland.yaml | 3 +- config/w_fugue.yaml | 3 +- libs/alpha_composite.js | 13 ++++++- libs/assets_processor.js | 45 ++++++++++++++++++----- libs/config.js | 2 +- libs/directory.js | 11 +++++- 17 files changed, 90 insertions(+), 29 deletions(-) diff --git a/aklive2d.js b/aklive2d.js index 4ddaa34..5332b5a 100644 --- a/aklive2d.js +++ b/aklive2d.js @@ -119,9 +119,12 @@ async function main() { 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) => { - 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 = [ @@ -146,6 +149,11 @@ async function main() { source: path.join(OPERATOR_SOURCE_FOLDER, OPERATOR_NAME), 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) => { copy(path.join(file.source, file.filename), path.join(file.target, file.filename)) diff --git a/config/_template.yaml b/config/_template.yaml index bf145b7..1d13bc1 100644 --- a/config/_template.yaml +++ b/config/_template.yaml @@ -10,4 +10,5 @@ invert_filter: false color: rgba(14, 126, 239, 0.85) codename: zh-CN: 假日威龙陈 - en-US: Ch'en/Chen the Holungday \ No newline at end of file + en-US: Ch'en/Chen the Holungday +portrait: null \ No newline at end of file diff --git a/config/dusk_everything_is_a_miracle.yaml b/config/dusk_everything_is_a_miracle.yaml index 723f9f4..fd3c47b 100644 --- a/config/dusk_everything_is_a_miracle.yaml +++ b/config/dusk_everything_is_a_miracle.yaml @@ -10,4 +10,5 @@ invert_filter: true color: rgb(78, 201, 187) codename: zh-CN: 染尘烟 · 夕 - en-US: Everything is a Miracle / Dusk \ No newline at end of file + en-US: Everything is a Miracle / Dusk +portrait: dyn_portrait_char_2015_dusk_nian#7 \ No newline at end of file diff --git a/config/lee_trust_your_eyes.yaml b/config/lee_trust_your_eyes.yaml index 4053417..666dc6d 100644 --- a/config/lee_trust_your_eyes.yaml +++ b/config/lee_trust_your_eyes.yaml @@ -10,4 +10,5 @@ invert_filter: true color: rgb(206, 0, 0) codename: zh-CN: 手到牌来 · 老鲤 - en-US: Trust Your Eyes / Lee \ No newline at end of file + en-US: Trust Your Eyes / Lee +portrait: dyn_portrait_char_322_lmlee_witch#3 \ No newline at end of file diff --git a/config/ling_it_does_wash_the_strings.yaml b/config/ling_it_does_wash_the_strings.yaml index 8dc8cfa..3a5d8a6 100644 --- a/config/ling_it_does_wash_the_strings.yaml +++ b/config/ling_it_does_wash_the_strings.yaml @@ -10,4 +10,5 @@ invert_filter: true color: rgb(37, 148, 197) codename: zh-CN: 濯缨 · 令 - en-US: It Does Wash the Strings / Ling \ No newline at end of file + en-US: It Does Wash the Strings / Ling +portrait: dyn_portrait_char_2023_ling_nian#9 \ No newline at end of file diff --git a/config/nearl_relight.yaml b/config/nearl_relight.yaml index 05a8fc0..5620a26 100644 --- a/config/nearl_relight.yaml +++ b/config/nearl_relight.yaml @@ -10,4 +10,5 @@ invert_filter: true color: rgb(141, 213, 228) codename: zh-CN: 复现荣光 · 耀骑士临光 - en-US: Relight / Nearl \ No newline at end of file + en-US: Relight / Nearl +portrait: dyn_portrait_char_1014_nearl2_epoque#17 \ No newline at end of file diff --git a/config/nian_unfettered_freedom.yaml b/config/nian_unfettered_freedom.yaml index 29deb34..51591d4 100644 --- a/config/nian_unfettered_freedom.yaml +++ b/config/nian_unfettered_freedom.yaml @@ -10,4 +10,5 @@ invert_filter: true color: rgb(187, 163, 106) codename: zh-CN: 乐逍遥 · 年 - en-US: Unfettered Freedom / Nian \ No newline at end of file + en-US: Unfettered Freedom / Nian +portrait: dyn_portrait_char_2014_nian_nian#4 \ No newline at end of file diff --git a/config/passager_dream_in_a_moment.yaml b/config/passager_dream_in_a_moment.yaml index bf15daf..9c4184a 100644 --- a/config/passager_dream_in_a_moment.yaml +++ b/config/passager_dream_in_a_moment.yaml @@ -10,4 +10,5 @@ invert_filter: true color: rgb(231, 166, 144) codename: zh-CN: 今昔须臾之梦 · 异客 - en-US: Dream in a Moment / Passager \ No newline at end of file + en-US: Dream in a Moment / Passager +portrait: dyn_portrait_char_472_pasngr_epoque#17 \ No newline at end of file diff --git a/config/pozemka_snowy_plains_in_words.yaml b/config/pozemka_snowy_plains_in_words.yaml index e38e1db..f3acd02 100644 --- a/config/pozemka_snowy_plains_in_words.yaml +++ b/config/pozemka_snowy_plains_in_words.yaml @@ -9,4 +9,5 @@ invert_filter: false color: rgb(145, 220, 253) codename: zh-CN: 字句中的雪原 · 鸿雪 - en-US: Snowy Plains in Words / Позёмка \ No newline at end of file + en-US: Snowy Plains in Words / Позёмка +portrait: dyn_portrait_char_4055_bgsnow_wild#7 \ No newline at end of file diff --git a/config/rosmontis_become_anew.yaml b/config/rosmontis_become_anew.yaml index 20824fa..04c44a4 100644 --- a/config/rosmontis_become_anew.yaml +++ b/config/rosmontis_become_anew.yaml @@ -10,4 +10,5 @@ invert_filter: true color: rgb(116, 177, 222) codename: zh-CN: 拥抱新生 · 迷迭香 - en-US: Become Anew / Rosmontis \ No newline at end of file + en-US: Become Anew / Rosmontis +portrait: dyn_portrait_char_391_rosmon_epoque#17 \ No newline at end of file diff --git a/config/skadi_sublimation.yaml b/config/skadi_sublimation.yaml index c505de0..b09c68f 100644 --- a/config/skadi_sublimation.yaml +++ b/config/skadi_sublimation.yaml @@ -10,4 +10,5 @@ invert_filter: true color: rgba(95, 116, 187, 0.74) codename: zh-CN: 升华 · 浊心斯卡蒂 - en-US: Sublimation / Skadi the Corrupting Heart \ No newline at end of file + en-US: Sublimation / Skadi the Corrupting Heart +portrait: dyn_portrait_char_1012_skadi2_boc#4 \ No newline at end of file diff --git a/config/surtr_colorful_wonderland.yaml b/config/surtr_colorful_wonderland.yaml index d9e028b..c77d7cb 100644 --- a/config/surtr_colorful_wonderland.yaml +++ b/config/surtr_colorful_wonderland.yaml @@ -10,4 +10,5 @@ invert_filter: false color: rgb(177, 226, 249) codename: zh-CN: 缤纷奇境 CW03 · 史尔特尔 - en-US: Colorful Wonderland CW03 / Surtr \ No newline at end of file + en-US: Colorful Wonderland CW03 / Surtr +portrait: dyn_portrait_char_350_surtr_summer#9 \ No newline at end of file diff --git a/config/w_fugue.yaml b/config/w_fugue.yaml index 8a514a7..ffcfbb3 100644 --- a/config/w_fugue.yaml +++ b/config/w_fugue.yaml @@ -11,4 +11,5 @@ invert_filter: true color: rgba(0, 0, 0, 0.83) codename: zh-CN: 恍惚 · W - en-US: Wonder \ No newline at end of file + en-US: Wonder / W +portrait: dyn_portrait_char_113_cqbw_epoque#7 \ No newline at end of file diff --git a/libs/alpha_composite.js b/libs/alpha_composite.js index edd1ebd..74577a0 100644 --- a/libs/alpha_composite.js +++ b/libs/alpha_composite.js @@ -3,12 +3,12 @@ import path from "path"; export default class AlphaComposite { - async process(filename, extractedDir) { + async process(filename, maskFilename, extractedDir) { const image = sharp(path.join(extractedDir, filename)) .removeAlpha() const imageMeta = await image.metadata() 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") .resize(imageMeta.width, imageMeta.height) .toBuffer(); @@ -16,7 +16,16 @@ export default class AlphaComposite { return sharp(imageBuffer) .joinChannel(mask) .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() } } \ No newline at end of file diff --git a/libs/assets_processor.js b/libs/assets_processor.js index f8b163d..e718044 100644 --- a/libs/assets_processor.js +++ b/libs/assets_processor.js @@ -1,38 +1,63 @@ import path from 'path' -import { copy, read, write } from './file.js' +import { read, write, readSync } from './file.js' import AlphaComposite from './alpha_composite.js' export default class AssetsProcessor { #operatorSourceFolder #alphaCompositer #operatorName + #shareFolder - constructor(operatorName) { + constructor(operatorName, shareFolder) { this.#operatorSourceFolder = path.join(__projetRoot, __config.folder.operator) this.#alphaCompositer = new AlphaComposite() this.#operatorName = operatorName + this.#shareFolder = shareFolder } 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_PNG_PREFIX = 'data:image/png;base64,' + const BASE64_PNG_PREFIX = 'data:image/png;base64,' const assetsJson = {} - const skelFilename = `${__config.operators[this.#operatorName].filename}.skel` + const skelFilename = `${filename}.skel` 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 dimensions = atlas.match(new RegExp(/^size:(.*),(.*)/gm))[0].replace('size: ', '').split(',') const matches = atlas.match(new RegExp(/(.*).png/g)) 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/${skelFilename.replace('#', '%23')}`] = BASE64_BINARY_PREFIX + skel.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 { dimensions, assetsJson diff --git a/libs/config.js b/libs/config.js index ffa1637..ced88a4 100644 --- a/libs/config.js +++ b/libs/config.js @@ -10,7 +10,7 @@ function process(config) { // add title operator.title = `${config.share.title["en-US"]}${operator.codename["en-US"]} - ${config.share.title["zh-CN"]}${operator.codename["zh-CN"]}` // add type - operator.type = operator.codename["zh-CN"].includes('·') ? 'operator' : 'skin' + operator.type = operator.codename["zh-CN"].includes('·') ? 'skin' : 'operator' // add link operator.link = operatorName diff --git a/libs/directory.js b/libs/directory.js index 122878f..2a0afd3 100644 --- a/libs/directory.js +++ b/libs/directory.js @@ -1,6 +1,11 @@ import path from 'path' import { writeSync, copy, rmdir } from './file.js' +/** + * TODO: + * 1. add voice config -> look up charword table + */ + export default function () { const targetFolder = path.join(__projetRoot, __config.folder.release, __config.folder.directory); 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")) filesToCopy.forEach((key) => { - const filename = `${__config.operators[key].filename}.json`; - copy(path.join(sourceFolder, key, 'assets.json'), path.join(targetFolder, filename)) + copy(path.join(sourceFolder, key, 'assets.json'), path.join(targetFolder, `${__config.operators[key].filename}.json`)) + if (__config.operators[key].portrait) { + copy(path.join(sourceFolder, key, 'assets_portrait.json'), path.join(targetFolder, `${__config.operators[key].portrait}.json`)) + } }) }