【问题标题】:Simple 3D cube roll animations not working with threejs简单的 3D 立方体滚动动画不适用于threejs
【发布时间】:2021-07-01 17:07:45
【问题描述】:

我是 THREEJS 的新手,目前我正在尝试使用箭头键移动立方体。请看这个小提琴:https://jsfiddle.net/mauricederegt/y6cw7foj/26/

一切正常,我可以使用箭头键移动立方体,我什至在移动立方体时设法围绕正确的轴旋转立方体。问题在于动画。我似乎无法让他们工作。在您按下左箭头键的那一刻,立方体向左移动并绕轴滚动。嗯……现在它会卡入到位,而不是平滑过渡。

我想要的是它在旋转时平滑地向左移动,但是怎么做呢?在代码的末尾,我确实调用了

requestAnimationFrame

但这并没有多大作用。我在这里尝试使用 CSS 进行此操作。这里的动画工作(但从来没有得到正确的旋转方向):https://jsfiddle.net/mauricederegt/5ozqg9uL/3/ 这确实显示了我想要的动画。

那么我在 THREEjs 中缺少什么?非常感谢

【问题讨论】:

    标签: javascript animation three.js


    【解决方案1】:

    您正在寻找的是一种称为“补间”的东西,您可以在其中绘制中间步骤,而不是立即跳到最终结果。有几个 JavaScript 库可以为您执行此操作,但我将介绍一些自己实现它的基础知识。

    以你为例。当您点击左箭头时,您将沿-x 轴移动网格1 单元,并围绕+y 轴旋转-PI/2。与其捕捉这些位置/旋转,不如考虑您希望动画持续多长时间,然后开始划分步骤。

    假设您要拍摄500ms(半秒)。您的浏览器尝试以大约60fps 运行,因此您有30 帧(大约500ms)以该速率运行。因此,对于每一帧,您可以移动框 1/30 单位,并将其旋转 -PI/60。在30 帧之后,框应该在正确的位置,给或取一些舍入。

    在谈论浏览器的帧率时,我使用“about”,因为不能保证您总是得到60FPS。如果您的帧率下降,并且您被锁定到帧率来绘制动画,那么它也会减慢并花费比您想要的更长的时间。那么有什么办法呢?

    与其依赖requestAnimationFrame 作为您的计时器,您可以设置一个真实 计时器来逐步完成您的动画。折腾计算完成动画所需的帧数,而是计算所需的步骤。

    我们已经知道60fps 大约是1 帧每16.6ms,所以这是浏览器可以绘制的绝对最大目标。但是,当我们逐步进行更新时,没有什么能阻止我们走得更快。为了使事情更容易计算,假设我们想要执行50 更新步骤,而不是之前的30。这意味着对于500ms 播放时间,我们需要每隔10ms 执行一次更新(比帧速率略快)。另外,因为我们执行了 50 步,我们将以1/50 为单位更新位置,并以-PI/100 为单位进行旋转。

    let animationId = setInterval( ()=>{
      // update position by 1/50 units
      // update rotation by -PI/100
    }, 10 ); // every 10 ms
    

    随着时间间隔的运行,它将更新对象。同时,动画循环会尽可能地生成新的帧。

    这是一个完整的运行示例,仅支持左箭头:

    let W = window.innerWidth;
    let H = window.innerHeight;
    
    const renderer = new THREE.WebGLRenderer({
      antialias: true,
      alpha: true
    });
    document.body.appendChild(renderer.domElement);
    
    const scene = new THREE.Scene();
    
    const camera = new THREE.PerspectiveCamera(28, 1, 1, 1000);
    camera.position.set(0, 0, 50);
    camera.lookAt(scene.position);
    scene.add(camera);
    
    const light = new THREE.DirectionalLight(0xffffff, 1);
    light.position.set(0, 0, -1);
    camera.add(light);
    
    const cube = new THREE.Mesh(
      new THREE.BoxBufferGeometry(1, 1, 1),
      new THREE.MeshPhongMaterial({
        color: "red"
      })
    );
    cube.position.set(10, 0, 0);
    scene.add(cube);
    
    function render() {
      renderer.render(scene, camera);
    }
    
    function resize() {
      W = window.innerWidth;
      H = window.innerHeight;
      renderer.setSize(W, H);
      camera.aspect = W / H;
      camera.updateProjectionMatrix();
      render();
    }
    
    window.addEventListener("resize", resize);
    
    resize();
    
    function animate() {
      requestAnimationFrame(animate);
      render();
    }
    requestAnimationFrame(animate);
    
    const yAxis = new THREE.Vector3(0, 1, 0);
    
    function updateCube() {
      //cube.position.x -= 1;
      //cube.rotateOnWorldAxis(yAxis, THREE.Math.degToRad(-90));
      cube.position.x -= 1 / 50;
      cube.rotateOnWorldAxis(yAxis, -(Math.PI / 100));
    }
    
    let step = 0;
    let animationId = null;
    
    function startStepping() {
      animationId = setInterval(() => {
        updateCube();
        if (++step === 50) {
          clearInterval(animationId);
          animationId = null;
        }
      }, 10)
    }
    
    function handleKeyboard(e) {
      //if (e.keyCode == 65 || e.keyCode == 37) {
      //  updateCube();
      //}
      if (animationId === null && (e.keyCode == 65 || e.keyCode == 37)) {
        step = 0;
        startStepping();
      }
    }
    document.addEventListener("keydown", handleKeyboard, false);
    html,
    body {
      width: 100%;
      height: 100%;
      padding: 0;
      margin: 0;
      overflow: hidden;
      background: skyblue;
    }
    <script src="https://threejs.org/build/three.min.js"></script>

    现在这种方法有一个缺点。有时您可能会看到更新被跳过(如果浏览器的帧率下降),或者相同的位置被绘制了两次(如果您的更新率明显低于浏览器的帧率)。您可以尝试两全其美,但从渲染循环中实时计算帧速率,并相应地调整帧步长,但此时您必须询问花费在计算这些统计数据上的额外时间是否真的会伤害尝试实现摇滚- 稳定的帧率锁定绘制率。

    根据按键输入切换

    由于您的键输入现在与绘图脱节,您现在需要某种标志来确定正在执行的操作。您的按键处理程序将设置该标志,然后updateCube 将根据该标志采取行动。比如:

    let action = null
    
    function startStepping(){
      // set up the interval...
      // but then also ensure the action stops after the animation plays:
      setTimeout( () => action = null, 500 );
    }
    
    function handleKeyboard(e){
      if (animationId === null) {
        step = 0;
    
        switch(e.keyCode){
          case 37:
          case 65:
            action = "left";
            break;
          // other keys...
        }
    
        startStepping();
      }
    }
    
    function updateCube(){
      switch(action){
        case "left":
          // move as if it's rolling left
          break;
        case "right":
          // move as if it's rolling right
          break;
        // etc. for the other directions
      }
    }
    

    【讨论】:

    • 非常感谢您的精彩解释。这很有帮助!!!
    • @Maurice 如果这回答了您的问题,请记得将其标记为已接受,以便其他人可以看到您的问题已得到回答。
    • 我还有一个问题。您的示例仅在左箭头支持下效果很好。我也将如何实现右箭头支持?我正在尝试实现右箭头支持,但我失败了。我尝试在 handleKeyboard 中添加另一个 IF,但这不会更新 updateCube() 函数,因为它位于 startStepping() 函数中。我在这里想念的东西一定很简单,但我对此很陌生。那么我怎样才能添加右箭头支持呢? (当我清楚如何添加右箭头支持时,我可以自己弄清楚)。非常感谢
    • 分阶段考虑。 1.确定您的方向并设置一些指示所选方向的标志。 2. 启动计时器。 3. 当定时器触发时,updateCube 应该使用方向标志以正确的方向更新。只要您有 startSteppinganimationId 检查阻止,它就会阻止其他输入中断动画,直到它完成。这有帮助吗?
    • updateCube 将包含您决定做什么的逻辑。除了根据键值来决定之外,它必须根据动作标志设置的内容来决定。请参阅我的答案底部的编辑。
    猜你喜欢
    • 2013-05-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-05
    • 1970-01-01
    • 1970-01-01
    • 2012-02-27
    相关资源
    最近更新 更多