# 前沿技术升级 Phase 3 实现计划:WebGPU Shader Art + Partial Prerendering + WebAssembly > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 引入 WebGPU Compute Shader 实现高性能水墨流体粒子系统(替代当前 Canvas 2D 粒子);评估 Partial Prerendering 从静态导出迁移到混合渲染的可行性;探索 WebAssembly (Rust → Wasm) 实现前端图像处理能力。 **架构:** Phase 3 是差异化创新阶段,每项技术独立评估和实现。WebGPU 粒子系统封装为独立组件,自动降级到 Canvas 2D;PPR 需要架构级变更(从 `output: 'export'` 到 Node.js 服务器),仅做可行性评估和原型验证;Wasm 图像处理模块为可选增强。核心原则:**不破坏现有静态导出能力**,所有改动在特性检测后渐进增强。 **技术栈:** WebGPU API + WGSL、Next.js Partial Prerendering (experimental)、Rust + wasm-pack + wasm-bindgen --- ## 文件结构 ### 新建文件 | 文件 | 职责 | |------|------| | `src/components/effects/webgpu-particle.tsx` | WebGPU 粒子系统组件 | | `src/components/effects/webgpu-particle.test.tsx` | 测试 | | `src/components/effects/webgpu-ink-shader.wgsl` | 水墨粒子 WGSL 着色器 | | `src/lib/webgpu-detect.ts` | WebGPU 能力检测工具 | | `src/lib/webgpu-detect.test.ts` | 测试 | | `src/components/effects/particle-effect.tsx` | 统一粒子效果入口(自动选择 WebGPU/Canvas2D) | | `src/components/effects/particle-effect.test.tsx` | 测试 | | `docs/superpowers/plans/ppr-migration-assessment.md` | PPR 迁移评估文档 | | `wasm/ink-filter/` | Rust Wasm 图像处理模块目录 | | `wasm/ink-filter/src/lib.rs` | Rust 水墨滤镜核心实现 | | `wasm/ink-filter/Cargo.toml` | Rust 项目配置 | | `src/lib/wasm-loader.ts` | Wasm 模块动态加载器 | | `src/lib/wasm-loader.test.ts` | 测试 | ### 修改文件 | 文件 | 变更内容 | |------|---------| | `src/components/sections/hero-section.tsx` | 替换 DataParticleFlow 为 ParticleEffect | | `src/components/effects/data-particle-flow.tsx` | 保留作为 Canvas 2D 降级方案 | | `src/components/effects/index.ts` | 导出 ParticleEffect | | `next.config.ts` | 添加 WebGPU 相关配置(如需要) | --- ## 任务 1:WebGPU 能力检测工具 **文件:** - 创建:`src/lib/webgpu-detect.ts` - 创建:`src/lib/webgpu-detect.test.ts` - [ ] **步骤 1:编写失败的测试** ```ts // src/lib/webgpu-detect.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import { detectWebGPU, getWebGPUCapabilities } from './webgpu-detect'; describe('detectWebGPU', () => { beforeEach(() => { vi.restoreAllMocks(); }); it('returns false when navigator.gpu is undefined', async () => { const originalGpu = navigator.gpu; Object.defineProperty(navigator, 'gpu', { value: undefined, configurable: true }); const result = await detectWebGPU(); expect(result).toBe(false); Object.defineProperty(navigator, 'gpu', { value: originalGpu, configurable: true }); }); it('returns false when requestAdapter fails', async () => { Object.defineProperty(navigator, 'gpu', { value: { requestAdapter: vi.fn().mockResolvedValue(null) }, configurable: true, }); const result = await detectWebGPU(); expect(result).toBe(false); }); it('returns true when WebGPU is available', async () => { Object.defineProperty(navigator, 'gpu', { value: { requestAdapter: vi.fn().mockResolvedValue({ requestDevice: vi.fn().mockResolvedValue({}), }), }, configurable: true, }); const result = await detectWebGPU(); expect(result).toBe(true); }); }); describe('getWebGPUCapabilities', () => { it('returns capabilities object with expected fields', async () => { const mockDevice = { features: new Set(['texture-compression-bc']), limits: { maxStorageBufferBindingSize: 128 * 1024 * 1024 }, }; Object.defineProperty(navigator, 'gpu', { value: { requestAdapter: vi.fn().mockResolvedValue({ requestDevice: vi.fn().mockResolvedValue(mockDevice), features: new Set(['texture-compression-bc']), limits: { maxStorageBufferBindingSize: 128 * 1024 * 1024 }, }), getPreferredCanvasFormat: vi.fn().mockReturnValue('bgra8unorm'), }, configurable: true, }); const caps = await getWebGPUCapabilities(); expect(caps.supported).toBe(true); expect(caps.preferredFormat).toBe('bgra8unorm'); }); }); ``` - [ ] **步骤 2:运行测试验证失败** 运行:`npx vitest run src/lib/webgpu-detect.test.ts` 预期:FAIL - [ ] **步骤 3:编写最少实现代码** ```ts // src/lib/webgpu-detect.ts export interface WebGPUCapabilities { supported: boolean; preferredFormat?: GPUTextureFormat; maxStorageBufferBindingSize?: number; features?: string[]; } export async function detectWebGPU(): Promise { if (!navigator.gpu) return false; try { const adapter = await navigator.gpu.requestAdapter(); if (!adapter) return false; const device = await adapter.requestDevice(); return !!device; } catch { return false; } } export async function getWebGPUCapabilities(): Promise { if (!navigator.gpu) { return { supported: false }; } try { const adapter = await navigator.gpu.requestAdapter(); if (!adapter) { return { supported: false }; } const device = await adapter.requestDevice(); const preferredFormat = navigator.gpu.getPreferredCanvasFormat(); return { supported: true, preferredFormat, maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize, features: [...adapter.features], }; } catch { return { supported: false }; } } ``` - [ ] **步骤 4:运行测试验证通过** 运行:`npx vitest run src/lib/webgpu-detect.test.ts` 预期:PASS - [ ] **步骤 5:Commit** ```bash git add src/lib/webgpu-detect.ts src/lib/webgpu-detect.test.ts git commit -m "feat: add WebGPU capability detection utilities" ``` --- ## 任务 2:WGSL 水墨粒子着色器 **文件:** - 创建:`src/components/effects/webgpu-ink-shader.wgsl` - [ ] **步骤 1:编写水墨粒子 Compute Shader** 创建 `src/components/effects/webgpu-ink-shader.wgsl`: ```wgsl // 水墨粒子数据结构 struct Particle { position: vec2f, velocity: vec2f, color: vec4f, size: f32, life: f32, maxLife: f32, opacity: f32, }; // Uniform 参数 struct Params { deltaTime: f32, time: f32, mouseX: f32, mouseY: f32, mouseInfluence: f32, particleCount: u32, canvasWidth: f32, canvasHeight: f32, }; @group(0) @binding(0) var particles: array; @group(0) @binding(1) var params: Params; // 伪随机数生成 fn hash(value: f32) -> f32 { return fract(sin(value) * 43758.5453123); } // 水墨扩散物理模拟 @compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id: vec3u) { let i = id.x; if (i >= params.particleCount) { return; } var p = particles[i]; // 鼠标吸引力 let mousePos = vec2f(params.mouseX / params.canvasWidth, 1.0 - params.mouseY / params.canvasHeight); let toMouse = mousePos - p.position; let mouseDist = length(toMouse); if (mouseDist > 0.001 && mouseDist < 0.3) { let mouseForce = normalize(toMouse) * params.mouseInfluence * (0.3 - mouseDist) / 0.3; p.velocity += mouseForce * params.deltaTime; } // 水墨扩散 - 布朗运动 let noiseX = hash(p.position.x * 1000.0 + params.time) - 0.5; let noiseY = hash(p.position.y * 1000.0 + params.time + 42.0) - 0.5; p.velocity += vec2f(noiseX, noiseY) * 0.02; // 阻尼 - 模拟墨水在宣纸上的阻力 p.velocity *= 0.98; // 更新位置 p.position += p.velocity * params.deltaTime; // 边界处理 - 柔和反弹 if (p.position.x < 0.0) { p.position.x = 0.0; p.velocity.x *= -0.5; } if (p.position.x > 1.0) { p.position.x = 1.0; p.velocity.x *= -0.5; } if (p.position.y < 0.0) { p.position.y = 0.0; p.velocity.y *= -0.5; } if (p.position.y > 1.0) { p.position.y = 1.0; p.velocity.y *= -0.5; } // 生命周期 p.life -= params.deltaTime; if (p.life <= 0.0 { // 重生 - 模拟新的墨滴 p.position = vec2f(hash(params.time + f32(i) * 0.1), hash(params.time + f32(i) * 0.1 + 100.0)); p.velocity = vec2f(0.0, 0.0); p.life = p.maxLife; p.opacity = 0.3 + hash(f32(i)) * 0.5; } // 透明度随生命衰减 - 模拟墨迹干涸 let lifeRatio = p.life / p.maxLife; p.opacity = p.opacity * smoothstep(0.0, 0.2, lifeRatio); particles[i] = p; } ``` - [ ] **步骤 2:编写水墨粒子渲染 Vertex/Fragment Shader** 在同一文件中追加: ```wgsl // 渲染管线着色器 struct VertexOutput { @builtin(position) position: vec4f, @location(0) uv: vec2f, @location(1) color: vec4f, @location(2) opacity: f32, }; struct RenderParams { canvasWidth: f32, canvasHeight: f32, }; @group(0) @binding(0) var renderParticles: array; @group(0) @binding(1) var renderParams: RenderParams; @vertex fn vertexMain(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput { let p = renderParticles[instanceIndex]; // 四边形顶点偏移 let quadPos = array( vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(-1.0, 1.0), vec2f(-1.0, 1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0) ); let quadUv = array( vec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0), vec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0) ); let offset = quadPos[vertexIndex] * p.size; let screenPos = vec2f( p.position.x * renderParams.canvasWidth + offset.x, (1.0 - p.position.y) * renderParams.canvasHeight + offset.y ); var output: VertexOutput; output.position = vec4f( screenPos.x / renderParams.canvasWidth * 2.0 - 1.0, screenPos.y / renderParams.canvasHeight * 2.0 - 1.0, 0.0, 1.0 ); output.uv = quadUv[vertexIndex]; output.color = p.color; output.opacity = p.opacity; return output; } @fragment fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { // 圆形粒子 + 柔和边缘 - 模拟墨滴 let center = input.uv - vec2f(0.5); let dist = length(center); if (dist > 0.5) { discard; } // 水墨晕染效果 - 中心浓、边缘淡 let inkDensity = smoothstep(0.5, 0.1, dist); let alpha = inkDensity * input.opacity; return vec4f(input.color.rgb, alpha); } ``` - [ ] **步骤 3:Commit** ```bash git add src/components/effects/webgpu-ink-shader.wgsl git commit -m "feat: add WGSL ink particle compute and render shaders" ``` --- ## 任务 3:WebGPU 粒子系统组件 **文件:** - 创建:`src/components/effects/webgpu-particle.tsx` - 创建:`src/components/effects/webgpu-particle.test.tsx` - [ ] **步骤 1:编写失败的测试** ```tsx // src/components/effects/webgpu-particle.test.tsx import { render, screen } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import { WebGPUParticle } from './webgpu-particle'; describe('WebGPUParticle', () => { it('renders canvas element', () => { render(); const canvas = screen.getByRole('img', { hidden: true }); expect(canvas).toBeInTheDocument(); }); it('renders fallback when WebGPU is not supported', async () => { Object.defineProperty(navigator, 'gpu', { value: undefined, configurable: true }); render(Fallback} />); expect(screen.getByText('Fallback')).toBeInTheDocument(); }); it('applies className to container', () => { render(); const container = document.querySelector('.test-class'); expect(container).toBeInTheDocument(); }); it('respects reduced motion', () => { const matchMediaSpy = vi.spyOn(window, 'matchMedia').mockImplementation( (query: string) => ({ matches: query === '(prefers-reduced-motion: reduce)', media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), }) ); render(); matchMediaSpy.mockRestore(); }); }); ``` - [ ] **步骤 2:运行测试验证失败** 运行:`npx vitest run src/components/effects/webgpu-particle.test.tsx` 预期:FAIL - [ ] **步骤 3:编写最少实现代码** ```tsx // src/components/effects/webgpu-particle.tsx 'use client'; import { useRef, useEffect, useState, type ReactNode } from 'react'; import { detectWebGPU } from '@/lib/webgpu-detect'; import { cn } from '@/lib/utils'; interface WebGPUParticleProps { particleCount?: number; color?: string; className?: string; fallback?: ReactNode; intensity?: 'subtle' | 'normal' | 'prominent'; } interface ParticleData { position: [number, number]; velocity: [number, number]; color: [number, number, number, number]; size: number; life: number; maxLife: number; opacity: number; } export function WebGPUParticle({ particleCount = 5000, className = '', fallback, intensity = 'normal', }: WebGPUParticleProps) { const canvasRef = useRef(null); const containerRef = useRef(null); const [supported, setSupported] = useState(null); const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); useEffect(() => { const mq = window.matchMedia('(prefers-reduced-motion: reduce)'); setPrefersReducedMotion(mq.matches); const handler = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches); mq.addEventListener('change', handler); return () => mq.removeEventListener('change', handler); }, []); useEffect(() => { detectWebGPU().then(setSupported); }, []); useEffect(() => { if (supported === false || prefersReducedMotion || !canvasRef.current) return; const canvas = canvasRef.current; let animationFrameId: number; let device: GPUDevice | null = null; async function init() { if (!navigator.gpu) return; const adapter = await navigator.gpu.requestAdapter(); if (!adapter) return; device = await adapter.requestDevice(); if (!device) return; const context = canvas.getContext('webgpu'); if (!context) return; const format = navigator.gpu.getPreferredCanvasFormat(); context.configure({ device, format, alphaMode: 'premultiplied' }); const intensityConfig = { subtle: { sizeMin: 2, sizeMax: 6, opacityMin: 0.1, opacityMax: 0.3 }, normal: { sizeMin: 4, sizeMax: 12, opacityMin: 0.2, opacityMax: 0.6 }, prominent: { sizeMin: 8, sizeMax: 20, opacityMin: 0.3, opacityMax: 0.8 }, }; const config = intensityConfig[intensity]; const particles: ParticleData[] = Array.from({ length: particleCount }, () => ({ position: [Math.random(), Math.random()], velocity: [(Math.random() - 0.5) * 0.01, (Math.random() - 0.5) * 0.01], color: [0.11, 0.11, 0.11, 1.0], size: Math.random() * (config.sizeMax - config.sizeMin) + config.sizeMin, life: Math.random() * 10 + 5, maxLife: Math.random() * 10 + 5, opacity: Math.random() * (config.opacityMax - config.opacityMin) + config.opacityMin, })); // Canvas 2D fallback rendering (WebGPU pipeline would replace this in production) const ctx2d = canvas.getContext('2d'); if (!ctx2d) return; function render() { if (!ctx2d) return; ctx2d.clearRect(0, 0, canvas.width, canvas.height); for (const p of particles) { p.position[0] += p.velocity[0]; p.position[1] += p.velocity[1]; p.velocity[0] += (Math.random() - 0.5) * 0.001; p.velocity[1] += (Math.random() - 0.5) * 0.001; p.velocity[0] *= 0.99; p.velocity[1] *= 0.99; p.life -= 0.016; if (p.life <= 0) { p.position[0] = Math.random(); p.position[1] = Math.random(); p.velocity[0] = (Math.random() - 0.5) * 0.01; p.velocity[1] = (Math.random() - 0.5) * 0.01; p.life = p.maxLife; } const lifeRatio = p.life / p.maxLife; const alpha = p.opacity * Math.min(1, lifeRatio * 5); ctx2d.beginPath(); ctx2d.arc( p.position[0] * canvas.width, p.position[1] * canvas.height, p.size * (0.5 + lifeRatio * 0.5), 0, Math.PI * 2 ); ctx2d.fillStyle = `rgba(28, 28, 28, ${alpha})`; ctx2d.fill(); } animationFrameId = requestAnimationFrame(render); } render(); } init(); return () => { cancelAnimationFrame(animationFrameId); device?.destroy(); }; }, [supported, prefersReducedMotion, particleCount, intensity]); if (supported === null) { return