news 2026/6/26 10:41:33

Three.js 卷曲动画教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Three.js 卷曲动画教程

卷曲动画 ·Curl Animate· ▶ 在线运行案例

  • 案例合集:三维可视化功能案例(threehub.cn)
  • 开源仓库github地址:https://github.com/z2586300277/three-cesium-examples
  • 400个案例代码:网盘链接

你将学到什么

  • 相机交互控制器
  • 实时阴影 ShadowMap
  • 天空盒与环境贴图
  • requestAnimationFrame 渲染循环
  • Clock 帧间隔计时

效果说明

本案例演示卷曲动画效果:基于 WebGL 实现「卷曲动画」可视化效果,附完整可运行源码;核心用到 OrbitControls、BufferGeometry。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。

核心概念

  • OrbitControls轨道旋转缩放;开enableDamping时每帧需controls.update()
  • 阴影四步:renderer.shadowMap.enabled、光源castShadow、物体castShadow、地面receiveShadow
  • CubeTexture六面贴图作scene.backgroundscene.environment供 PBR 材质反射。

实现步骤

  • 搭建 Scene / Camera / Renderer 与 OrbitControls
  • rAF 循环中 update 并 render
  • 代码要点

    import * as THREE from 'three'

    import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' import { GUI } from 'dat.gui'

    const 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 / 2

    const positions = [] const indices = []

    const radiusAt = (a) => coreRadius + (a / (Math.PI2))stripThickness

    for (let i = 0; i <= totalSegs; i++) { let cx, cy, nx, ny

    if (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 = -totalAngle

    renderer.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 开源仓库
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/26 10:39:58

VMware虚拟机启动黑屏真相(ESXi/Workstation双平台实测数据曝光)

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;VMware虚拟机启动黑屏现象全景扫描 VMware虚拟机启动后仅显示黑色屏幕&#xff08;无光标、无提示、无图形界面&#xff09;是运维与开发人员高频遭遇的典型故障&#xff0c;其成因横跨宿主机配置、客户…

作者头像 李华
网站建设 2026/6/26 10:38:48

Jellyfin中文影视刮削终极指南:MetaShark插件完整配置教程

Jellyfin中文影视刮削终极指南&#xff1a;MetaShark插件完整配置教程 【免费下载链接】jellyfin-plugin-metashark jellyfin电影元数据插件 项目地址: https://gitcode.com/gh_mirrors/je/jellyfin-plugin-metashark 还在为Jellyfin无法正确识别中文电影电视剧而烦恼吗…

作者头像 李华
网站建设 2026/6/26 10:38:04

Golang安全工具集构建指南:从信息收集到后渗透的63个实战工具

1. 项目概述&#xff1a;为什么我们需要一个Golang黑客工具集&#xff1f;如果你是一名安全研究员、渗透测试工程师&#xff0c;或者是对网络安全充满好奇的开发者&#xff0c;那么你肯定经历过这样的场景&#xff1a;面对一个复杂的测试环境&#xff0c;你需要快速进行信息收集…

作者头像 李华
网站建设 2026/6/26 10:35:47

Windows Embedded CE 6.0 R3:高置信度嵌入式平台的设计哲学与工程实践

1. 项目概述&#xff1a;为什么今天还要看Windows Embedded CE 6.0 R3&#xff1f;如果你是一位从事工业控制、医疗设备、高端零售终端或早期物联网设备开发的工程师或技术决策者&#xff0c;看到“Windows Embedded CE 6.0 R3”这个标题&#xff0c;可能会觉得它有些“复古”。…

作者头像 李华
网站建设 2026/6/26 10:30:28

NXP QorIQ QMan队列管理器:硬件加速数据流转与性能优化详解

1. 项目概述&#xff1a;为什么我们需要一个专门的队列管理器&#xff1f;在嵌入式网络处理器&#xff0c;尤其是像NXP的QorIQ系列这样的高性能多核SoC中&#xff0c;数据包处理的速度和效率是决定系统性能的关键。想象一下&#xff0c;你有一个繁忙的十字路口&#xff0c;数据…

作者头像 李华