第172期:three相机沿预定轨道拍摄

934 阅读3分钟

封面图

tutieshi_640x360_5s.gif

上次说到了室内的漫游效果需要给相机架设一条轨道,这里就这个简单的示例来实现一个相机跟随轨道运动的基本功能。

基本要实现的效果如封面图所示,创建一个十字结的通道,然后相机在通道内按照通道的轨迹变换位置。

需要用到的知识

这里我们需要用到 THREE.TubeGeometry() 管道几何体和GrannyKnot十字结。十字结是继承自Curve的子类。其源码如下:

class GrannyKnot extends Curve {

	getPoint( t, optionalTarget = new Vector3() ) {

		const point = optionalTarget;

		t = 2 * Math.PI * t;

		const x = - 0.22 * Math.cos( t ) - 1.28 * Math.sin( t ) - 0.44 * Math.cos( 3 * t ) - 0.78 * Math.sin( 3 * t );
		const y = - 0.1 * Math.cos( 2 * t ) - 0.27 * Math.sin( 2 * t ) + 0.38 * Math.cos( 4 * t ) + 0.46 * Math.sin( 4 * t );
		const z = 0.7 * Math.cos( 3 * t ) - 0.4 * Math.sin( 3 * t );

		return point.set( x, y, z ).multiplyScalar( 20 );

	}

}

我们可以用它建造一条十字结的曲线。

TubeGeometry中所需的参数如下:

  • path: 需要继承THREE.Curve类
  • tublarSegments: 沿曲线的线段数
  • radius: 管道的半径
  • radialSegments: 横截面中的分段数
  • closed: 是否关闭管道

开始实现

在真正开始实现之前,我们依旧要先创建场景、相机、渲染器这些基本的东西。

// 场景
const scene = new THREE.Scent()


// 相机
const camera = new THREE.PerspectiveCamera(60, (window.innerWidth / window.innerHeight), 1, 1000)
camera.position.set(0, 4, 57)
camera.lookAt(0, 1.5, 0);


// 渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true, })
renderer.setSize(window.innerWidth, window.innerHeight);

OrbitDom.value.appendChild(renderer.domElement)

因为我是用vue3j进行开发,所以在这个组件中定义了一个OrbitDom的ref ,用来承载渲染器的dom,也就是canvase。

此时我们刷新界面,发现界面上什么都没有,如图:

image.png

这里有个辅助线,是因为我在场景中添加了坐标轴的辅助器。

// 坐标辅助线
const axesHelper = new THREE.AxesHelper(500)
scene.add(axesHelper)

因为我们相机设置的lookAt为0,1.5,0所以看起来就是这种效果。

接下来我们用GrannyKnot创建一条十字结曲线。

// 十字结曲线

const curve = new GrannyKnot()
console.log('curve', curve)

我们可以在控制台简单查看一下该曲线,它是一个继承自Curve的GrannyKnot对象。

image.png

曲线并不会直接展示到场景中,我们需要用到TubeGeometry这个几何体,TubeGeometry可以沿着threejs 中的曲线创建一条管道,代码如下:


const curve = new GrannyKnot()
console.log('curve', curve)

const geometry = new THREE.TubeGeometry(
  curve, 100, 2, 8, true
)
const material = new THREE.MeshBasicMaterial({
  color: 0xffffff,
  wireframe: true,
  side: THREE.DoubleSide
})
const tube = new THREE.Mesh(geometry, material)
scene.add(tube)

这时候刷新界面,我们就可以得到下面的效果:

tutieshi_640x360_3s.gif

接下来我们需要让相机沿着这个通道不断的变换位置,我们定义一个方法moveCamrea,然后在该方法中计算相机的位置:

// 移动相机
const clock = new THREE.Clock()
const moveCamera = () => {
  const time = clock.getElapsedTime()
  const looptime = 100
  const t = (time % looptime) / looptime
  const t2 = (time + 0.1) % looptime / looptime
  const pos = tube.geometry.parameters.path.getPointAt(t)
  const pos2 = tube.geometry.parameters.path.getPointAt(t2)

  camera.position.copy(pos)
  camera.lookAt(pos2)
}

Clock是THREEjs中核心方法中的时钟对象,你可以理解为它用来保持对时间的一个记录。

getElapsedTime这个方法用来获取时钟启动后经过的秒数,并将.oldTime属性设置为当前时间。如果.autoStart属性为true且时钟未运行,则也会启动时钟。

我们定义time 用来获取当前时间,设置循环时间为100,然后定义

const t = (time % looptime) / looptime

当前时间节点,用来计算相机当前所在的位置的时间点

接下来定义t2

const t2 = (time + 0.1) % looptime / looptime

来定义相机要看向的位置的时间点

然后通过getPointAt方法来获取两个时间点的位置。

  const pos = tube.geometry.parameters.path.getPointAt(t)
  const pos2 = tube.geometry.parameters.path.getPointAt(t2)

最后我们设置相机的位置,和lookAt()的坐标。

  camera.position.copy(pos)
  camera.lookAt(pos2)

这样,我们就基本完成了移动相机的方法。但是此时我们还不能达到封面图中的效果,我们需要一个动画来不断的更新相机的位置。

这就要用到requestAnimationFrame方法,我们定义update方法:

const update = () => {
  requestAnimationFrame(update)
  moveCamera()
  renderer.render(scene, camera)
}

在update方法中调用moveCamera并且让renderer重新渲染,此时,执行update方法,我们就可以得到封面中的效果。

tutieshi_640x360_5s.gif

即:相机按照预定的十字结曲线进行拍摄

最后

这里用到的知识如下:

  • TubeGeometry 管道几何体
  • GrannyKnot 曲线
  • Clock 时间轨迹
  • 以及如何计算相机的位置

谢谢大家~