wgpu-kit 0.9.10 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +8 -1
  2. package/dist/core/buffer.d.ts +1 -1
  3. package/dist/core/buffer.js +109 -0
  4. package/dist/core/context.d.ts +2 -0
  5. package/dist/core/context.js +62 -0
  6. package/dist/core/errors.js +42 -0
  7. package/dist/core/kernel.js +197 -0
  8. package/dist/core/layout.d.ts +5 -1
  9. package/dist/core/layout.js +69 -0
  10. package/dist/core/pingpong.js +61 -0
  11. package/dist/core/raw.js +38 -0
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +11 -484
  14. package/dist/interop/three.js +29 -0
  15. package/dist/media.js +70 -0
  16. package/dist/observe.d.ts +19 -0
  17. package/dist/observe.js +76 -0
  18. package/dist/packs/fields/index.js +230 -0
  19. package/dist/packs/image/index.js +250 -0
  20. package/dist/packs/life/boids.js +367 -0
  21. package/dist/packs/life/index.js +8 -0
  22. package/dist/packs/life/map.js +110 -0
  23. package/dist/packs/life/physarum.js +228 -0
  24. package/dist/packs/life/tentacles.js +238 -0
  25. package/dist/packs/life/turing.js +203 -0
  26. package/dist/packs/particles/config.d.ts +1 -0
  27. package/dist/packs/particles/config.js +36 -0
  28. package/dist/packs/particles/grid.js +248 -0
  29. package/dist/packs/particles/index.js +361 -0
  30. package/dist/packs/particles/presets.js +75 -0
  31. package/dist/packs/particles/render.js +116 -0
  32. package/dist/packs/particles/wgsl.js +175 -0
  33. package/dist/react/index.js +36 -0
  34. package/dist/vite.js +27 -28
  35. package/package.json +1 -1
  36. package/dist/fields.js +0 -585
  37. package/dist/image.js +0 -318
  38. package/dist/life.js +0 -1407
  39. package/dist/particles.js +0 -1245
  40. package/dist/react.js +0 -1289
  41. package/dist/three.js +0 -33
