JavaScript Three.js /逻辑-如何仅用本机JS在给定时间内将对象从一个位置补间到另一个

我基本上希望能够在准确的时间内将一个对象从开始位置移动到结束位置 ,例如,将一个立方体从{x:0,y:0, z:0}到{x:5,y:-3,z:10}恰好在750毫秒内。我需要能够计算精确到毫秒的运动,以便正确地将其与音频对齐,因此我需要能够计算从一帧到下一帧所花费的时间,并将其计入我的方程式中。我几乎可以正常工作,我不想粘贴我的整个项目,因为它依赖于几个文件,但是我会引用到目前为止的关键计算,但是请记住,下面的代码是不可测试,因为它取决于很多其他事情,但是我只想分享通用方法。在具有所有辅助功能的主文件中,我在setInterval(update,1000 / FPS)中设置了update()函数,并将FPS设置为60。

您可能已经猜到了,它每秒不会准确地发生60次,因为浏览器的性能有时会使速度降低几十毫秒,甚至更少,但仍然如此。因此,在我的更新函数中,它是这样的(我不确定要捕获哪些变量,无论是一次帧到下一帧的字面差异还是差异的比率,所以我现在都捕获了这两个变量):

var lastFrame = Date.now();
    var difference = 0;
    var deltaTime = 0;
    function update() {
        renderer.render(scene,camera);

        raycaster.setfromCamera(
            mouse.position3D,camera
        );

        things.forEach(x => {
            if(x.update) {

                x.update(x);
            }
        });
        //other general update things which take some time etc.
        deltaTime = Date.now() / lastFrame;
        difference = Date.now() - lastFrame;
        lastFrame = Date.now()
    }

因此您可以看到,我的所有THREE.js对象都嵌套在称为“事物”的包装对象数组中,并且这些“事物”中的每一个都有一个从主update函数调用的update函数。我们将在几秒钟后再讲到这一点,但首先是另一个主要辅助函数(这是问题的重点),补间函数(仍在此通用主辅助函数文件中):

Object.defineProperties(Object,{
    copy: {
        get() {
            return function(other,other2) {
                if(!(typeof other2 == "object")) {
                    var tmp = {};
                    for(var k in other) {
                        if(typeof other == "object") {
                            tmp[k] = other[k];
                        }
                    }
                    return tmp;
                } else {

                    for(var k in other2) {
                        other[k] = other2[k]
                    }
                    var o2a;
                    if(t(other2,String)) {
                        o2a = other2.split(",");
                    } else if(t(other2,Array)) {
                        o2a = other2
                    }
                //  console.log(o2a)
                    if(t(o2a,Array)) {
                        var r  = {};
                        o2a.forEach(k => {
                            r[k] = other[k];
                        })
                        return r
                    }
                }
            };
        }
    }

});

function myTween(opts) { 
    var th = this;
    if(!opts) opts = {};
    this.changeObj = opts.changeObj || {};
    var first = opts.first || {};
    this.second = opts.second || {};
    var originalTotalTime = opts.totalTime || 1000; 
    this.totalTime = originalTotalTime

    Object.defineProperties(this,{
        first: {
            get() {
                return first;
            },set(v) {
                first = v;
                Object.copy(obj,v);
                th.totalTime = originalTotalTime;
            }
        }
    });
    var rezs = {},rez = "going",obj = this.changeObj,hasReached = false,curTotal = this.totalTime;
    Object.copy(obj,first)
    this.reached = () => {
        if(t(opts.reached,Function)) {
            opts.reached(this);
        }
        if(hasReached) {
            curTotal = Date.now() - timeOflast
            console.log(
                originalTotalTime - curTotal,curTotal,diff
            );
            timeOflast = Date.now();
            hasReached = false;
        }

    };

    var lastF = Date.now(),diff = 1,timeOflast = lastF,totalTime = 0;

    this.update = () => {
        if(!hasReached) {
        //  timeOflast = Date.now()


            hasReached = true;
        }
        diff = Date.now() - lastF;


        rezs = {};
        rez = ""
        Object.keys(this.first).forEach(k => {

            var distanceNeeded = Math.abs(
                this.first[k] - this.second[k]
            ),curDist = Math.abs(obj[k] - this.second[k]),stepAmount = (
                ((curDist) /   
                    (
                        this.totalTime * (
                            1
                        )
                    )) * diff
                    *deltaTween * deltaTime
                    /*
                    (
                        (originalTotalTime - this.totalTime) /
                        (this.totalTime)
                    )*/
            ),leeway = stepAmount
            //console.log(stepAmount)
            this.totalTime -= diff / Object.keys(this.first).length;
            if(this.totalTime < 0) {
            //  this.reached()
            /// this.totalTime = originalTotalTime;

            }
            var ox = this.second[k];
            if(ox < obj[k] + leeway) {
                stepAmount = -Math.abs(stepAmount);
            } else if (ox > obj[k] - leeway) {
                stepAmount = Math.abs(stepAmount);
            }
            if(curDist < leeway) {
                obj[k] = this.second[k];
                stepAmount = 0;
                rezs[k] = "there";

            }

            obj[k] += stepAmount;
        });

        var count = 0;
        for(var r in rezs) {
            if(rezs[r] == "there") count++;
        }

        if(count > 0) {
            rez = "there";
        } else {
            rez = "going"
        }

        if(rez == "there") {
            this.reached();
        }

        lastF = Date.now();
    };
}

