Skip to content

Commit 4adcf09

Browse files
authored
GPU accelerated maskeditor rendering (#6767)
## GPU accelerated brush engine for the mask editor - Full GPU acceleration using TypeGPU and type-safe shaders - Catmull-Rom Spline Smoothing - arc-length equidistant resampling - much improved performance, even for huge images - photoshop like opacity clamping for brush strokes - much improved soft brushes - fallback to CPU fully implemented, much improved CPU rendering features as well ### Tested Browsers - Chrome (fully supported) - Safari 26 (fully supported, prev versions CPU fallback) - Firefox (CPU fallback, flags needed for full support) https://github.com/user-attachments/assets/b7b5cb8a-2290-4a95-ae7d-180e11fccdb0 https://github.com/user-attachments/assets/4297aaa5-f249-499a-9b74-869677f1c73b https://github.com/user-attachments/assets/602b4783-3e2b-489e-bcb9-70534bcaac5e ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6767-GPU-accelerated-maskeditor-rendering-2b16d73d3650818cb294e1fca03f6169) by [Unito](https://www.unito.io)
1 parent 1dbb3fc commit 4adcf09

24 files changed

+2896
-360
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"@vitest/coverage-v8": "catalog:",
7676
"@vitest/ui": "catalog:",
7777
"@vue/test-utils": "catalog:",
78+
"@webgpu/types": "catalog:",
7879
"cross-env": "catalog:",
7980
"eslint": "catalog:",
8081
"eslint-config-prettier": "catalog:",
@@ -112,6 +113,7 @@
112113
"typescript": "catalog:",
113114
"typescript-eslint": "catalog:",
114115
"unplugin-icons": "catalog:",
116+
"unplugin-typegpu": "catalog:",
115117
"unplugin-vue-components": "catalog:",
116118
"uuid": "^11.1.0",
117119
"vite": "catalog:",
@@ -176,6 +178,7 @@
176178
"semver": "^7.7.2",
177179
"three": "^0.170.0",
178180
"tiptap-markdown": "^0.8.10",
181+
"typegpu": "catalog:",
179182
"vue": "catalog:",
180183
"vue-i18n": "catalog:",
181184
"vue-router": "catalog:",

pnpm-lock.yaml

Lines changed: 82 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ catalog:
4343
'@vue/test-utils': ^2.4.6
4444
'@vueuse/core': ^11.0.0
4545
'@vueuse/integrations': ^13.9.0
46+
'@webgpu/types': ^0.1.66
4647
algoliasearch: ^5.21.0
4748
axios: ^1.8.2
4849
cross-env: ^10.1.0
@@ -83,9 +84,11 @@ catalog:
8384
tailwindcss-primeui: ^0.6.1
8485
tsx: ^4.15.6
8586
tw-animate-css: ^1.3.8
87+
typegpu: ^0.8.2
8688
typescript: ^5.9.2
8789
typescript-eslint: ^8.44.0
8890
unplugin-icons: ^0.22.0
91+
unplugin-typegpu: 0.8.0
8992
unplugin-vue-components: ^0.28.0
9093
vite: ^5.4.19
9194
vite-plugin-dts: ^4.5.4

src/components/maskeditor/BrushCursor.vue

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
<script setup lang="ts">
2727
import { computed } from 'vue'
2828
29+
import {
30+
getEffectiveBrushSize,
31+
getEffectiveHardness
32+
} from '@/composables/maskeditor/brushUtils'
2933
import { BrushShape } from '@/extensions/core/maskeditor/types'
3034
import { useMaskEditorStore } from '@/stores/maskEditorStore'
3135
@@ -36,11 +40,14 @@ const { containerRef } = defineProps<{
3640
const store = useMaskEditorStore()
3741
3842
const brushOpacity = computed(() => {
39-
return store.brushVisible ? '1' : '0'
43+
return store.brushVisible ? 1 : 0
4044
})
4145
4246
const brushRadius = computed(() => {
43-
return store.brushSettings.size * store.zoomRatio
47+
const size = store.brushSettings.size
48+
const hardness = store.brushSettings.hardness
49+
const effectiveSize = getEffectiveBrushSize(size, hardness)
50+
return effectiveSize * store.zoomRatio
4451
})
4552
4653
const brushSize = computed(() => {
@@ -78,19 +85,26 @@ const gradientVisible = computed(() => {
7885
})
7986
8087
const gradientBackground = computed(() => {
88+
const size = store.brushSettings.size
8189
const hardness = store.brushSettings.hardness
90+
const effectiveSize = getEffectiveBrushSize(size, hardness)
91+
const effectiveHardness = getEffectiveHardness(size, hardness, effectiveSize)
8292
83-
if (hardness === 1) {
93+
if (effectiveHardness === 1) {
8494
return 'rgba(255, 0, 0, 0.5)'
8595
}
8696
87-
const midStop = hardness * 100
97+
const midStop = effectiveHardness * 100
8898
const outerStop = 100
99+
// Add an intermediate stop to approximate the squared falloff
100+
// At 50% of the fade region, squared falloff is 0.25 (relative to max)
101+
const fadeMidStop = midStop + (outerStop - midStop) * 0.5
89102
90103
return `radial-gradient(
91104
circle,
92105
rgba(255, 0, 0, 0.5) 0%,
93-
rgba(255, 0, 0, 0.25) ${midStop}%,
106+
rgba(255, 0, 0, 0.5) ${midStop}%,
107+
rgba(255, 0, 0, 0.125) ${fadeMidStop}%,
94108
rgba(255, 0, 0, 0) ${outerStop}%
95109
)`
96110
})

src/components/maskeditor/BrushSettingsPanel.vue

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
<SliderControl
5656
:label="t('maskEditor.thickness')"
5757
:min="1"
58-
:max="100"
58+
:max="500"
5959
:step="1"
6060
:model-value="store.brushSettings.size"
6161
@update:model-value="onThicknessChange"
@@ -80,12 +80,12 @@
8080
/>
8181

8282
<SliderControl
83-
:label="t('maskEditor.smoothingPrecision')"
83+
label="Stepsize"
8484
:min="1"
8585
:max="100"
8686
:step="1"
87-
:model-value="store.brushSettings.smoothingPrecision"
88-
@update:model-value="onSmoothingPrecisionChange"
87+
:model-value="store.brushSettings.stepSize"
88+
@update:model-value="onStepSizeChange"
8989
/>
9090
</div>
9191
</template>
@@ -119,8 +119,8 @@ const onHardnessChange = (value: number) => {
119119
store.setBrushHardness(value)
120120
}
121121
122-
const onSmoothingPrecisionChange = (value: number) => {
123-
store.setBrushSmoothingPrecision(value)
122+
const onStepSizeChange = (value: number) => {
123+
store.setBrushStepSize(value)
124124
}
125125
126126
const resetToDefault = () => {

0 commit comments

Comments
 (0)