Three.js 卷曲动画教程

📅 2026/6/26 10:41:41
Three.js 卷曲动画教程
卷曲动画 ·Curl Animate· ▶ 在线运行案例案例合集三维可视化功能案例threehub.cn开源仓库github地址https://github.com/z2586300277/three-cesium-examples400个案例代码:网盘链接你将学到什么相机交互控制器实时阴影 ShadowMap天空盒与环境贴图requestAnimationFrame 渲染循环Clock 帧间隔计时效果说明本案例演示卷曲动画效果基于 WebGL 实现「卷曲动画」可视化效果附完整可运行源码核心用到 OrbitControls、BufferGeometry。建议先打开文首在线案例查看动态画面再对照下方源码逐步理解。核心概念OrbitControls轨道旋转缩放开enableDamping时每帧需controls.update()。阴影四步renderer.shadowMap.enabled、光源castShadow、物体castShadow、地面receiveShadow。CubeTexture六面贴图作scene.backgroundscene.environment供 PBR 材质反射。实现步骤搭建 Scene / Camera / Renderer 与 OrbitControlsrAF 循环中 update 并 render代码要点import * as THREE from threeimport { OrbitControls } from three/examples/jsm/controls/OrbitControls.js import { GUI } from dat.guiconst box document.getElementById(box) const scene new THREE.Scene() scene.background new THREE.Color(0x1a1a2e)const camera new THREE.PerspectiveCamera(50, box.clientWidth / box.clientHeight, 0.1, 1000) camera.position.set(0, 12, 20)const renderer new THREE.WebGLRenderer({ antialias: true }) renderer.setSize(box.clientWidth, box.clientHeight) // renderer.shadowMap.enabled true box.appendChild(renderer.domElement)new OrbitControls(camera, renderer.domElement)scene.add(new THREE.AmbientLight(0xffffff, 0.8)) const dirLight new THREE.DirectionalLight(0xffffff, 1.5) dirLight.position.set(5, 10, 5) dirLight.castShadow true scene.add(dirLight) scene.add(new THREE.DirectionalLight(0x4488ff, 0.5).position.set(-5, 5, -5))// 钢带材质 const stripMat new THREE.MeshStandardMaterial({ color: white, metalness: 0.7, roughness: 0.15, side: THREE.DoubleSide, // envMapIntensity: 1.0 })// 参数 const params { speed: 1.0, stripWidth: 4, thickness: 0.15, coreRadius: 1.5, maxTurns: 8, feedLength: 12, playing: true, reset() { totalAngle 0 } }let totalAngle 0 // 已卷绕的总角度(弧度) let stripMesh null// 构建钢带几何体水平进料段 螺旋卷绕段 function buildStripGeometry(angle, coreRadius, stripThickness, width, feedLen) { const segsPerRad 10 const coilSegs Math.max(Math.floor(angle * segsPerRad), 1) const feedSegs 20 const totalSegs feedSegs coilSegs const halfW width / 2 const halfT stripThickness / 2const positions [] const indices []const radiusAt (a) coreRadius (a / (Math.PI2))stripThicknessfor (let i 0; i totalSegs; i) { let cx, cy, nx, nyif (i feedSegs) { // 进料段水平从右侧远端到卷芯右侧 const t 1 - i / feedSegs cx radiusAt(angle) t * feedLen cy 0 nx 0 ny 1 } else { // 螺旋段从最外圈(angle)顺时针卷到最内圈(0) // 连续递减角度同时半径递减不会穿层 const t (i - feedSegs) / coilSegs // 0-1 const a angle * (1 - t) // angle-0 递减 const r radiusAt(a) // 顺时针旋转负角度 const theta -(angle - a) // 从0度开始顺时针转了多少 cx Math.cos(theta) * r cy Math.sin(theta) * r nx Math.cos(theta) ny Math.sin(theta) }positions.push(cx nxhalfT, cy nyhalfT, halfW) positions.push(cx nxhalfT, cy nyhalfT, -halfW) positions.push(cx - nxhalfT, cy - nyhalfT, halfW) positions.push(cx - nxhalfT, cy - nyhalfT, -halfW) }for (let i 0; i totalSegs; i) { const base i * 4 const next base 4 indices.push(base, next, base 1, base 1, next, next 1) indices.push(base 2, base 3, next 2, base 3, next 3, next 2) indices.push(base, base 2, next, next, base 2, next 2) indices.push(base 1, next 1, base 3, base 3, next 1, next 3) }indices.push(0, 1, 2, 1, 3, 2) const last totalSegs * 4 indices.push(last, last 2, last 1, last 1, last 2, last 3)const geo new THREE.BufferGeometry() geo.setAttribute(position, new THREE.Float32BufferAttribute(positions, 3)) geo.setIndex(indices) geo.computeVertexNormals() return geo }// 卷芯 const coreMat new THREE.MeshStandardMaterial({ color: 0x444444, metalness: 0.9, roughness: 0.2 }) const core new THREE.Mesh(new THREE.CylinderGeometry(params.coreRadius, params.coreRadius, params.stripWidth 0.2, 32), coreMat) core.rotation.x Math.PI / 2 scene.add(core)// 支撑辊 const rollerMat new THREE.MeshStandardMaterial({ color: 0x666666, metalness: 0.7, roughness: 0.4 }) const roller new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.3, params.stripWidth 1, 16), rollerMat) roller.rotation.x Math.PI / 2 roller.position.set(params.coreRadius params.feedLength * 0.5, -0.5, 0) scene.add(roller)function updateStrip() { if (stripMesh) { scene.remove(stripMesh) stripMesh.geometry.dispose() }const remainFeed params.feedLengthMath.max(1 - totalAngle / (params.maxTurnsMath.PI * 2), 0)const geo buildStripGeometry( totalAngle, params.coreRadius, params.thickness, params.stripWidth, remainFeed )stripMesh new THREE.Mesh(geo, stripMat) stripMesh.castShadow true stripMesh.receiveShadow true scene.add(stripMesh)// 更新卷芯大小 const outerR params.coreRadius (totalAngle / (Math.PI2))params.thickness core.scale.set(1, 1, 1) }// GUI const gui new GUI() gui.add(params, speed, 0.1, 5).name(速度) gui.add(params, coreRadius, 0.5, 4).name(卷芯半径).onChange(v { core.geometry.dispose() core.geometry new THREE.CylinderGeometry(v, v, params.stripWidth 0.2, 32) }) gui.add(params, thickness, 0.05, 0.5).name(钢带厚度) gui.add(params, maxTurns, 2, 20, 1).name(最大圈数) gui.add(params, stripWidth, 1, 8).name(钢带宽度).onChange(v { core.geometry.dispose() core.geometry new THREE.CylinderGeometry(params.coreRadius, params.coreRadius, v 0.2, 32) }) gui.add(params, playing).name(播放) gui.add(params, reset).name(重置)const clock new THREE.Clock()function animate() { requestAnimationFrame(animate) const delta clock.getDelta()const maxAngle params.maxTurnsMath.PI2 if (params.playing totalAngle maxAngle) { totalAngle deltaparams.speed2 totalAngle Math.min(totalAngle, maxAngle) updateStrip() }// 卷芯旋转钢带不转 core.rotation.y -totalAnglerenderer.render(scene, camera) } animate()window.onresize () { renderer.setSize(box.clientWidth, box.clientHeight) camera.aspect box.clientWidth / box.clientHeight camera.updateProjectionMatrix() }完整源码GitHub小结本文提供卷曲动画完整 Three.js 源码与在线 Demo建议先运行案例再改 uniform/参数做二次实验更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库