如何用p5.js实现点在3D球体表面沿大圆无畸变移动?
实现球面无畸变行走的解决方案
原有实现的问题
- 直接对球坐标的经纬度做线性增量更新,极区附近经度每单位角度对应的球面弧长会随纬度升高而缩短,自然会出现畸变
- 这种更新方式的移动路径也不是大圆,不符合球面行走的直观预期
核心实现思路
抛弃球坐标增量更新逻辑,改用三维向量+轴角旋转实现:
- 存储单位长度的三维向量表示当前点在球面上的位置,该向量始终和球心指向表面点的法线重合
- 存储当前的前进方向向量,该向量始终和位置向量垂直(处于球面该点的切平面上)
- 转向操作:将前进方向绕位置向量(法线)旋转对应角度,实现原地转向不位移
- 移动操作:将位置向量绕「前进方向的垂直切向量」旋转对应步长角度,移动路径自然是大圆,全程无经纬度映射带来的畸变
修改后的完整可运行代码
var scaler = 100; var moveSpeed = 0.02; // 每帧移动的角度(弧度) var turnSpeed = PI/64; // 每帧转向的角度 var MOVE_FORWARD = false; var TURN_LEFT = false; var TURN_RIGHT = false; // 核心状态:位置(单位向量)、前进方向(单位向量,和位置垂直) let pos; let forward; function setup() { createCanvas(400, 400, WEBGL); // 初始位置:(1,0,0) 单位向量 pos = createVector(1, 0, 0); // 初始前进方向:沿经度增加方向 forward = createVector(0, 1, 0); background(220); } function keyPressed(){ if(key == "w") MOVE_FORWARD = true; else if(keyCode == LEFT_ARROW) TURN_LEFT = true; else if(keyCode == RIGHT_ARROW) TURN_RIGHT = true; } function keyReleased(){ if(key == "w") MOVE_FORWARD = false; else if(keyCode == LEFT_ARROW) TURN_LEFT = false; else if(keyCode == RIGHT_ARROW) TURN_RIGHT = false; } // 轴角旋转函数:将向量v绕轴axis旋转angle弧度 function rotateVector(v, axis, angle) { let cos = Math.cos(angle); let sin = Math.sin(angle); // 罗德里格斯旋转公式 let x = axis.x, y = axis.y, z = axis.z; let cx = v.x * (cos + x*x*(1-cos)) + v.y * (x*y*(1-cos) - z*sin) + v.z * (x*z*(1-cos) + y*sin); let cy = v.x * (y*x*(1-cos) + z*sin) + v.y * (cos + y*y*(1-cos)) + v.z * (y*z*(1-cos) - x*sin); let cz = v.x * (z*x*(1-cos) - y*sin) + v.y * (z*y*(1-cos) + x*sin) + v.z * (cos + z*z*(1-cos)); return createVector(cx, cy, cz).normalize(); } function draw() { background(220); // 处理转向 if(TURN_LEFT) { // 绕位置向量(法线)顺时针旋转 forward = rotateVector(forward, pos, turnSpeed); } if(TURN_RIGHT) { forward = rotateVector(forward, pos, -turnSpeed); } // 处理前进 if(MOVE_FORWARD) { // 旋转轴是前进方向叉乘位置向量,得到切平面上垂直前进的右方向 let axis = p5.Vector.cross(forward, pos).normalize(); // 位置绕轴旋转移动步长 pos = rotateVector(pos, axis, moveSpeed); // 前进方向也同步旋转更新 forward = rotateVector(forward, axis, moveSpeed); } // 绘制球体和当前点 sphere(scaler); push(); translate(pos.x * scaler, pos.y * scaler, pos.z * scaler); fill(255,0,0); sphere(5); pop(); }
扩展功能说明
如果要添加左右平移、后退功能,逻辑也很简单:
- 后退就是把前进的旋转角度取负即可
- 左右平移就是计算出切平面上的右方向向量,然后绕前进方向旋转位置向量即可,不需要改动整体逻辑
内容的提问来源于stack exchange,提问作者Joe Dimagio
相关产品推荐
相关产品推荐