@@ -0,0 +1,230 @@
1
+ import { GpuContext } from "../../core/context.js";
2
+ import { Buffer } from "../../core/buffer.js";
3
+ import { PingPong } from "../../core/pingpong.js";
4
+ import { CompileError } from "../../core/errors.js";
5
+ import { MapRenderer } from "../life/map.js";
6
+ import { mulberry32 } from "../particles/presets.js";
7
+ const FIELD_FNS = {
8
+ // 绕心漩涡:切向速度,离心得越远越慢
9
+ vortex: `
10
+ fn fieldAt(p: vec2f, t: f32) -> vec2f {
11
+ let r = length(p) + 0.12;
12
+ return vec2f(-p.y, p.x) / r * 1.4;
13
+ }`,
14
+ // curl noise:值噪声的旋度,无散度,像真实的湍流
15
+ curl: `
16
+ fn hash(p: vec2f) -> f32 {
17
+ return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453);
18
+ }
19
+ fn vnoise(p: vec2f) -> f32 {
20
+ let i = floor(p);
21
+ let f = fract(p);
22
+ let u = f * f * (3.0 - 2.0 * f);
23
+ return mix(mix(hash(i), hash(i + vec2f(1.0, 0.0)), u.x), mix(hash(i + vec2f(0.0, 1.0)), hash(i + vec2f(1.0, 1.0)), u.x), u.y);
24
+ }
25
+ fn fieldAt(p: vec2f, t: f32) -> vec2f {
26
+ let s = 2.2;
27
+ let e = 0.01;
28
+ let n1 = vnoise(p * s + vec2f(0.0, t * 0.15));
29
+ let n2 = vnoise(p * s + vec2f(17.3, t * 0.15));
30
+ let dx = vnoise(p * s + vec2f(e, 0.0) + vec2f(0.0, t * 0.15)) - n1;
31
+ let dy = vnoise(p * s + vec2f(0.0, e) + vec2f(17.3, t * 0.15)) - n2;
32
+ return normalize(vec2f(dy, -dx) / e + vec2f(1e-5));
33
+ }`,
34
+ // 双涡:左右反向旋转,中间有剪切层
35
+ twin: `
36
+ fn fieldAt(p: vec2f, t: f32) -> vec2f {
37
+ let s = select(-1.0, 1.0, p.x > 0.0);
38
+ let c = vec2f(0.55 * s, 0.0);
39
+ let r = length(p - c) + 0.1;
40
+ let swirl = vec2f(-(p - c).y, (p - c).x) / r;
41
+ return swirl * s * 1.3 + vec2f(0.0, sin(t * 0.4) * 0.2);
42
+ }`,
43
+ };
44
+ const AWG = 32;
45
+ export async function flow(config = {}) {
46
+ const { count: N = 131_072, mapSize = 1024, field = 'curl', speed = 0.004, decay = 0.045, deposit = 1.0, seed = 'flow', colormap = 'ice', } = config;
47
+ const seedHash = typeof seed === 'string' ? hashStr(seed) : (seed ?? 11);
48
+ const ctx = await GpuContext.get();
49
+ const device = ctx.device;
50
+ const posBuf = await Buffer.create('vec2f', N);
51
+ {
52
+ const rand = mulberry32(seedHash);
53
+ const p = new Float32Array(N * 2);
54
+ for (let i = 0; i < N; i++) {
55
+ p[i * 2] = rand() * 1.8 - 0.9;
56
+ p[i * 2 + 1] = rand() * 1.8 - 0.9;
57
+ }
58
+ posBuf.write(p);
59
+ }
60
+ const trail = await PingPong.create({ t: 'f32' }, mapSize * mapSize);
61
+ const trailA = trail.current.t;
62
+ const trailB = trail.other.t;
63
+ const uniform = device.createBuffer({ size: AWG, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
64
+ // 持久参数对象:每次全量写入(首版只写部分字段,未写字段被清零 → deposit=0 的教训)
65
+ const u = { count: N, pad: 0, speed, worldHalf: 1.0, deposit, time: 0, p0: 0, p1: 0 };
66
+ const writeUniform = () => {
67
+ const b = new ArrayBuffer(AWG);
68
+ const v = new DataView(b);
69
+ v.setUint32(0, u.count, true);
70
+ v.setUint32(4, u.pad, true);
71
+ v.setFloat32(8, u.speed, true);
72
+ v.setFloat32(12, u.worldHalf, true);
73
+ v.setFloat32(16, u.deposit, true);
74
+ v.setFloat32(20, u.time, true);
75
+ device.queue.writeBuffer(uniform, 0, b);
76
+ };
77
+ writeUniform();
78
+ const diffuseUniform = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
79
+ device.queue.writeBuffer(diffuseUniform, 0, new Uint32Array([mapSize, mapSize]));
80
+ device.queue.writeBuffer(diffuseUniform, 8, new Float32Array([1 - decay, 0]));
81
+ const compile = async (code, label) => {
82
+ const m = device.createShaderModule({ code, label });
83
+ const info = await m.getCompilationInfo();
84
+ const errors = info.messages.filter((x) => x.type === 'error');
85
+ if (errors.length > 0)
86
+ throw new CompileError(label, errors.map((x) => ({ line: x.lineNum, msg: x.message })), 0);
87
+ return m;
88
+ };
89
+ const mAdvect = await compile(advectWgsl(FIELD_FNS[field], mapSize), `fields-advect(${field})`);
90
+ const mDiffuse = await compile(diffuseWgsl(), 'fields-diffuse');
91
+ const pAdvect = device.createComputePipeline({ layout: 'auto', compute: { module: mAdvect, entryPoint: 'main' } });
92
+ const pDiffuse = device.createComputePipeline({ layout: 'auto', compute: { module: mDiffuse, entryPoint: 'main' } });
93
+ const bgAdvect = (t) => device.createBindGroup({
94
+ layout: pAdvect.getBindGroupLayout(0),
95
+ entries: [
96
+ { binding: 0, resource: { buffer: uniform } },
97
+ { binding: 1, resource: { buffer: posBuf.gpuBuffer } },
98
+ { binding: 2, resource: { buffer: t.gpuBuffer } },
99
+ ],
100
+ });
101
+ const bgDiffuse = (read, write) => device.createBindGroup({
102
+ layout: pDiffuse.getBindGroupLayout(0),
103
+ entries: [
104
+ { binding: 0, resource: { buffer: diffuseUniform } },
105
+ { binding: 1, resource: { buffer: read.gpuBuffer } },
106
+ { binding: 2, resource: { buffer: write.gpuBuffer } },
107
+ ],
108
+ });
109
+ let renderer = null;
110
+ let frame = 0;
111
+ let time = 0;
112
+ let lastFps = 0;
113
+ let fFrames = 0;
114
+ let fAcc = 0;
115
+ let fLast = performance.now();
116
+ return {
117
+ async attach(canvas) {
118
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
119
+ canvas.width = Math.max(1, Math.floor(canvas.clientWidth * dpr));
120
+ canvas.height = Math.max(1, Math.floor(canvas.clientHeight * dpr));
121
+ renderer = await MapRenderer.create(canvas, { width: mapSize, height: mapSize, maxV: 5.0, gamma: 0.65, colormap });
122
+ },
123
+ tick() {
124
+ time += 1 / 60;
125
+ u.time = time;
126
+ writeUniform();
127
+ const readT = trail.current.t;
128
+ const writeT = trail.other.t;
129
+ const enc = device.createCommandEncoder();
130
+ const pass = enc.beginComputePass();
131
+ pass.setPipeline(pAdvect);
132
+ pass.setBindGroup(0, bgAdvect(readT));
133
+ pass.dispatchWorkgroups(Math.ceil(N / 64));
134
+ pass.setPipeline(pDiffuse);
135
+ pass.setBindGroup(0, bgDiffuse(readT, writeT));
136
+ pass.dispatchWorkgroups(Math.ceil((mapSize * mapSize) / 64));
137
+ pass.end();
138
+ device.queue.submit([enc.finish()]);
139
+ renderer?.render(writeT);
140
+ trail.swap();
141
+ frame++;
142
+ fFrames++;
143
+ const now = performance.now();
144
+ fAcc += now - fLast;
145
+ fLast = now;
146
+ if (fAcc >= 500) {
147
+ lastFps = fFrames / (fAcc / 1000);
148
+ fFrames = 0;
149
+ fAcc = 0;
150
+ }
151
+ },
152
+ stats() { return { fps: lastFps }; },
153
+ async sampleTrail() { return (await trail.current.t.read()); },
154
+ destroy() {
155
+ posBuf.destroy();
156
+ trail.destroy();
157
+ uniform.destroy();
158
+ diffuseUniform.destroy();
159
+ },
160
+ };
161
+ }
162
+ function advectWgsl(fieldFn, mapSize) {
163
+ return /* wgsl */ `
164
+ struct Params {
165
+ count: u32, _pad: u32,
166
+ speed: f32, worldHalf: f32, deposit: f32, time: f32,
167
+ _p0: f32, _p1: f32,
168
+ };
169
+ @group(0) @binding(0) var<uniform> params: Params;
170
+ @group(0) @binding(1) var<storage, read_write> pos: array<vec2f>;
171
+ @group(0) @binding(2) var<storage, read_write> trail: array<f32>;
172
+
173
+ const TRAIL_W: u32 = ${mapSize}u;
174
+ ${fieldFn}
175
+
176
+ @compute @workgroup_size(64)
177
+ fn main(@builtin(global_invocation_id) gid: vec3u) {
178
+ let i = gid.x;
179
+ if (i >= params.count) { return; }
180
+ let p = pos[i];
181
+ let v = fieldAt(p, params.time) * params.speed;
182
+ var np = p + v;
183
+ let h = params.worldHalf * 0.98;
184
+ if (abs(np.x) > h || abs(np.y) > h) {
185
+ // 出界重生:随机撒回(确定性 hash,免额外随机源)
186
+ let r1 = fract(sin(f32(i) * 12.9898 + params.time * 78.233) * 43758.5453);
187
+ let r2 = fract(sin(f32(i) * 78.233 + params.time * 12.9898) * 24634.6345);
188
+ np = vec2f(r1, r2) * 1.8 - 0.9;
189
+ }
190
+ pos[i] = np;
191
+ let w = TRAIL_W;
192
+ let tx = min(u32((np.x * 0.5 + 0.5) * f32(w)), w - 1u);
193
+ let ty = min(u32((np.y * 0.5 + 0.5) * f32(w)), w - 1u);
194
+ let t = ty * w + tx;
195
+ trail[t] = trail[t] + params.deposit;
196
+ }
197
+ `;
198
+ }
199
+ function diffuseWgsl() {
200
+ return /* wgsl */ `
201
+ @group(0) @binding(0) var<uniform> vp: vec4f; // w, h, keep, pad
202
+ @group(0) @binding(1) var<storage, read> src: array<f32>;
203
+ @group(0) @binding(2) var<storage, read_write> dst: array<f32>;
204
+
205
+ @compute @workgroup_size(64)
206
+ fn main(@builtin(global_invocation_id) gid: vec3u) {
207
+ let i = gid.x;
208
+ let w = u32(vp.x);
209
+ let h = u32(vp.y);
210
+ if (i >= w * h) { return; }
211
+ let x = i32(i % w);
212
+ let y = i32(i / w);
213
+ let c = src[i] * 2.0;
214
+ var s = c;
215
+ s = s + src[u32(clamp(y - 1, 0, i32(h) - 1)) * w + u32(clamp(x, 0, i32(w) - 1))];
216
+ s = s + src[u32(clamp(y + 1, 0, i32(h) - 1)) * w + u32(clamp(x, 0, i32(w) - 1))];
217
+ s = s + src[u32(clamp(y, 0, i32(h) - 1)) * w + u32(clamp(x - 1, 0, i32(w) - 1))];
218
+ s = s + src[u32(clamp(y, 0, i32(h) - 1)) * w + u32(clamp(x + 1, 0, i32(w) - 1))];
219
+ dst[i] = (s / 6.0 * 0.5 + src[i] * 0.5) * vp.z;
220
+ }
221
+ `;
222
+ }
223
+ function hashStr(s) {
224
+ let h = 2166136261;
225
+ for (let i = 0; i < s.length; i++) {
226
+ h ^= s.charCodeAt(i);
227
+ h = Math.imul(h, 16777619);
228
+ }
229
+ return h >>> 0;
230
+ }
@@ -0,0 +1,250 @@
1
+ import { GpuContext } from "../../core/context.js";
2
+ import { CompileError } from "../../core/errors.js";
3
+ const OPS = ['grayscale', 'invert', 'edge', 'blur', 'sharpen', 'brightness', 'contrast'];
4
+ const OP_IDS = { grayscale: 0, invert: 1, edge: 2, blur: 3, sharpen: 4, brightness: 5, contrast: 6 };
5
+ export async function applyImage(source, target, ops) {
6
+ if (ops.length === 0)
7
+ throw new Error('applyImage 需要至少一个算子');
8
+ const width = 'naturalWidth' in source ? source.naturalWidth : source.width;
9
+ const height = 'naturalHeight' in source ? source.naturalHeight : source.height;
10
+ const ctx = await GpuContext.get();
11
+ const device = ctx.device;
12
+ // 源纹理
13
+ const srcTex = device.createTexture({
14
+ size: [width, height],
15
+ format: 'rgba8unorm',
16
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
17
+ });
18
+ // 上传走 2D getImageData + writeTexture(纯 CPU 路径):
19
+ // 实测 headless 里 copyExternalImageToTexture 从 canvas 上传拿不到内容(合成器限制)
20
+ const c2d = document.createElement('canvas');
21
+ c2d.width = width;
22
+ c2d.height = height;
23
+ const sctx = c2d.getContext('2d', { willReadFrequently: true });
24
+ sctx.drawImage(source, 0, 0);
25
+ const imageData = sctx.getImageData(0, 0, width, height);
26
+ device.queue.writeTexture({ texture: srcTex }, imageData.data, { bytesPerRow: width * 4, rowsPerImage: height }, [width, height]);
27
+ // 中间纹理池
28
+ const pool = [];
29
+ const temp = () => {
30
+ const t = pool.pop() ?? device.createTexture({
31
+ size: [width, height],
32
+ format: 'rgba8unorm',
33
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
34
+ });
35
+ return t;
36
+ };
37
+ const module = device.createShaderModule({ code: shader(), label: 'image-filters' });
38
+ const info = await module.getCompilationInfo();
39
+ const errors = info.messages.filter((m) => m.type === 'error');
40
+ if (errors.length > 0)
41
+ throw new CompileError('image-filters', errors.map((m) => ({ line: m.lineNum, msg: m.message })), 0);
42
+ const pipeline = device.createRenderPipeline({
43
+ layout: 'auto',
44
+ vertex: { module, entryPoint: 'vs' },
45
+ fragment: { module, entryPoint: 'fs', targets: [{ format: 'rgba8unorm' }] },
46
+ primitive: { topology: 'triangle-list' },
47
+ });
48
+ const makePass = (input, output, op) => {
49
+ const p = op;
50
+ const uniform = device.createBuffer({ size: 32, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
51
+ const ub = new ArrayBuffer(32);
52
+ const uv = new DataView(ub);
53
+ uv.setUint32(0, OP_IDS[op.op] ?? 0, true);
54
+ uv.setUint32(4, Math.max(1, Math.min(4, Math.round(p.radius ?? 1))), true);
55
+ uv.setFloat32(8, width, true);
56
+ uv.setFloat32(12, height, true);
57
+ uv.setFloat32(16, p.amount ?? 1, true);
58
+ uv.setFloat32(20, p.value ?? 0, true);
59
+ device.queue.writeBuffer(uniform, 0, ub);
60
+ const sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear' });
61
+ const bg = device.createBindGroup({
62
+ layout: pipeline.getBindGroupLayout(0),
63
+ entries: [
64
+ { binding: 0, resource: { buffer: uniform } },
65
+ { binding: 1, resource: sampler },
66
+ { binding: 2, resource: input.createView() },
67
+ ],
68
+ });
69
+ const enc = device.createCommandEncoder();
70
+ const pass = enc.beginRenderPass({
71
+ colorAttachments: output
72
+ ? [{ view: output.createView(), clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: 'clear', storeOp: 'store' }]
73
+ : [],
74
+ });
75
+ pass.setPipeline(pipeline);
76
+ pass.setBindGroup(0, bg);
77
+ pass.draw(3);
78
+ pass.end();
79
+ return enc.finish();
80
+ };
81
+ let cur = srcTex;
82
+ let passes = 0;
83
+ for (const op of ops) {
84
+ const out = temp();
85
+ device.queue.submit([makePass(cur, out, op)]);
86
+ if (cur !== srcTex)
87
+ pool.push(cur);
88
+ cur = out;
89
+ passes++;
90
+ }
91
+ // 最终结果上屏到目标 canvas
92
+ const gpuCtx = target.getContext('webgpu');
93
+ if (!gpuCtx)
94
+ throw new Error('目标 canvas.getContext("webgpu") 返回空');
95
+ const format = navigator.gpu.getPreferredCanvasFormat();
96
+ gpuCtx.configure({ device, format, alphaMode: 'opaque' });
97
+ const blit = device.createShaderModule({
98
+ code: `
99
+ @group(0) @binding(0) var samp: sampler;
100
+ @group(0) @binding(1) var tex: texture_2d<f32>;
101
+ struct VsOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f };
102
+ @vertex fn vs(@builtin(vertex_index) v: u32) -> VsOut {
103
+ var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
104
+ var o: VsOut;
105
+ o.pos = vec4f(p[v], 0.0, 1.0);
106
+ o.uv = (p[v] + vec2f(1.0)) * 0.5;
107
+ o.uv.y = 1.0 - o.uv.y;
108
+ return o;
109
+ }
110
+ @fragment fn fs(i: VsOut) -> @location(0) vec4f {
111
+ let c = textureSample(tex, samp, i.uv);
112
+ return vec4f(c.rgb, 1.0);
113
+ }
114
+ `,
115
+ label: 'image-blit',
116
+ });
117
+ const blitPipeline = device.createRenderPipeline({
118
+ layout: 'auto',
119
+ vertex: { module: blit, entryPoint: 'vs' },
120
+ fragment: { module: blit, entryPoint: 'fs', targets: [{ format }] },
121
+ primitive: { topology: 'triangle-list' },
122
+ });
123
+ const enc = device.createCommandEncoder();
124
+ const pass = enc.beginRenderPass({
125
+ colorAttachments: [{ view: gpuCtx.getCurrentTexture().createView(), clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: 'clear', storeOp: 'store' }],
126
+ });
127
+ pass.setPipeline(blitPipeline);
128
+ pass.setBindGroup(0, device.createBindGroup({
129
+ layout: blitPipeline.getBindGroupLayout(0),
130
+ entries: [
131
+ { binding: 0, resource: device.createSampler({ magFilter: 'linear', minFilter: 'linear' }) },
132
+ { binding: 1, resource: cur.createView() },
133
+ ],
134
+ }));
135
+ pass.draw(3);
136
+ pass.end();
137
+ device.queue.submit([enc.finish()]);
138
+ srcTex.destroy();
139
+ for (const t of pool)
140
+ t.destroy();
141
+ const bytesPerRow = Math.ceil((width * 4) / 256) * 256;
142
+ const staging = device.createBuffer({ size: bytesPerRow * height, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
143
+ return {
144
+ width,
145
+ height,
146
+ passes,
147
+ async readback() {
148
+ // cur = 最后一个输出纹理(不在销毁池里);按行 256B 对齐拷回
149
+ const enc = device.createCommandEncoder();
150
+ enc.copyTextureToBuffer({ texture: cur }, { buffer: staging, bytesPerRow, rowsPerImage: height }, [width, height]);
151
+ device.queue.submit([enc.finish()]);
152
+ await staging.mapAsync(GPUMapMode.READ);
153
+ const ab = staging.getMappedRange().slice(0);
154
+ staging.unmap();
155
+ staging.destroy();
156
+ return new Uint8Array(ab);
157
+ },
158
+ };
159
+ }
160
+ function shader() {
161
+ return /* wgsl */ `
162
+ struct U {
163
+ op: u32, radius: u32,
164
+ w: f32, h: f32, amount: f32, value: f32,
165
+ _p0: u32, _p1: u32,
166
+ };
167
+ @group(0) @binding(0) var<uniform> u: U;
168
+ @group(0) @binding(1) var samp: sampler;
169
+ @group(0) @binding(2) var tex: texture_2d<f32>;
170
+
171
+ struct VsOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f };
172
+
173
+ @vertex
174
+ fn vs(@builtin(vertex_index) v: u32) -> VsOut {
175
+ var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
176
+ var o: VsOut;
177
+ o.pos = vec4f(p[v], 0.0, 1.0);
178
+ o.uv = (p[v] + vec2f(1.0)) * 0.5;
179
+ o.uv.y = 1.0 - o.uv.y;
180
+ return o;
181
+ }
182
+
183
+ fn lum(c: vec3f) -> f32 {
184
+ return dot(c, vec3f(0.2126, 0.7152, 0.0722));
185
+ }
186
+
187
+ @fragment
188
+ fn fs(i: VsOut) -> @location(0) vec4f {
189
+ let c = textureSample(tex, samp, i.uv).rgb;
190
+ let op = u.op;
191
+
192
+ if (op == 0u) { // grayscale
193
+ let g = vec3f(lum(c));
194
+ return vec4f(g, 1.0);
195
+ }
196
+ if (op == 1u) { // invert
197
+ return vec4f(1.0 - c, 1.0);
198
+ }
199
+ if (op == 2u) { // edge (sobel 幅值)
200
+ let e = 1.0 / vec2f(u.w, u.h);
201
+ let t00 = textureSample(tex, samp, i.uv + vec2f(-e.x, -e.y)).rgb;
202
+ let t10 = textureSample(tex, samp, i.uv + vec2f(0.0, -e.y)).rgb;
203
+ let t20 = textureSample(tex, samp, i.uv + vec2f(e.x, -e.y)).rgb;
204
+ let t01 = textureSample(tex, samp, i.uv + vec2f(-e.x, 0.0)).rgb;
205
+ let t21 = textureSample(tex, samp, i.uv + vec2f(e.x, 0.0)).rgb;
206
+ let t02 = textureSample(tex, samp, i.uv + vec2f(-e.x, e.y)).rgb;
207
+ let t12 = textureSample(tex, samp, i.uv + vec2f(0.0, e.y)).rgb;
208
+ let t22 = textureSample(tex, samp, i.uv + vec2f(e.x, e.y)).rgb;
209
+ let sx = (t22 + 2.0 * t21 + t02) - (t00 + 2.0 * t01 + t20);
210
+ let sy = (t02 + 2.0 * t12 + t22) - (t00 + 2.0 * t10 + t20);
211
+ let g = sqrt(vec3f(dot(sx, sx) / 3.0 + dot(sy, sy) / 3.0));
212
+ return vec4f(clamp(g * u.amount, vec3f(0.0), vec3f(1.0)), 1.0);
213
+ }
214
+ if (op == 3u) { // box blur,radius 折叠成步长采样
215
+ let r = f32(u.radius);
216
+ let e = vec2f(1.0) / vec2f(u.w, u.h) * r;
217
+ var s = vec3f(0.0);
218
+ var n = 0.0;
219
+ for (var dy = -2; dy <= 2; dy++) {
220
+ for (var dx = -2; dx <= 2; dx++) {
221
+ s = s + textureSample(tex, samp, i.uv + vec2f(f32(dx), f32(dy)) * e * 0.6).rgb;
222
+ n = n + 1.0;
223
+ }
224
+ }
225
+ return vec4f(s / n, 1.0);
226
+ }
227
+ if (op == 4u) { // sharpen(3x3 卷积)
228
+ let e = vec2f(1.0) / vec2f(u.w, u.h);
229
+ let c0 = textureSample(tex, samp, i.uv).rgb;
230
+ let t00 = textureSample(tex, samp, i.uv + vec2f(-e.x, -e.y)).rgb;
231
+ let t10 = textureSample(tex, samp, i.uv + vec2f(0.0, -e.y)).rgb;
232
+ let t20 = textureSample(tex, samp, i.uv + vec2f(e.x, -e.y)).rgb;
233
+ let t01 = textureSample(tex, samp, i.uv + vec2f(-e.x, 0.0)).rgb;
234
+ let t21 = textureSample(tex, samp, i.uv + vec2f(e.x, 0.0)).rgb;
235
+ let t02 = textureSample(tex, samp, i.uv + vec2f(-e.x, e.y)).rgb;
236
+ let t12 = textureSample(tex, samp, i.uv + vec2f(0.0, e.y)).rgb;
237
+ let t22 = textureSample(tex, samp, i.uv + vec2f(e.x, e.y)).rgb;
238
+ let k = u.amount;
239
+ let acc = t00 + t10 + t20 + t01 + t21 + t02 + t12 + t22;
240
+ return vec4f(clamp(c0 * (1.0 + 8.0 * k) - acc * k, vec3f(0.0), vec3f(1.0)), 1.0);
241
+ }
242
+ if (op == 5u) { // brightness
243
+ return vec4f(clamp(c + vec3f(u.value), vec3f(0.0), vec3f(1.0)), 1.0);
244
+ }
245
+ // contrast
246
+ let g = vec3f(0.5);
247
+ return vec4f(clamp(g + (c - g) * u.value, vec3f(0.0), vec3f(1.0)), 1.0);
248
+ }
249
+ `;
250
+ }