You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

设备陀螺仪姿态与Web端3D模型关联异常问题排查

问题

搭建模型火箭项目时,用旧手机通过Flask+Flask-SocketIO的Web应用读取陀螺仪数据:

  • 火箭端手机访问/rocket页面,通过SocketIO发送陀螺仪alpha、beta、gamma数据到服务器
  • 服务器将数据推送到/render页面的客户端,用ThreeJS修改GLTF模型的x/y/z轴旋转值来渲染
  • 仅手机平放(背部朝下)时功能正常,竖放(火箭实际姿态)时模型出现随机跳转等异常

相关代码

app.py

app = Flask(__name__)
socketio = SocketIO(app)

@app.route('/rocket')
def rocket():
    return render_template('rocket.html')

@socketio.on('send_data')
def handle_send_data(json):
    print(f"Received data: {json}")
    emit('update', json, broadcast=True) 

@app.route('/render')
def gltf_view():
    return render_template('render.html')

rocket.html

{% extends 'base.html' %}

{% block head %}
<title>Gyroscopic Data and G-Forces</title>
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<script src="{{ url_for('static', filename='rocket.js') }}"></script>
{% endblock %}

{% block body %}
<h1>Gyroscopic Data and G-Forces</h1>
<div>
    <h2>Gyroscope</h2>
    <p>Alpha (Rotation around Z-axis): <span id="alpha">0</span></p>
    <p>Beta (Rotation around X-axis): <span id="beta">0</span></p>
    <p>Gamma (Rotation around Y-axis): <span id="gamma">0</span></p>
</div>
<div>
    <h2>G-Forces</h2>
    <p>G-Forces: <span id="gforces">0</span> G</p>
</div>
{% endblock %}

rocket.js

document.addEventListener('DOMContentLoaded', (event) => {
    const socket = io();
    let previousGForce = 1

    if (window.DeviceOrientationEvent) {
        window.addEventListener('deviceorientation', handleOrientation);
    }

    if (window.DeviceMotionEvent) {
        window.addEventListener('devicemotion', handleMotion);
    }

    function handleOrientation(event) {
        const alpha = event.alpha.toFixed(2);
        const beta = event.beta.toFixed(2);
        const gamma = event.gamma.toFixed(2);

        document.getElementById('alpha').textContent = alpha;
        document.getElementById('beta').textContent = beta;
        document.getElementById('gamma').textContent = gamma;

        socket.emit('send_data', { type: 'orientation', alpha, beta, gamma });
    }

    function handleMotion(event) {
        const acceleration = event.accelerationIncludingGravity;
        const gForce = (Math.sqrt(acceleration.x ** 2 + acceleration.y ** 2 + acceleration.z ** 2) / 9.8).toFixed(2);

        if (Math.abs(gForce - previousGForce).toFixed(2) > 0.01) {
            document.getElementById('gforces').textContent = gForce;
            socket.emit('send_data', { type: 'motion', gForce });

            previousGForce = gForce
        }
    }
});

render.html

{% extends 'base.html' %}

{% block head %}
<title>GLTF Viewer</title>

<script type="importmap">
    {
        "imports": {
            "three": "https://cdn.jsdelivr.net/npm/three@0.166.1/build/three.module.js",
            "three/addons": "https://cdn.jsdelivr.net/npm/three@0.166.1/examples/jsm/loaders/GLTFLoader.js"
        }
    }
</script>
<script src="{{ url_for('static', filename='render.js') }}" type="module"></script>
{% endblock %}

{% block body %}
<h1>GLTF Model Viewer</h1>
<div id="container"></div>
{% endblock %}

render.js

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons';

const socket = io();
let scene, camera, renderer, model;

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.getElementById('container').appendChild(renderer.domElement);

    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
    directionalLight.position.set(0, 1, 1).normalize();
    scene.add(directionalLight);

    const loader = new GLTFLoader();

    loader.load("/static/rocket.gltf", function (gltf) {
        model = gltf.scene;
        model.scale.set(0.5, 0.5, 0.5);
        model.position.set(0, 0, 0);
        scene.add(model);
    }, undefined, function (error) {
        console.error(error);
    });

    camera.position.z = 5;
    animate();
}


function animate() {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
}

socket.on('update', function (data) {
    if (model && data.type === "orientation") {
        console.log(data)
        model.rotation.x = THREE.MathUtils.degToRad(data.gamma); // Yaw (turning)
        model.rotation.y = THREE.MathUtils.degToRad(data.alpha); // Roll (tilting sideways)
        model.rotation.z = THREE.MathUtils.degToRad(data.beta - 90); // Pit
    }
});

init();

问题原因分析

  1. 坐标系不匹配:手机DeviceOrientation的坐标系与ThreeJS的3D坐标系定义差异较大,竖放时beta值的范围变化会直接导致旋转映射逻辑失效。
  2. 欧拉角万向锁问题:直接用alpha/beta/gamma对应ThreeJS的rotation属性(欧拉角),当手机竖放时beta值接近±90°,会触发万向锁,导致模型旋转出现异常跳转。
  3. 数据精度丢失:rocket.js中用toFixed(2)将陀螺仪数据转为字符串传递,后续转回数字时丢失精度,加剧旋转计算的不稳定。

解决方法

1. 修复坐标系映射,用四元数替代欧拉角

ThreeJS的四元数(Quaternion)可以避免万向锁问题,同时需要重新映射手机姿态到3D模型的坐标系。

2. 保留原始数据精度

发送陀螺仪数据时不做字符串格式化,直接传递原始数值。

修改后的代码

rocket.js(仅修改handleOrientation和handleMotion函数)

function handleOrientation(event) {
    // 保留原始数值,仅在页面展示时格式化
    const alpha = event.alpha;
    const beta = event.beta;
    const gamma = event.gamma;

    document.getElementById('alpha').textContent = alpha.toFixed(2);
    document.getElementById('beta').textContent = beta.toFixed(2);
    document.getElementById('gamma').textContent = gamma.toFixed(2);

    socket.emit('send_data', { type: 'orientation', alpha, beta, gamma });
}

function handleMotion(event) {
    const acceleration = event.accelerationIncludingGravity;
    const gForce = Math.sqrt(acceleration.x ** 2 + acceleration.y ** 2 + acceleration.z ** 2) / 9.8;

    if (Math.abs(gForce - previousGForce) > 0.01) {
        document.getElementById('gforces').textContent = gForce.toFixed(2);
        socket.emit('send_data', { type: 'motion', gForce });

        previousGForce = gForce
    }
}

render.js(修改socket.on('update')部分)

socket.on('update', function (data) {
    if (model && data.type === "orientation") {
        // 转弧度
        const alpha = THREE.MathUtils.degToRad(data.alpha);
        const beta = THREE.MathUtils.degToRad(data.beta);
        const gamma = THREE.MathUtils.degToRad(data.gamma);

        // 手机坐标系转ThreeJS坐标系,使用四元数避免万向锁
        const quaternion = new THREE.Quaternion();
        quaternion.setFromEuler(new THREE.Euler(-beta, alpha, -gamma, 'YXZ'));
        
        // 设置初始旋转,匹配火箭竖放的初始姿态
        const initialRotation = new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI/2, 0, 0));
        model.quaternion.multiplyQuaternions(initialRotation, quaternion);
    }
});

内容的提问来源于stack exchange,提问作者SH3LDR0ID

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 00:07:34