Three.js 中的动画文本带

Animated text ribbons in Three.js

我是 three.js 的新手,不是数学天才。但是,我挑战自己通过构建动画文本丝带来学习它。

我正在尝试构建的是这样的:

基本想法:

  1. 沿着自定义路径创建至少两条光滑的织物状丝带。
  2. 丝带的正面和背面可以有不同的纹理
  3. 丝带的路径应该是封闭的,这样我就可以 show/hide 在无尽的动画中分段。

动画:

我想显示 1/3 的色带,然后沿着路径设置动画以显示下一部分,同时隐藏之前的部分。所以 text/material 在丝带露出时似乎在房间中处于静止位置。如果你看下面我的例子,我有点用 setdrawRange 来实现这个。但是,在之前的范围结束之前,我还不能再次显示第一段。这会导致路径末尾的动画跳转。

这是我目前的情况:

演示: https://w73zt.csb.app/

代码沙盒: https://codesandbox.io/s/ribbons-w73zt

我正在努力解决的问题:

  1. 我的方法是使用 TubeGeometry 并只给它两个径向段。这是合法的,还是有更好的方法来实现我想要实现的目标?
  2. 不知何故,我无法在该管的 FrontSideBackSide 上放置不同的纹理。如果我尝试根据文档将它们添加为数组,则根本不会显示任何纹理。我在这里错过了什么?
  3. 如何使用setdrawRange在路径的end/start处创建一个没有跳跃的无尽动画效果?希望我想说的是可以理解的。上面的演示应该使问题形象化。
  4. 出于某种原因,管道的“关闭”选项在结尾和开头之间创建了一个奇怪的连接,但段数较少。这会导致纹理被拉伸。我怎样才能避免这种情况?
  5. 像这样使用 SVG 渲染文本是否有益,或者您会推荐另一种方法吗?

非常感谢任何帮助!

这里是非常粗略的,未优化的,不完善的方案(根本算不上终极方案,但至少是一个起点),基于ExtrudeGeometry的源码形成ribbon,使用修改后的MeshBasicMaterial 使用 discard:

剪彩

body{
  overflow: hidden;
  margin: 0;
}
<script type="module">
console.clear();
import * as THREE from "https://threejs.org/build/three.module.js";
import { OrbitControls } from "https://threejs.org/examples/jsm/controls/OrbitControls.js";

let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 1, 100);
camera.position.set(5, 8, 13);
let renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setClearColor(0x404040);
document.body.appendChild(renderer.domElement);

window.addEventListener( 'resize', onWindowResize );

let controls = new OrbitControls(camera, renderer.domElement);

let grid = new THREE.GridHelper(10, 50);
scene.add(grid);

const pCount = 7;
let pts = new Array(pCount).fill(0).map((p, idx) => {
  return new THREE.Vector3().setFromSphericalCoords(
    5,
    Math.random() * Math.PI * (2 / 3) + Math.PI / 6,
    (idx / pCount) * Math.PI * 2
  );
});

let curve = new THREE.CatmullRomCurve3(pts, true);

const segments = 500;

let lpts = curve.getSpacedPoints(segments);
let lg = new THREE.BufferGeometry().setFromPoints(lpts);
let lm = new THREE.LineBasicMaterial({ color: "yellow" });
let l = new THREE.Line(lg, lm);
scene.add(l);

const frames = curve.computeFrenetFrames(segments, true);

let g = buildRibbon([
  { x: 0, y: 0.5 },
  { x: 0, y: -0.5 }
]);
let m = new THREE.MeshBasicMaterial({
  map: new THREE.TextureLoader().load(
    "https://threejs.org/examples/textures/uv_grid_opengl.jpg"
  ),
  side: THREE.DoubleSide,
  onBeforeCompile: (shader) => {
    shader.uniforms.time = m.userData.uniforms.time;
    shader.fragmentShader = `
      uniform float time;
      ${shader.fragmentShader}
    `.replace(
      `#include <clipping_planes_fragment>`,
      `
        if ( fract(time - vUv.x) < (2. / 3.)) discard;
        
      #include <clipping_planes_fragment>`
    );
    console.log(shader.fragmentShader);
  }
});
m.userData.uniforms = { time: { value: 0 } };
m.defines = { USE_UV: "" };
let r = new THREE.Mesh(g, m);
scene.add(r);

let clock = new THREE.Clock();

renderer.setAnimationLoop((_) => {
  m.userData.uniforms.time.value = clock.getElapsedTime() * 0.125;
  renderer.render(scene, camera);
});

function buildRibbon(points) {
  let g = new THREE.PlaneGeometry(1, 1, segments, 1);

  let ps = [];
  let P = new THREE.Vector3(),
    N = new THREE.Vector3(),
    B = new THREE.Vector3();

  points.forEach((p) => {
    for (let i = 0; i <= segments; i++) {
      P = lpts[i];
      N.copy(frames.normals[i]).multiplyScalar(p.x);
      B.copy(frames.binormals[i]).multiplyScalar(p.y);
      ps.push(new THREE.Vector3().copy(P).add(N).add(B));
    }
  });

  g.setFromPoints(ps);
  return g;
}

function onWindowResize() {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize(innerWidth, innerHeight);
}
</script>