Three.js 中的动画文本带
Animated text ribbons in Three.js
我是 three.js 的新手,不是数学天才。但是,我挑战自己通过构建动画文本丝带来学习它。
我正在尝试构建的是这样的:
基本想法:
- 沿着自定义路径创建至少两条光滑的织物状丝带。
- 丝带的正面和背面可以有不同的纹理
- 丝带的路径应该是封闭的,这样我就可以 show/hide 在无尽的动画中分段。
动画:
我想显示 1/3 的色带,然后沿着路径设置动画以显示下一部分,同时隐藏之前的部分。所以 text/material 在丝带露出时似乎在房间中处于静止位置。如果你看下面我的例子,我有点用 setdrawRange
来实现这个。但是,在之前的范围结束之前,我还不能再次显示第一段。这会导致路径末尾的动画跳转。
这是我目前的情况:
代码沙盒:
https://codesandbox.io/s/ribbons-w73zt
我正在努力解决的问题:
- 我的方法是使用 TubeGeometry 并只给它两个径向段。这是合法的,还是有更好的方法来实现我想要实现的目标?
- 不知何故,我无法在该管的
FrontSide
和 BackSide
上放置不同的纹理。如果我尝试根据文档将它们添加为数组,则根本不会显示任何纹理。我在这里错过了什么?
- 如何使用
setdrawRange
在路径的end/start处创建一个没有跳跃的无尽动画效果?希望我想说的是可以理解的。上面的演示应该使问题形象化。
- 出于某种原因,管道的“关闭”选项在结尾和开头之间创建了一个奇怪的连接,但段数较少。这会导致纹理被拉伸。我怎样才能避免这种情况?
- 像这样使用 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>
我是 three.js 的新手,不是数学天才。但是,我挑战自己通过构建动画文本丝带来学习它。
我正在尝试构建的是这样的:
基本想法:
- 沿着自定义路径创建至少两条光滑的织物状丝带。
- 丝带的正面和背面可以有不同的纹理
- 丝带的路径应该是封闭的,这样我就可以 show/hide 在无尽的动画中分段。
动画:
我想显示 1/3 的色带,然后沿着路径设置动画以显示下一部分,同时隐藏之前的部分。所以 text/material 在丝带露出时似乎在房间中处于静止位置。如果你看下面我的例子,我有点用 setdrawRange
来实现这个。但是,在之前的范围结束之前,我还不能再次显示第一段。这会导致路径末尾的动画跳转。
这是我目前的情况:
代码沙盒: https://codesandbox.io/s/ribbons-w73zt
我正在努力解决的问题:
- 我的方法是使用 TubeGeometry 并只给它两个径向段。这是合法的,还是有更好的方法来实现我想要实现的目标?
- 不知何故,我无法在该管的
FrontSide
和BackSide
上放置不同的纹理。如果我尝试根据文档将它们添加为数组,则根本不会显示任何纹理。我在这里错过了什么? - 如何使用
setdrawRange
在路径的end/start处创建一个没有跳跃的无尽动画效果?希望我想说的是可以理解的。上面的演示应该使问题形象化。 - 出于某种原因,管道的“关闭”选项在结尾和开头之间创建了一个奇怪的连接,但段数较少。这会导致纹理被拉伸。我怎样才能避免这种情况?
- 像这样使用 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>