diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 85d3c1f..4b52492 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -57,7 +57,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version. - releaseName: "App v__VERSION__" + releaseName: "v__VERSION__" releaseBody: "See the assets to download this version and install." releaseDraft: true prerelease: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..961b8a2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +## [0.1.0] +- 首次發布測試版 \ No newline at end of file diff --git a/README.md b/README.md index 0193b14..29498ec 100644 --- a/README.md +++ b/README.md @@ -1 +1,20 @@ -# Maple Character Creator \ No newline at end of file +# MapleSalon2 +Salon Simulator for Maplestory, preview all of hair cut or eyes color and all mix dye recipe. + +Now also able to simulate item dye! and preview all action. + +## Screenshot +![](./doc/preview_app.png) + +## Download +All download avaiable at [Releases page](./releases). + +Installer will only check `Webview` and extract the app exe to desire folder, fell free to move it anywhere. + +## Build it manually +Make sure you have environment below. +- Rust 1.70.0 or higher +- Node.js 16 or higher + +then +> npm run tauri build \ No newline at end of file diff --git a/bun.lockb b/bun.lockb deleted file mode 100644 index 7897e15..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/doc/preview_app.png b/doc/preview_app.png new file mode 100644 index 0000000..5bf03c3 Binary files /dev/null and b/doc/preview_app.png differ diff --git a/index.html b/index.html index 5d77742..055570e 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - + Tauri + Solid + Typescript App diff --git a/package-lock.json b/package-lock.json index 9fa2df0..d790e12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@tauri-apps/plugin-store": "^2.0.0-rc.0", "@tauri-apps/plugin-window-state": "^2.0.0-rc.0", "@zip.js/zip.js": "^2.7.47", + "anime4k-webgpu": "^1.0.0", "dom-to-image-more": "^3.3.1", "lucide-solid": "^0.394.0", "mingcute_icon": "^2.9.4", @@ -3457,6 +3458,14 @@ "node": ">=0.4.0" } }, + "node_modules/anime4k-webgpu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/anime4k-webgpu/-/anime4k-webgpu-1.0.0.tgz", + "integrity": "sha512-syZZyDRHYukFnDLxdES4AuMHzKwxlu5Lo52yhEEc1hwKQmvza8DGC62/VDPrrWOu4KArU5jh/0yFJQBwetGuxg==", + "peerDependencies": { + "@webgpu/types": "^0.1.38" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -8711,6 +8720,12 @@ "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true }, + "anime4k-webgpu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/anime4k-webgpu/-/anime4k-webgpu-1.0.0.tgz", + "integrity": "sha512-syZZyDRHYukFnDLxdES4AuMHzKwxlu5Lo52yhEEc1hwKQmvza8DGC62/VDPrrWOu4KArU5jh/0yFJQBwetGuxg==", + "requires": {} + }, "ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", diff --git a/package.json b/package.json index 3b9f10d..2dd20c6 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@tauri-apps/plugin-store": "^2.0.0-rc.0", "@tauri-apps/plugin-window-state": "^2.0.0-rc.0", "@zip.js/zip.js": "^2.7.47", + "anime4k-webgpu": "^1.0.0", "dom-to-image-more": "^3.3.1", "lucide-solid": "^0.394.0", "mingcute_icon": "^2.9.4", diff --git a/public/tauri.svg b/public/tauri.svg deleted file mode 100644 index 509dded..0000000 --- a/public/tauri.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 730b2f6..3a30426 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2158,7 +2158,7 @@ dependencies = [ [[package]] name = "maplesalon2" -version = "0.0.1" +version = "0.1.0" dependencies = [ "axum", "futures", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index bbc3e37..df4b2ec 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "maplesalon2" -version = "0.0.1" +version = "0.1.0" description = "MapleSalon2 - A tool for preview hair, face and item dye using MapleStory wz files." authors = ["Leo"] edition = "2021" diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index 6be5e50..8142ce0 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index e81bece..6042d11 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index a437dd5..7489420 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png index 0ca4f27..c74a375 100644 Binary files a/src-tauri/icons/Square107x107Logo.png and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png index b81f820..a64ecb1 100644 Binary files a/src-tauri/icons/Square142x142Logo.png and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png index 624c7bf..151527d 100644 Binary files a/src-tauri/icons/Square150x150Logo.png and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png index c021d2b..838449a 100644 Binary files a/src-tauri/icons/Square284x284Logo.png and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png index 6219700..118058c 100644 Binary files a/src-tauri/icons/Square30x30Logo.png and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png index f9bc048..ec98fda 100644 Binary files a/src-tauri/icons/Square310x310Logo.png and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png index d5fbfb2..4ba6e0f 100644 Binary files a/src-tauri/icons/Square44x44Logo.png and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png index 63440d7..9eca674 100644 Binary files a/src-tauri/icons/Square71x71Logo.png and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png index f3f705a..9656b5b 100644 Binary files a/src-tauri/icons/Square89x89Logo.png and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png index 4556388..a463879 100644 Binary files a/src-tauri/icons/StoreLogo.png and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 12a5bce..b69070b 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index b3636e4..07ea487 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index e1cd261..58bb80f 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6d5951d..1e3d567 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "productName": "MapleSalon2", - "version": "0.0.0", - "identifier": "com.maplecreator.io", + "version": "0.1.0", + "identifier": "com.maplesalon.io", "build": { "beforeDevCommand": "npm run dev", "devUrl": "http://localhost:1420", @@ -29,6 +29,14 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" - ] + ], + "windows": { + "nsis": { + "languages": ["English", "TradChinese"] + }, + "wix": { + "language": ["en-US", "zh-TW", "zh-CN"] + } + } } } diff --git a/src/assets/icon.png b/src/assets/icon.png new file mode 100644 index 0000000..5d002d3 Binary files /dev/null and b/src/assets/icon.png differ diff --git a/src/assets/logo.svg b/src/assets/logo.svg deleted file mode 100644 index 025aa30..0000000 --- a/src/assets/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/CharacterPreview/Character.tsx b/src/components/CharacterPreview/Character.tsx index c12c511..c61bd6e 100644 --- a/src/components/CharacterPreview/Character.tsx +++ b/src/components/CharacterPreview/Character.tsx @@ -1,7 +1,6 @@ import { onMount, onCleanup, createEffect, createSignal } from 'solid-js'; import type { ReadableAtom } from 'nanostores'; - import { $preferRenderer } from '@/store/renderer'; import type { CharacterData } from '@/store/character/store'; import { @@ -12,6 +11,10 @@ import { updateCenter, updateZoom, } from '@/store/previewZoom'; +import { + resetUpscaleSource, + setUpscaleSource, +} from '@/store/expirement/upscale'; import { usePureStore } from '@/store'; import { Application } from 'pixi.js'; @@ -43,7 +46,7 @@ export const CharacterView = (props: CharacterViewProps) => { height: 340, background: 0x000000, backgroundAlpha: 0, - antialias: true, + // antialias: true, preference: $preferRenderer.get(), }); viewport = new ZoomContainer(app, { @@ -77,6 +80,9 @@ export const CharacterView = (props: CharacterViewProps) => { container.appendChild(app.canvas); viewport.addChild(ch); app.stage.addChild(viewport); + if (props.target === 'preview') { + setUpscaleSource(app.canvas); + } setIsInit(true); } @@ -88,6 +94,9 @@ export const CharacterView = (props: CharacterViewProps) => { onCleanup(() => { ch.reset(); app.destroy(); + if (props.target === 'preview') { + setUpscaleSource(app.canvas); + } }); createEffect(() => { diff --git a/src/components/CharacterPreview/CharacterScene.tsx b/src/components/CharacterPreview/CharacterScene.tsx index 89cbe6e..c96c023 100644 --- a/src/components/CharacterPreview/CharacterScene.tsx +++ b/src/components/CharacterPreview/CharacterScene.tsx @@ -8,13 +8,18 @@ import { $previewCharacter, $sceneCustomColorStyle, } from '@/store/character/selector'; -import { $showPreviousCharacter } from '@/store/trigger'; +import { + $showPreviousCharacter, + $showUpscaledCharacter, +} from '@/store/trigger'; import LoaderCircle from 'lucide-solid/icons/loader-circle'; import ChevronRightIcon from 'lucide-solid/icons/chevron-right'; import { CharacterView } from './Character'; +import { UpscaleCharacter } from './UpscaleCharacter'; import { CharacterSceneSelection } from './CharacterSceneSelection'; import { ShowPreviousSwitch } from './ShowPreviousSwitch'; +import { ShowUpscaleSwitch } from './ShowUpscaleSwitch'; import { ZoomControl } from './ZoomControl'; import { PreviewSceneBackground } from '@/const/scene'; @@ -26,6 +31,7 @@ export const CharacterScene = () => { const scene = useStore($currentScene); const customColorStyle = useStore($sceneCustomColorStyle); const isShowComparison = useStore($showPreviousCharacter); + const isShowUpscale = useStore($showUpscaledCharacter); function handleLoad() { setIsLoading(true); @@ -83,8 +89,12 @@ export const CharacterScene = () => { target="preview" isLockInteraction={isLockInteraction()} /> + + + + @@ -144,6 +154,8 @@ const TopTool = styled('div', { transition: 'opacity 0.2s', backgroundColor: 'bg.default', boxShadow: 'md', + display: 'flex', + gap: 2, _hover: { opacity: 1, }, diff --git a/src/components/CharacterPreview/ShowUpscaleSwitch.tsx b/src/components/CharacterPreview/ShowUpscaleSwitch.tsx new file mode 100644 index 0000000..a174c84 --- /dev/null +++ b/src/components/CharacterPreview/ShowUpscaleSwitch.tsx @@ -0,0 +1,24 @@ +import { Show } from 'solid-js'; +import { useStore } from '@nanostores/solid'; + +import { $enableExperimentalUpscale } from '@/store/settingDialog'; +import { $showUpscaledCharacter } from '@/store/trigger'; + +import { Switch, type ChangeDetails } from '@/components/ui/switch'; + +export const ShowUpscaleSwitch = () => { + const isEnable = useStore($enableExperimentalUpscale); + const isShow = useStore($showUpscaledCharacter); + + function handleChange(details: ChangeDetails) { + $showUpscaledCharacter.set(details.checked); + } + + return ( + + + 顯示高清化 + + + ); +}; diff --git a/src/components/CharacterPreview/UpscaleCharacter.tsx b/src/components/CharacterPreview/UpscaleCharacter.tsx new file mode 100644 index 0000000..14d5581 --- /dev/null +++ b/src/components/CharacterPreview/UpscaleCharacter.tsx @@ -0,0 +1,91 @@ +import { onMount, onCleanup, createEffect, createSignal } from 'solid-js'; + +import { $upscaleSource } from '@/store/expirement/upscale'; +import { usePureStore } from '@/store'; + +import { + PipelineType, + createGpuDevice, + createRendererWithPipelines, + type GpuResource, + type Anime4KRenderer, + type PipelineOption, +} from '@/renderer/filter/anime4k'; + +import { toaster } from '@/components/GlobalToast'; + +export const UpscaleCharacter = () => { + const source = usePureStore($upscaleSource); + const [isInit, setIsInit] = createSignal(false); + let canvasRef!: HTMLCanvasElement; + let requestId: number | null = null; + let gpuResource: GpuResource | null = null; + let renderer: Anime4KRenderer | null = null; + const useType: (PipelineOption | PipelineType)[] = [ + { + pipeline: PipelineType.BilateralMean, + params: { + strength: 0.2, + strength2: 1, + }, + }, + { + pipeline: PipelineType.Dog, + params: { + strength: 2, + }, + }, + PipelineType.CNNSoftVL, + ]; + + onMount(async () => { + const soruceCanvas = source(); + if (!soruceCanvas) { + return; + } + try { + gpuResource = await createGpuDevice(canvasRef); + + renderer = await createRendererWithPipelines(soruceCanvas, canvasRef, { + ...gpuResource, + pipelines: useType, + }); + setIsInit(true); + } catch (e) { + console.error(e); + toaster.error({ + title: '無法初始化 WebGPU', + description: '請確保您的 Webview 版本支援 WebGPU。', + }); + } + }); + + onCleanup(() => { + if (requestId) { + cancelAnimationFrame(requestId); + } + if (gpuResource) { + gpuResource.context.unconfigure(); + } + }); + + createEffect(async () => { + if (!isInit()) { + return; + } + const sourceCanvas = source(); + function frame() { + if (renderer && sourceCanvas) { + renderer.updateFrameTexture(); + renderer.render(); + } + } + function loop() { + frame(); + requestId = requestAnimationFrame(loop); + } + requestId = requestAnimationFrame(loop); + }); + + return ; +}; diff --git a/src/components/dialog/SettingDialog/RenderSetting/UpscaleSwitch.tsx b/src/components/dialog/SettingDialog/RenderSetting/UpscaleSwitch.tsx new file mode 100644 index 0000000..fd5b938 --- /dev/null +++ b/src/components/dialog/SettingDialog/RenderSetting/UpscaleSwitch.tsx @@ -0,0 +1,31 @@ +import { useStore } from '@nanostores/solid'; + +import { + $enableExperimentalUpscale, + setEnableExperimentalUpscale, +} from '@/store/settingDialog'; + +import { HStack } from 'styled-system/jsx/hstack'; +import { Text } from '@/components/ui/text'; +import { Switch, type ChangeDetails } from '@/components/ui/switch'; +import { SettingTooltip } from '@/components/dialog/SettingDialog/SettingTooltip'; + +export const UpscaleSwitch = () => { + const enableExperimentalUpscale = useStore($enableExperimentalUpscale); + + function handleChange(details: ChangeDetails) { + setEnableExperimentalUpscale(details.checked); + } + + return ( + + + 實驗性高清預覽 + + + + ); +}; diff --git a/src/components/dialog/SettingDialog/RenderSetting/index.tsx b/src/components/dialog/SettingDialog/RenderSetting/index.tsx index f46dcbd..c13a37e 100644 --- a/src/components/dialog/SettingDialog/RenderSetting/index.tsx +++ b/src/components/dialog/SettingDialog/RenderSetting/index.tsx @@ -7,6 +7,7 @@ import { DefaultCharacterRenderingSwitch } from './DefaultCharacterRenderingSwit import { ShowItemGenderSwitch } from './ShowItemGenderSwitch'; import { ShowItemDyeableSwitch } from './ShowItemDyeableSwitch'; import { ItemEffectPreview } from './ItemEffectPreview'; +import { UpscaleSwitch } from './UpscaleSwitch'; import { SettingTooltip } from '@/components/dialog/SettingDialog/SettingTooltip'; export const RenderSetting = () => { @@ -26,6 +27,9 @@ export const RenderSetting = () => { + + + ); }; diff --git a/src/renderer/filter/anime4k/fullscreenTexturedQuad.wgsl b/src/renderer/filter/anime4k/fullscreenTexturedQuad.wgsl new file mode 100644 index 0000000..c0237cd --- /dev/null +++ b/src/renderer/filter/anime4k/fullscreenTexturedQuad.wgsl @@ -0,0 +1,30 @@ +struct VertexOutput { + @builtin(position) Position : vec4, + @location(0) fragUV : vec2, +} + +@vertex +fn vert_main(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { + const pos = array( + vec2( 1.0, 1.0), + vec2( 1.0, -1.0), + vec2(-1.0, -1.0), + vec2( 1.0, 1.0), + vec2(-1.0, -1.0), + vec2(-1.0, 1.0), + ); + + const uv = array( + vec2(1.0, 0.0), + vec2(1.0, 1.0), + vec2(0.0, 1.0), + vec2(1.0, 0.0), + vec2(0.0, 1.0), + vec2(0.0, 0.0), + ); + + var output : VertexOutput; + output.Position = vec4(pos[VertexIndex], 0.0, 1.0); + output.fragUV = uv[VertexIndex]; + return output; +} \ No newline at end of file diff --git a/src/renderer/filter/anime4k/index.ts b/src/renderer/filter/anime4k/index.ts new file mode 100644 index 0000000..0419db2 --- /dev/null +++ b/src/renderer/filter/anime4k/index.ts @@ -0,0 +1,311 @@ +import type { Anime4KPipeline } from 'anime4k-webgpu'; +import fullscreenTexturedQuadVert from './fullscreenTexturedQuad.wgsl'; +import sampleExternalTextureFrag from './sampleExternalTexture.wgsl'; + +type Anime4kPipelineConstructor = new (desc: { + device: GPUDevice; + inputTexture: GPUTexture; + nativeDimensions?: { width: number; height: number }; + targetDimensions?: { width: number; height: number }; +}) => Anime4KPipeline; + +export enum PipelineType { + /** + * deblur, must set param: strength + * + * @param strength Deblur Strength (0.1 - 15.0) + */ + Dog = 'Dog', + /** + * denoise, must set param: strength, strength2 + * + * @param strength Itensity Sigma (0.1 - 2.0) + * @param strength2 Spatial Sigma (1 - 15) + */ + BilateralMean = 'BilateralMean', + + /** restore */ + CNNM = 'CNNM', + /** restore */ + CNNSoftM = 'CNNSoftM', + /** restore */ + CNNSoftVL = 'CNNSoftVL', + /** restore */ + CNNVL = 'CNNVL', + /** restore */ + CNNUL = 'CNNUL', + /** restore */ + GANUUL = 'GANUUL', + + /** upscale */ + CNNx2M = 'CNNx2M', + /** upscale */ + CNNx2VL = 'CNNx2VL', + /** upscale */ + DenoiseCNNx2VL = 'DenoiseCNNx2VL', + /** upscale */ + CNNx2UL = 'CNNx2UL', + /** upscale */ + GANx3L = 'GANx3L', + /** upscale */ + GANx4UUL = 'GANx4UUL', + + ClampHighlights = 'ClampHighlights', + DepthToSpace = 'DepthToSpace', + + /* anime4k preset @see https://github.com/bloc97/Anime4K/blob/master/md/GLSL_Instructions_Advanced.md */ + /** Restore -> Upscale -> Upscale */ + ModeA = 'ModeA', + /** Restore_Soft -> Upscale -> Upscale */ + ModeB = 'ModeB', + /** Upscale_Denoise -> Upscale */ + ModeC = 'ModeC', + /** Restore -> Upscale -> Restore -> Upscale */ + ModeAA = 'ModeAA', + /** Restore_Soft -> Upscale -> Restore_Soft -> Upscale */ + ModeBB = 'ModeBB', + /** Upscale_Denoise -> Restore -> Upscale */ + ModeCA = 'ModeCA', +} + +const PipelineMap: Record Promise> = { + [PipelineType.Dog]: () => import('anime4k-webgpu').then((m) => m.DoG), + [PipelineType.BilateralMean]: () => + import('anime4k-webgpu').then((m) => m.BilateralMean), + + [PipelineType.CNNM]: () => import('anime4k-webgpu').then((m) => m.CNNM), + [PipelineType.CNNSoftM]: () => + import('anime4k-webgpu').then((m) => m.CNNSoftM), + [PipelineType.CNNSoftVL]: () => + import('anime4k-webgpu').then((m) => m.CNNSoftVL), + [PipelineType.CNNVL]: () => import('anime4k-webgpu').then((m) => m.CNNVL), + [PipelineType.CNNUL]: () => import('anime4k-webgpu').then((m) => m.CNNUL), + [PipelineType.GANUUL]: () => import('anime4k-webgpu').then((m) => m.GANUUL), + + [PipelineType.CNNx2M]: () => import('anime4k-webgpu').then((m) => m.CNNx2M), + [PipelineType.CNNx2VL]: () => import('anime4k-webgpu').then((m) => m.CNNx2VL), + [PipelineType.DenoiseCNNx2VL]: () => + import('anime4k-webgpu').then((m) => m.DenoiseCNNx2VL), + [PipelineType.CNNx2UL]: () => import('anime4k-webgpu').then((m) => m.CNNx2UL), + [PipelineType.GANx3L]: () => import('anime4k-webgpu').then((m) => m.GANx3L), + [PipelineType.GANx4UUL]: () => + import('anime4k-webgpu').then((m) => m.GANx4UUL), + + [PipelineType.ClampHighlights]: () => + import('anime4k-webgpu').then((m) => m.ClampHighlights), + [PipelineType.DepthToSpace]: () => + import('anime4k-webgpu').then((m) => m.DepthToSpace), + + [PipelineType.ModeA]: () => import('anime4k-webgpu').then((m) => m.ModeA), + [PipelineType.ModeB]: () => import('anime4k-webgpu').then((m) => m.ModeB), + [PipelineType.ModeC]: () => import('anime4k-webgpu').then((m) => m.ModeC), + [PipelineType.ModeAA]: () => import('anime4k-webgpu').then((m) => m.ModeAA), + [PipelineType.ModeBB]: () => import('anime4k-webgpu').then((m) => m.ModeBB), + [PipelineType.ModeCA]: () => import('anime4k-webgpu').then((m) => m.ModeCA), +}; + +export type GpuResource = { + device: GPUDevice; + context: GPUCanvasContext; +}; + +export async function createGpuDevice(canvas: HTMLCanvasElement) { + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) { + throw new Error('WebGPU is not supported'); + } + const device = await adapter.requestDevice(); + const context = canvas.getContext('webgpu') as GPUCanvasContext; + const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + context.configure({ + device, + format: presentationFormat, + alphaMode: 'premultiplied', + }); + + return { device, context }; +} + +export interface PipelineOption { + pipeline: PipelineType; + params: Record; +} + +export interface CreateRendererOptions { + device: GPUDevice; + context: GPUCanvasContext; + pipelines: (PipelineType | PipelineOption)[]; +} + +export type Anime4KRenderer = { + render: () => void; + updateFrameTexture: () => void; +}; + +export async function createRendererWithPipelines( + sourceCanvas: HTMLCanvasElement, + destCanvas: HTMLCanvasElement, + options: CreateRendererOptions, +) { + const { device, context, pipelines } = options; + const { width: WIDTH, height: HEIGHT } = sourceCanvas; + + destCanvas.width = WIDTH; + destCanvas.height = HEIGHT; + + const mainTexture = device.createTexture({ + size: [WIDTH, HEIGHT, 1], + format: 'rgba16float', + usage: + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + + const loadPipelines = await Promise.all( + pipelines.map((pipeline) => { + if (typeof pipeline === 'string') { + return PipelineMap[pipeline](); + } + return PipelineMap[pipeline.pipeline](); + }), + ); + + const pipelineChain: Anime4KPipeline[] = loadPipelines.reduce( + (chain: Anime4KPipeline[], pipeline, index) => { + const inputTexture = + index === 0 ? mainTexture : chain[index - 1].getOutputTexture(); + const pipeClass = pipeline as Anime4kPipelineConstructor; + const pipeOption = pipelines[index]; + const pipe = new pipeClass({ + device, + inputTexture, + nativeDimensions: { width: WIDTH, height: HEIGHT }, + targetDimensions: { width: WIDTH, height: HEIGHT }, + }); + if (typeof pipeOption !== 'string' && pipeOption.params) { + for (const [key, value] of Object.entries(pipeOption.params)) { + pipe.updateParam(key, value); + } + } + chain.push(pipe); + return chain; + }, + [] as Anime4KPipeline[], + ); + + const lastPipeline = pipelineChain[pipelineChain.length - 1]; + // const lastTexture = lastPipeline.getOutputTexture(); + // destCanvas.width = lastTexture.width; + // destCanvas.height = lastTexture.height; + + // render pipeline setups + const renderBindGroupLayout = device.createBindGroupLayout({ + label: 'Render Bind Group Layout', + entries: [ + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + sampler: {}, + }, + { + binding: 2, + visibility: GPUShaderStage.FRAGMENT, + texture: {}, + }, + ], + }); + + const renderPipelineLayout = device.createPipelineLayout({ + label: 'Render Pipeline Layout', + bindGroupLayouts: [renderBindGroupLayout], + }); + + const renderPipeline = device.createRenderPipeline({ + layout: renderPipelineLayout, + vertex: { + module: device.createShaderModule({ + code: fullscreenTexturedQuadVert, + }), + entryPoint: 'vert_main', + }, + fragment: { + module: device.createShaderModule({ + code: sampleExternalTextureFrag, + }), + entryPoint: 'main', + targets: [ + { + format: navigator.gpu.getPreferredCanvasFormat(), + }, + ], + }, + primitive: { + topology: 'triangle-list', + }, + }); + + const sampler = device.createSampler({ + magFilter: 'linear', + minFilter: 'linear', + }); + + const renderBindGroup = device.createBindGroup({ + layout: renderBindGroupLayout, + entries: [ + { + binding: 1, + resource: sampler, + }, + { + binding: 2, + resource: lastPipeline.getOutputTexture().createView(), + }, + ], + }); + + function updateFrameTexture() { + device.queue.copyExternalImageToTexture( + { source: sourceCanvas }, + { texture: mainTexture }, + [WIDTH, HEIGHT], + ); + } + function render() { + let textureView: GPUTextureView | undefined; + try { + textureView = context.getCurrentTexture().createView(); + } catch (e) { + console.info('render canceled'); + } + if (!textureView) { + return; + } + const commandEncoder = device.createCommandEncoder(); + for (const pipe of pipelineChain) { + pipe.pass(commandEncoder); + } + const passEncoder = commandEncoder.beginRenderPass({ + colorAttachments: [ + { + view: textureView, + clearValue: { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + passEncoder.setPipeline(renderPipeline); + passEncoder.setBindGroup(0, renderBindGroup); + passEncoder.draw(6); + passEncoder.end(); + device.queue.submit([commandEncoder.finish()]); + } + + return { render, updateFrameTexture }; +} diff --git a/src/renderer/filter/anime4k/sampleExternalTexture.wgsl b/src/renderer/filter/anime4k/sampleExternalTexture.wgsl new file mode 100644 index 0000000..2305e0b --- /dev/null +++ b/src/renderer/filter/anime4k/sampleExternalTexture.wgsl @@ -0,0 +1,7 @@ +@group(0) @binding(1) var mySampler: sampler; +@group(0) @binding(2) var myTexture: texture_2d; + +@fragment +fn main(@location(0) fragUV : vec2f) -> @location(0) vec4f { + return textureSampleBaseClampToEdge(myTexture, mySampler, fragUV); +} \ No newline at end of file diff --git a/src/store/expirement/upscale.ts b/src/store/expirement/upscale.ts new file mode 100644 index 0000000..e5fbea9 --- /dev/null +++ b/src/store/expirement/upscale.ts @@ -0,0 +1,10 @@ +import { atom } from 'nanostores'; + +export const $upscaleSource = atom(null); + +export function setUpscaleSource(canvas: HTMLCanvasElement | null) { + $upscaleSource.set(canvas); +} +export function resetUpscaleSource() { + $upscaleSource.set(null); +} diff --git a/src/store/settingDialog.ts b/src/store/settingDialog.ts index a4c8766..758c2a4 100644 --- a/src/store/settingDialog.ts +++ b/src/store/settingDialog.ts @@ -39,6 +39,7 @@ export interface AppSetting extends Record { defaultCharacterRendering: boolean; showItemGender: boolean; showItemDyeable: boolean; + enableExperimentalUpscale: boolean; } const DEFAULT_SETTING: AppSetting = { @@ -50,6 +51,7 @@ const DEFAULT_SETTING: AppSetting = { defaultCharacterRendering: false, showItemGender: true, showItemDyeable: true, + enableExperimentalUpscale: false, }; export const $appSetting = deepMap(DEFAULT_SETTING); @@ -81,6 +83,10 @@ export const $showItemDyeable = computed( $appSetting, (setting) => setting.showItemDyeable, ); +export const $enableExperimentalUpscale = computed( + $appSetting, + (setting) => setting.enableExperimentalUpscale, +); /* action */ export async function initializeSavedSetting() { @@ -152,3 +158,6 @@ export function setShowItemGender(value: boolean) { export function setShowItemDyeable(value: boolean) { $appSetting.setKey('showItemDyeable', value); } +export function setEnableExperimentalUpscale(value: boolean) { + $appSetting.setKey('enableExperimentalUpscale', value); +} diff --git a/src/store/trigger.ts b/src/store/trigger.ts index 9b035a1..015b402 100644 --- a/src/store/trigger.ts +++ b/src/store/trigger.ts @@ -12,6 +12,8 @@ export const $sceneSelectionOpen = atom(false); export const $showPreviousCharacter = atom(false); +export const $showUpscaledCharacter = atom(false); + export const $confirmDialogOpen = atom(false); export const $settingDialogOpen = atom(false);