Threejs:FBO实现动态文字效果

77 阅读3分钟

Position FBO

canvas画布纹理

使用canvas API书写文字后,将canvas作为纹理。使用uv坐标获取canvas纹理上一点,如果大于0,说明是文字,否则是背景。

随机坐标

随机生成长宽和canvas纹理一样的随机坐标纹理。

Vertex shader

由于该fbo只是用来计算位置,然后将位置作为纹理输出到renderTarget里给最终renderer使用的,所以vertex shader很简单,直接平面的position做mvp变换即可,最终还是平面,重要的是每个平面的颜色记载了这个uv对应的点的位置。

const positionSimulationVertexShader = `
  // this value stores the texture coordinates the data for this vertex is stored in
  varying vec2 vUv;

  void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`

Fragement shader

fbo类中会自动生成一个plane几何体,上面的canvas纹理和随机坐标纹理都作为uniform变量传入到自定义的shader中。

在vertex shader中使用uv坐标分别从canvas纹理获取透明度,从默认坐标纹理获取随机坐标,表示的是当前屏幕这一点被打乱后的所在位置,如果说canvas透明度大于0,也就是文字,那么需要将打乱的点复位。也就是需要将随机坐标移动到当前uv。

目标是将default position 移动到uv附近

const positionSimulationFragmentShader = `
  /** generates a random number between 0 and 1 **/
  highp float rand(vec2 co) {
    highp float a = 12.9898;
    highp float b = 78.233;
    highp float c = 43758.5453;
    highp float dt= dot(co.xy ,vec2(a,b));
    highp float sn= mod(dt,3.14);
    return fract(sin(sn) * c);
  }

  // this is the texture position the data for this particle is stored in
  varying vec2 vUv;

  uniform sampler2D tPrev;
  uniform sampler2D tCurr;

  uniform sampler2D tDefaultPosition;
  uniform sampler2D tText;

  uniform float topSpeed;
  uniform float acceleration;
  uniform float textPositionMultiplier;

  vec3 setTopSpeed(vec3 speed, float topSpeed) {
    return vec3(
      speed.x > topSpeed ? topSpeed : speed.x < -topSpeed ? -topSpeed : speed.x,
      speed.y > topSpeed ? topSpeed : speed.y < -topSpeed ? -topSpeed : speed.y,
      speed.z > topSpeed ? topSpeed : speed.z < -topSpeed ? -topSpeed : speed.z
    );
  }

  vec3 moveParticleToGoal(vec3 currPos, vec3 prevPos, vec3 goal) {
    vec3 distanceToGoal = goal - currPos;
    vec3 currVelocity = currPos - prevPos;

    vec3 calculatedAcceleration = normalize(distanceToGoal) * acceleration;
    float currVelocityL = length(currVelocity);
    float distanceToGoalL = length(distanceToGoal);

    if (distanceToGoalL > currVelocityL) {
      vec3 velocity = currVelocity + calculatedAcceleration;

      velocity = setTopSpeed(velocity, topSpeed);

      return currPos + velocity;
    } else {
      return goal;
    }
  }

  void main() {
    vec3 defaultPos = texture2D(tDefaultPosition, vUv).xyz;
    vec3 prevPos = texture2D(tPrev, vUv).xyz;
    vec3 position = texture2D(tCurr, vUv).xyz;
    float textOpacity = texture2D(tText, vUv).r;
    float isTextParticle = 0.0;

    if (prevPos == vec3(0.0, 0.0, 0.0)) {
      position = defaultPos;
    }

    if (textOpacity > 0.0) {
      position = moveParticleToGoal(position, prevPos, vec3((vUv - 0.5) * textPositionMultiplier, 0.0));

      isTextParticle = 1.0;
    }
     else {
      position = moveParticleToGoal(position, prevPos, defaultPos);
    }

    // write new positions out
    gl_FragColor = vec4(position, isTextParticle);
  }
`

Renderer

几何体数据

生成一个长宽和canvas纹理一样的顶点数组,每个点是uv坐标。这样在renderer自定义shader里的position获取到的就是uv坐标。(效果和生成plane几何体类似)。但是这里使用Points渲染方式。

Vertex shader

这里输入的数据是一个平面,渲染方式是点,显然我不是想渲染一个点平面,如果vertex shader是最简单的mvp转换,那最终就是点平面。所以vertex shader需要根据输入position去计算出真正点的位置。上面说过position实际是uv,那么根据uv到positionFbo对应的uniform里去取出当前uv对应的真实坐标,对该坐标mvp转换就拿到了gl_position。

gl_PointSize根据移动的比率计算最小值和最大值的插值。

const vertexShader = `

  uniform sampler2D tPosition;
  uniform float sizeMultiplierForScreen;
  uniform float textSizeMultiplier;
  uniform sampler2D tDefaultPosition;
  uniform sampler2D tSize;

  varying vec2 vUv;

  void main() {
    // gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    // gl_PointSize = 9.0;

    vUv = position.xy;
    vec3 position = texture2D(tPosition, vUv).xyz;

    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);

    gl_Position = projectionMatrix * mvPosition;

    vec3 goal = vec3((vUv - 0.5) * 4.0, 0);

    vec3 defaultPosition = texture2D(tDefaultPosition, vUv).xyz;
    float distanceToTravel = length(goal - defaultPosition);
    float distanceTravelled = length(position - defaultPosition);
    float distanceTravelledRatio = distanceTravelled / distanceToTravel;

    // float size = 0.3;
    float size = texture2D(tSize, vUv).a ;
    float textSize = size * size * textSizeMultiplier; // multiply star size against itself to create a size range when a text-star
    size = distanceTravelled > 0.0 ? mix(size, textSize, distanceTravelledRatio > 1.0 ? 1.0 : distanceTravelledRatio) : size;

    gl_PointSize = size * (sizeMultiplierForScreen / -mvPosition.z);
    
  }
`

Fragment shader

根据uv可以获取到当前位置的透明度,也就是是否为文字;可以从颜色纹理中得到一个颜色值;

根据gl_PointCoord从一张图片里获取到一个颜色值,最终颜色相乘。

gl_PointCoord的效果就是通过从纹理中采样设置颜色的方式,将points渲染模式默认的方形,转换成一张图片纹理

const fragmentShader = `
  uniform sampler2D tPosition;
  uniform sampler2D starImg;
  uniform sampler2D tColour;

  varying vec2 vUv;

  void main() {
    vec4 colour = texture2D(tColour, vUv).rgba;
    float isTextColor = texture2D(tPosition, vUv).a;

    if (isTextColor > 0.95) {
    gl_FragColor = colour * texture2D(starImg, gl_PointCoord);
    }
  }
`