揭秘Phantom.land的交互式网格与3D面部粒子系统构建

本文深入解析Phantom.land网站如何运用React Three Fiber、GLSL着色器和GSAP构建动态交互网格与3D面部粒子系统,包含技术架构、着色器编程和性能优化细节。

隐形力量:Phantom.land交互式网格与3D面部粒子系统的构建

Phantom.land的动态网格和体积面部扫描,采用React Three Fiber、GLSL着色器和GSAP精心打造。

技术选型

我们从Next/React基础开始,利用React Three Fiber库无缝连接DOM组件和全站使用的WebGL上下文。样式方面采用自定义CSS组件和SASS。对于交互行为和动画,选择GSAP因其包含SplitText、CustomEase和ScrollTrigger等插件,且能在DOM和WebGL组件间使用统一的动画框架。

首页网格

网格视图通过将原始Three.js对象集成到React Three Fiber场景中实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// GridView.tsx
const GridView = () => {
  return (
    <Canvas>
      ...
      <ProjectsGrid />
      <Postprocessing />
    </Canvas>
  );
}

由于组件复杂度,最终选择使用原生Three.js类而非React Three Fiber编写全部代码。

后处理失真效果

通过后处理管道中的自定义着色器通道实现标志性失真效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Postprocessing.tsx
const Postprocessing = () => {
  const {gl, scene, camera} = useThree();
  
  const {effectComposer, distortionShader} = useMemo(() => {
    const renderPass = new RenderPass(scene, camera);
    const distortionShader = new DistortionShader();
    const distortionPass = new ShaderPass(distortionShader);
    const outputPass = new OutputPass();

    const effectComposer = new EffectComposer(gl);
    effectComposer.addPass(renderPass);
    effectComposer.addPass(distortionPass);
    effectComposer.addPass(outputPass);

    return {effectComposer, distortionShader};
  }, []);
}

失真强度通过DistortionShader类中的简单补间动画进行变化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class DistortionShader extends ShaderMaterial {
  setDistortion(value: number) {
    gsap.to(this, {
      distortionIntensity: value,
      duration: 1,
      ease: 'power2.out',
      onUpdate: () => this.update()
    }
  }
}

失真通过自定义着色器应用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// fragment.ts
export const fragmentShader = /* glsl */ `
  uniform sampler2D tDiffuse;
  uniform vec2 distortion;
  
  void main() {
    vec2 shiftedUv = getShiftedUv(vUv);
    shiftedUv *= (0.88 + distortion * dot(shiftedUv));
    vec2 transformedUv = getUnshiftedUv(shiftedUv);
    
    color = texture2D(tDiffuse, distortedUV).rgb;
    gl_FragColor = vec4(color, 1.);
  }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

### 微交互设计

**环境鼠标偏移**:计算鼠标在网格上的位置并相应移动网格网格体:

```typescript
getAmbientCursorOffset() {
  const uv = this.navigation.pointerUv;
  const offset = uv.subScalar(0.5).multiplyScalar(0.2);
  return offset;
}

拖拽缩放:检测用户开始和停止拖拽时触发GSAP动画:

1
2
3
4
5
6
7
animateCameraZ(distance: number, duration: number) {
  gsap.to(this.camera.position, {
    z: distance,
    duration,
    ease: CustomEase.create('cameraZoom', '.23,1,0.32,1'),
  });
}

拖拽移动:用户拖拽释放后网格带有惯性滑动:

1
2
3
4
drag(offset: Vector2) {
  this.dragAction = offset;
  this.velocity.lerp(offset, 0.8);
}

面部粒子系统

员工面部轮播通过动态3D粒子系统呈现,使用React Three Fiber的BufferGeometry和自定义GLSL着色器构建。

核心概念:深度驱动的粒子生成

将2D图像转换为体积3D表示,每个面部仅使用两个优化的256×256 WebP图像(各小于15KB)。

通过RealityScan进行3D扫描,在Cinema4D中渲染位置和颜色通道,在Photoshop中转换为灰度深度图。

每个面部由约78,400个粒子(280×280网格)构成:

1
2
3
4
5
6
const POINT_AMOUNT = 280;
const points = useMemo(() => {
  const length = POINT_AMOUNT * POINT_AMOUNT;
  const vPositions = new Float32Array(length * 3);
  // ...位置属性数组生成
}, []);

动态粒子缩放

通过颜色数据分析影响粒子大小,较亮区域生成更大粒子:

1
2
3
4
/* vertex shader */
vec3 colorTexture1 = texture2D(colorMap1, vIndex.xy).xyz;
float density = (mainColorTexture.x + mainColorTexture.y + mainColorTexture.z) / 3.;
float pScale = mix(pScaleMin, pScaleMax, density);

环境噪声动画

使用卷曲噪声创建细微流动运动:

1
2
/* vertex shader */
pos += curlNoise(pos * curlFreq1 + time) * noiseScale * 0.1;

面部过渡动画

使用GSAP时间轴动画化多个着色器参数:

1
2
3
4
timelineRef.current = gsap
  .timeline()
  .fromTo(uniforms.transition, {value: 0}, {value: 1.3, duration: 1.6})
  .to(uniforms.posZ, {value: particlesParams.offset_z, duration: 1.6}, 0);

着色器处理两个面部状态间的视觉混合:

1
2
3
/* vertex shader */
vec3 mainColorTexture = mix(colorTexture1, colorTexture2, speed);
vec3 depthValue = mix(depthTexture1, depthTexture2, speed);

自定义景深效果

在着色器材质中实现自定义景深效果:

1
2
3
4
5
6
7
8
/* vertex shader */
vec4 viewPosition = viewMatrix * modelPosition;
vDistance = abs(focus + viewPosition.z);
gl_PointSize = pointSize * pScale * vDistance * blur * totalScale;

/* fragment shader */
float alpha = (1.04 - clamp(vDistance * 1.5, 0.0, 1.0));
gl_FragColor = vec4(color, alpha);

挑战:统一面部比例

需要为每个面部手动调整多个参数:

1
2
3
4
5
particle_params: { 
  offset_z: 0,           // 整体Z位置
  z_depth_scale: 0,      // 深度图缩放因子
  face_size: 0,          // 整体面部比例
}

结语

我们的面部粒子系统展示了如何通过简单而谨慎的技术实现,从最小资源创建有趣的视觉体验。通过结合轻量级WebP纹理、自定义着色器材料和动画,我们创建了一个将简单2D肖像转换为交互式3D图形的系统。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计