因此,此函数中发生了很多事情,但是希望一旦我在另一个文件中展示了如何使用这些细节,这些细节将变得显而易见。基本上,当在“事物”数组中创建新事物时,每个“事物”都具有一个更新功能(从主更新循环中更早调用)和一个启动功能,因此其用途如下:

new main.thing({
            color:"blue",position: {
                x:-3,y:-1
            },start(me) {
                me.ok = {

                            x:0,z:-2,y:4,};
                me.ko = {

                            y:-2,x:3,z:3
                        }
                me.done = []
                me.averages = [];
                me.notsoaverage = [];
                me.lolabuy = new main.myTween({
                    changeObj: me.position,first: me.ok,second: me.ko,totalTime: 200,reached(tween) {

                        tween.first = Object.copy(tween.second,"x,y,z");
                        tween.second = {
                            x: Math.random() * 8
                             - 4,z: Math.random() * 8
                             - 6,y: Math.random() * 8
                             - 4,}

                    }
                });
            },update(me) {

                me.lolabuy.update()
            }
        });

因此,基本上以前的函数myTween实际上是构造函数,并且从一开始就将新的myTween设置为“ me”对象,该对象引用“ things”(THREE.js对象的包装器)本身。补间的“第一个”设置为代表起始位置的“ me.ok”,而“第二个”则表示要补间到的位置,设置为变量“ me.ko”,当到达它时(从“到达”函数中调用),它将tween.first重置为旧的me.ko(也就是将新的开始位置更改为旧的结束位置),并将tween.second设置为新的随机生成的位置,它会不断重复和console.log-loging从一个到另一个的时间。 (您可能已经猜到“ totalTime”是它应该在一个补间和另一个补间之间花费的时间)。

它几乎可以完美地精确地工作,除了通常约16毫秒太少或太多(大约1000/60),或者有时少了几毫秒或几毫秒。这是一个问题,因为我需要精确到毫秒。

如果以上所有代码都太复杂而无法获取,则整个代码的关键部分是:

stepAmount = (
                    ((curDist) /   
                        (
                            this.totalTime * (
                                1
                            )
                        )) * diff
                        *deltaTween * deltaTime
                        /*
                        (
                            (originalTotalTime - this.totalTime) /
                            (this.totalTime)
                        )*/
                )

正确计算补间中每帧要拍摄的正确stepAmount。

不用说,我对使用诸如tween.js等之类的任何库感兴趣。

huohuangkate 回答:JavaScript Three.js /逻辑-如何仅用本机JS在给定时间内将对象从一个位置补间到另一个

大多数补间引擎不使用stepAmount也不保留任何状态(或不多的状态)。它们只有一个startTime和持续时间(或endTime和计算持续时间)。

因此,首先将其转换为归一化的(0到1)数字。

t = (currentTime - startTime) / duration

然后您用easing function修改t,

t = easingFunc(t)

然后您可以计算补间值

value = startValue + (endValue - startValue) * t

不需要状态。对于给定的time值,您将始终获得相同的结果。

这使您可以“擦洗”时间以显示动画的任何状态。

然后,他们可以选择在持续时间之前和之后如何处理t。常用选项是

  • 让我们继续

  • 就地循环

    t = t % 1
    
  • 走到尽头

    t = max(min(t,1),0)
    
  • 乒乓球

    t = (t % 2) < 1 ? t : 2 - t;
    

等...

此后,您需要一些选项来决定t = 1时的处理方式,例如从正在运行的动画列表中删除该动画或调用一个函数等。

