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
+
+
+## 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);