前言 本篇文章用threejs带大家实现一下无限隧道的效果。
实现逻辑 首先,我们要知道,如何才能无限,当然就是圆,而要实现隧道效果,自然就是需要的是立体的圆环,而我们的视角要在圆环里面。
初始化项目 ok,在知道了这个实现逻辑之后,我们先去初始化threejs项目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 <template> <div></div> </template> <script setup> import * as THREE from "three"; // 导入轨道控制器 import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; // 1.创建场景 const scene = new THREE.Scene(); // 2.创建相机 const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); // 设置相机位置 camera.position.set(0, 0, 10); // 添加相机 scene.add(camera); // 初始化渲染器 const renderer = new THREE.WebGLRenderer(); //设置渲染的尺寸大小 renderer.setSize(window.innerWidth, window.innerHeight); // 将webgl渲染的canvas内容添加到body上 document.body.appendChild(renderer.domElement); //当前是平面的,让平面立体起来,使用控制器controls // 创建轨道控制器 const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; // 添加坐标轴辅助器 const axesHelper = new THREE.AxesHelper(5); scene.add(axesHelper); // 持续渲染,让相机场景动起来 function render() { controls.update(); // 使用渲染器通过相机将场景渲染进来 renderer.render(scene, camera); // 渲染下一帧的时候就会调用render函数 requestAnimationFrame(render); } render(); // 监听画面的变化,更新渲染的画面 window.addEventListener("resize", () => { // 更新摄像头 camera.aspect = window.innerWidth / window.innerHeight; // 更新摄像机的投影矩阵 camera.updateProjectionMatrix(); // 更新渲染器 renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染器的像素比 renderer.setPixelRatio(window.devicePixelRatio); }); </script> <style> * { margin: 0; padding: 0; } canvas { width: 100vw; height: 100vh; position: fixed; top: 0; left: 0; } </style>
添加管道 我们可以看到,threejs中的管道比较符合我们的需求。
那我们先添加一个管道,下面这段是官网示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class CustomSinCurve extends THREE.Curve { constructor (scale = 1 ) { super (); this .scale = scale; } getPoint (t, optionalTarget = new THREE.Vector3() ) { const tx = t * 3 - 1.5 ; const ty = Math .sin (2 * Math .PI * t); const tz = 0 ; return optionalTarget.set (tx, ty, tz).multiplyScalar (this .scale ); } } const path = new CustomSinCurve (10 );const geometry = new THREE .TubeGeometry (path, 20 , 2 , 8 , false );const material = new THREE .MeshBasicMaterial ({ color : 0x00ff00 });const mesh = new THREE .Mesh (geometry, material);scene.add (mesh);
我们可以看到,管道已经被我添加进来了
将管道变成圆环 我们可以看到管道的获取点的函数,我们完全可以把这个getPoint改成我们自己的方法
原来的点位布置可以想象为,x方向一直变大,y方向按照sin曲线忽高忽低,z轴一直为0。可以简单理解为xy平面上的正弦运动。
我们将getPoint修改成为这样
1 2 3 const tx = Math .cos (2 * Math .PI * t);const ty = Math .sin (2 * Math .PI * t);const tz = 0.1 * Math .sin (8 * Math .PI * t);
这样的点位布置,xy平面上是一个圆,因为cos和sin的平方加起来是一个1,你可以理解,sin函数为1的时候cos为0,cos为1的时候sin为0,z方向设置了一个值,用来添加抖动。
然后我就得到了这样的一个立体圆环带一点点弧度的管道。这里我旋转了一下,为了大家方便看到z方向的变化。
添加一张贴图
这里添加完成纹理之后设置了wrapS和wrapT,让纹理在水平和垂直方向上进行重复,水平重复10次,垂直1次
1 2 3 4 5 6 7 const material = new THREE .MeshBasicMaterial ({ side : THREE .DoubleSide , map : new THREE .TextureLoader ().load ("/texture/texture.png" ), }); material.map .wrapS = THREE .RepeatWrapping material.map .wrapT = THREE .RepeatWrapping material.map .repeat .set (10 , 1 )
这样就得到了这样的一个管道
将视角移动到管道内 要实现穿越效果,我们要将摄像机放在模型内部,并沿着隧道路径运动 我们先实现视角放进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const startPoint = path.getPoint (0 );const offsetVector = new THREE .Vector3 (0 ,4 , 1 );camera.position .copy (startPoint).add (offsetVector); function render ( ) { controls.update (); camera.lookAt (startPoint); renderer.render (scene, camera); requestAnimationFrame (render); }
这里我的偏移三维向量(0,4,1)是基于我设置的参数进行设置的,你可以慢慢调整,直到你的视角在隧道内部就行。
添加运动效果 然后,我们添加运动效果,我们这时候就需要思考一下了,这个该如何实现,很明显就是我们需要根据t计算当前需要看的点以及不停调整偏移量
得到看的点 这个是简单的,我们设置time和speed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let time=0 ;let speed=0.02 ;function render ( ) { controls.update (); time+=speed const startPoint = path.getPoint (time); camera.position .copy (startPoint) camera.lookAt (startPoint); renderer.render (scene, camera); requestAnimationFrame (render); }
这时候得到的效果肯定是有问题的,因为我们没设置偏移量,因为我们把相机放到了构建管道的点上面,又让它看向这个点,也就是自己看到自己,这显然是不现实的。
偏移量思路分析 那这个偏移量该如何计算呢,其实我们可以换一个思路,我们其实就是想一直看到下一个点而已,那我们完全可以把相机放到前一个点位,然后看向下一个点位
实现代码 代码里面我都加了注释,大家自己理解一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 let time = 0 ;let binormal = new THREE .Vector3 ();let normal = new THREE .Vector3 ();let speed =10 ;function render ( ) { controls.update (); time += speed; let looptime = 20 * 1000 ; let t = (time % looptime) / looptime; let pos = geometry.parameters .path .getPointAt (t); let segments = geometry.tangents .length ; let pickt = t * segments; let pick = Math .floor (pickt); let pickNext = (pick + 1 ) % segments; binormal.subVectors (geometry.binormals [pickNext], geometry.binormals [pick]); binormal.multiplyScalar (pickt - pick).add (geometry.binormals [pick]); let dir = geometry.parameters .path .getTangentAt (t); normal.copy (binormal).cross (dir); camera.position .copy (pos); let lookAt = geometry.parameters .path .getPointAt ( (t + 1 / geometry.parameters .path .getLength ()) % 1 ); camera.matrix .lookAt (camera.position , lookAt, normal); camera.rotation .setFromRotationMatrix (camera.matrix , camera.rotation .order ); renderer.render (scene, camera); requestAnimationFrame (render); }
这个效果当然你可以用threejs自带的沿曲线运动实现,但是在弯道的部分效果过渡没有现在按照自己实现的丝滑。
给大家看看实现的效果
实现滚轮控制速度 这里我们不是有个speed吗,我们完全可以添加一个滚轮事件
1 2 3 document .addEventListener ('wheel' , e => { speed += e.deltaY * 0.02 })
完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 <template > <div > </div > </template > <script setup > import * as THREE from "three" ;import { OrbitControls } from "three/examples/jsm/controls/OrbitControls" ;const scene = new THREE .Scene ();const camera = new THREE .PerspectiveCamera ( 45 , window .innerWidth / window .innerHeight , 0.1 , 1000 ); camera.position .set (0 , 0 , 150 ); scene.add (camera); const renderer = new THREE .WebGLRenderer ();renderer.setSize (window .innerWidth , window .innerHeight ); document .body .appendChild (renderer.domElement );const controls = new OrbitControls (camera, renderer.domElement );controls.enableDamping = true ; const axesHelper = new THREE .AxesHelper (5 );scene.add (axesHelper); class CustomSinCurve extends THREE.Curve { constructor (scale = 1 ) { super (); this .scale = scale; } getPoint (t, optionalTarget = new THREE.Vector3() ) { const tx = Math .cos (2 * Math .PI * t); const ty = Math .sin (2 * Math .PI * t); const tz = 0.1 * Math .sin (8 * Math .PI * t); return optionalTarget.set (tx, ty, tz).multiplyScalar (this .scale ); } } const path = new CustomSinCurve (40 );const geometry = new THREE .TubeGeometry (path, 200 , 1 , 8 , false );const material = new THREE .MeshBasicMaterial ({ side : THREE .DoubleSide , map : new THREE .TextureLoader ().load ("/texture/texture.png" ), }); material.map .wrapS = THREE .RepeatWrapping ; material.map .wrapT = THREE .RepeatWrapping ; material.map .repeat .set (10 , 1 ); const mesh = new THREE .Mesh (geometry, material);scene.add (mesh); let time = 0 ;let binormal = new THREE .Vector3 ();let normal = new THREE .Vector3 ();let speed =10 ;function render ( ) { controls.update (); time += speed; let looptime = 20 * 1000 ; let t = (time % looptime) / looptime; let pos = geometry.parameters .path .getPointAt (t); let segments = geometry.tangents .length ; let pickt = t * segments; let pick = Math .floor (pickt); let pickNext = (pick + 1 ) % segments; binormal.subVectors (geometry.binormals [pickNext], geometry.binormals [pick]); binormal.multiplyScalar (pickt - pick).add (geometry.binormals [pick]); let dir = geometry.parameters .path .getTangentAt (t); normal.copy (binormal).cross (dir); camera.position .copy (pos); let lookAt = geometry.parameters .path .getPointAt ( (t + 1 / geometry.parameters .path .getLength ()) % 1 ); camera.matrix .lookAt (camera.position , lookAt, normal); camera.rotation .setFromRotationMatrix (camera.matrix , camera.rotation .order ); renderer.render (scene, camera); requestAnimationFrame (render); } render ();window .addEventListener ("resize" , () => { camera.aspect = window .innerWidth / window .innerHeight ; camera.updateProjectionMatrix (); renderer.setSize (window .innerWidth , window .innerHeight ); renderer.setPixelRatio (window .devicePixelRatio ); }); document .addEventListener ('wheel' , e => { speed += e.deltaY * 0.02 }) </script > <style > * { margin : 0 ; padding : 0 ; } canvas { width : 100vw ; height : 100vh ; position : fixed; top : 0 ; left : 0 ; } </style >
结语 通过这个管道,我们可以实现其他很多类似的效果,比如血管等等之类的。本篇文章就到这里了,更多内容敬请期待,债见~