const ctx = document.querySelector('canvas').getContext('2d');

const startValue = {x:0,y:0,z:0};
const endValue = {x:5,y:-3,z:10};
const duration = 750;
const startTime = 0;

function process(time) {
  // compute a normalized t value
  let t = (time - startTime) / duration;

  // remember if we hit 1.0 before doing the rest
  const isAtEnd = t >= 1;
  
  // decide how to handle t outside the current range
  t = t % 1;  // this is loop
  
  // apply easing
  t = easingFunc(t);
  
  // compute value
  const v = lerp3(startValue,endValue,t);
  
  // do something if we are at the end
  if (isAtEnd) {
     // remove tween from list of tweens?
     // call callback that tween is over?
     // chain to next tween?
     // etc...
  }
  
  // use value
  draw(v); 
}

function lerp3(start,end,t) {
  return {
    x: lerp(start.x,end.x,t),y: lerp(start.y,end.y,z: lerp(start.z,end.z,};
}

function lerp(start,t) {
  return start + (end - start) * t;
}

function easingFunc(t) {
  return t;  // just linear
}

function loop(time) {
  process(time);
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

function draw(v) {
  ctx.clearRect(0,ctx.canvas.width,ctx.canvas.height);
  ctx.save();
  
  const scale = 300 / 10;
  ctx.scale(scale,scale);
  ctx.translate(1,5);
  
  const size = v.z / 10;  // some rep for Z
  ctx.fillStyle = 'red';
  ctx.fillRect(v.x,v.y,size,size);
  
  ctx.restore();
}
canvas { border: 1px solid black }
<canvas></canvas>

有一些极端的情况要处理。例如,如果您使用乒乓球选项,则可能希望在t = 1时和t = 0时再次回调。

另一个人与t >= 1(可能是每一帧)和oldT < 1 && newT >= 1都没有分别。您可以全局存储oldTime

   oldT = (oldTime - startTime) / duration;
   t = (time - startTime) / duration;

   isAtEnd = oldT < 1 && t >= 1;

您说您不想使用一个很好的补间库,但是您可以在一个补间库中查看以了解他们在编写自己的库时正在做什么。

,

@gman在解释它如何工作方面做得很好。

我想提供一种使用“基于步骤的”补间的解决方案

class Tween {
    constructor ({from,to,change,steps,easeFn,duration,cb}) {
        this.from = from
        this.to = typeof to === 'undefined' ? from + change : to
        this.change = typeof change === 'undefined' ? to - from : change
        this.steps = steps
        this.step = 0
        this.duration = duration
        this.easeFn = typeof easeFn !== 'function' ? t => t : easeFn
        this.cb = typeof cb === 'undefined' ? () => {} : cb
        this.update = this.update.bind(this)
    }

    tick () { this.step < this.steps && this.step++ }

    update (now) {
        if (now > this.endTime) return this.stop()
        this.requestId = window.requestAnimationFrame(this.update)
        if (this.step < (now / this.endTime) * this.steps | 0) {
            this.tick()
            this.cb(this.value)
        }
    }

    start () {
        this.startTime = window.performance.now()
        this.endTime = this.startTime + this.duration
        this.requestId = window.requestAnimationFrame(this.update)
    }

    get value () {
        return this.from + (this.to - this.from) * this.easeFn(this.step / this.steps)
    }

    stop () { if (this.requestId) window.cancelAnimationFrame(this.requestId) }
}

我已经构建了它,以便您可以使用绝对最终值,也可以使用from属性的值更改。

cb属性将随着补间值的更新而被调用。

我什至抛出了一个可选的easeFn属性,因此您可以在简化值的同时仍保留该补间类的阶梯式性质。

// Example One (Using `from` and `to`)
const exampleOne = new Tween({
    from: -10,to: 0,steps: 5,duration: 1000,cb: v => console.log(v)
}).start()

// Example Two (Using `from` and `change`)
const exampleTwo = new Tween({
    from: -10,change: 10,cb: v => console.log(v)
}).start()

// Example Three (Using `easeFn`)
const exampleThree = new Tween({
    from: 0,to: 10,cb: v => console.log(v),easeFn: t => t < .5 ? Math.pow(t,2) / 2 : (1 - Math.pow(1 - (t * 2 - 1),2)) / 2 + .5
}).start()

如果您还有其他问题,请告诉我

本文链接:https://www.f2er.com/2832746.html

大家都在问