这是一段用 Three.js 写成的 3D 划船场景。画面里是一片青绿色的湖水,一条小船沿着椭圆形航线缓缓巡航,船上的人物随着节拍前后摆动身体、两支桨轮流入水,桨叶落点会荡开一圈圈涟漪,船尾则不断留下淡淡的白色尾迹。水面周围散布着睡莲与小花,它们会随着水波轻轻起伏。观众可以用鼠标拖拽绕场旋转,用滚轮拉近拉远,右上角还有一枚全屏按钮。整体是低多边形加柔和光照的清爽调性,适合放在首页顶部、加载页或产品故事页做氛围背景。
值得说明的是,整个场景没有加载任何外部模型文件——船体、人物、睡莲、花瓣全部由代码用基础几何体和 Shape / ExtrudeGeometry 现场拼出来,所以打包体积极小,颜色和比例也都能直接改参数。
这套 Three.js 划船场景值得留意的四个细节
水面是真的在动,而不是贴一张动图。脚本用一块高分段数的 PlaneGeometry 做湖面,每帧遍历顶点重新写入 Y 坐标,让整片水域按波函数起伏,法线也跟着重算,光泽会随波峰波谷移动。
划桨姿势由反向动力学算出来。源码里有一个 solveTwoBoneJoint 双骨骼求解器,只要给出肩、手的位置和上下臂长度,就能反推出肘关节该弯到哪里,手臂和腿都用它驱动,所以动作是连贯的而不是逐帧摆好的。
尾迹与涟漪走对象池。船尾的水花和桨叶涟漪都预先建好一批圆环网格,用完隐藏、循环复用,靠 age 值控制缩放与透明度衰减,长时间运行也不会持续新建对象。
漂浮物会跟着水面走。睡莲、小花、船体的高度都不是写死的,而是每帧调用同一个波高函数取值,所以水涨它们就抬,视觉上真的浮在同一片水上。
水面起伏和人物动作在源码里是怎么算出来的
其一,波形用的是多层正弦叠加。核心是一个接收坐标与时间的波高函数,把四条频率、方向、相位各不相同的正弦波加在一起,得到某一点此刻的水面高度:
const wave1 = Math.sin(x * 0.48 + time * 0.9) * 0.12; const wave2 = Math.sin(z * 0.41 - time * 0.7) * 0.08;
单条正弦看起来会很规整、很假,叠到三四条之后周期就错开了,眼睛便读不出重复感。振幅逐层递减是有意为之:第一条决定大轮廓,后面几条只负责打碎规律。想让湖面更平静就整体调小振幅,想做海面就加大第一条的系数。
其二,法线只隔帧重算一次。顶点位置每帧都更新,但 computeVertexNormals 是较重的操作,源码里用一个帧计数器做取模判断,让它两帧算一次。水波移动本来就慢,隔帧更新几乎看不出差别,帧率却能省下可观的开销——这是这类实时形变水面很典型的取舍。
其三,划桨节奏是一个统一的相位量。船的前冲速度、身体倾角、桨的扫掠角度、涟漪生成时机,全部由同一个不断累加的 rowingPhase 派生:用它的正弦值控制桨的前后摆动,用余弦值取正的部分作为“出力段”,只有出力时船才加速、桨叶入水才生成涟漪。所以整个动作是同步的,改一个数就能让全船划得更快或更慢。
拿到源码后能改哪些地方
配色最省事:脚本开头集中定义了一整组材质(船身、船木、桨、衣裤、帽子、睡莲等),每个都是一句带 color 的材质声明,搜索 Material 就能一次看全,换成品牌色几分钟的事。场景背景色与雾气颜色在 scene.background 与 scene.fog 两行,建议和水面材质一起改,否则远处会露出色差。
镜头行为在 OrbitControls 那一段:可以限制最近最远距离、锁定俯仰角范围、开关阻尼。如果只想做纯展示背景,把交互关掉再让相机自转即可。航线是一个由角度换算坐标的函数,两个系数分别是椭圆的长短半轴,改它们就能让船跑得更宽或更窄。
性能方面,水面网格的分段数、阴影贴图尺寸(源码里给到 2048)、像素比上限是三个最直接的调节点。移动端偏卡时优先降分段数和阴影贴图,观感损失最小。这些位置源码中都有英文注释分段标注(Water、Boat、Oars、Person、Wake 等),按注释找很快。
运行提示:页面通过 importmap 从 CDN 加载 Three.js 模块,且脚本以 type="module" 引入,因此必须用本地服务器打开,直接双击 index.html 会因模块跨域而空白。首次运行也需要能访问外网 CDN,若部署在内网,请把 Three.js 与 OrbitControls 下载到本地再改 importmap 路径。详细运行方法请参见压缩包内的帮助文档。
关于 3D 划船场景的常见问题
可以把整片水换成海蓝色或夜景吗?
可以。需要同时调整三处:水面材质的 color、场景背景色、雾气颜色。做夜景还要把平行光强度调低、色温改冷,并适当提高半球光的地面色,否则船底会黑成一片。
手机上跑得动吗?
中高端机型可以流畅运行,但这是实时 WebGL 渲染,低端机会掉帧。建议在移动端降低水面分段数与阴影贴图尺寸,或者干脆关掉阴影,观感差异不大而帧率提升明显。
能不能换成自己的船或人物模型?
可以。当前所有物体都是代码生成的,若要换成 glTF 模型,用 GLTFLoader 载入后替换对应的 Group 即可,但要注意保留脚本里读取波高来设置 Y 坐标的那部分逻辑,否则模型不会跟着水面浮动。
Safari 里画面偏暗或看不到阴影怎么办?
先确认浏览器版本,场景使用了 ACES 色调映射与物理材质,建议 Chrome、Edge、Firefox、Safari 均使用近两年的版本。若阴影缺失,多为设备 WebGL 上下文限制导致,可尝试把阴影类型换成开销更低的一种。
这段 3D 划船特效适合放在哪里
它的气质安静、有故事感,很适合做旅游与民宿站点的首页背景、环保或水资源主题的专题页、儿童教育产品的加载动画,也可以裁成一个卡片区块嵌在关于我们页面里。因为是纯代码建模,二次改造的自由度比导入模型高得多。完整实现(含水面形变、双骨骼求解、尾迹对象池与全屏控制)都在源码里,源码中均有分段注释,下载后可直接查看。