Animations in interactive applications
When animating properties and values in applications and games, we generally interpolate values between two values(or keyframes etc.). This
This works well for predefined animations, but animating between states in interactive apps, it get's complicated to manage multiple animations and their states.
To get around this, we can animate between the current and the target value, instead of needing a start value. This solves the need of tracking multiple from states and durations.
Standard lerp
function lerp(start: number, end: number, t: number): number {
return start + (end - start) * t;
}
Now we can modify it to avoid the start value and use the current value instead. This way, we can animate towards a target value without needing to track the start value.
function lerpFromCurrent(current: number, target: number, t: number, dt): number {
const l = 1 - t // time left
const k = Math.max(0, Math.min(dt, l) / l)
return current + (end - current) * k
}
Here we assume t
is the current time in the animation, dt
is the delta time. They are both normalized in range 0-1 based on the animation duration. That means, if animation duration is 5 secs, current time is 2 secs, and delta time is 0.1 secs, then t = 2/5
and dt = 0.1/5
.
notes
// const td = t - dt
// const start = (val - (1 - td) * end) / td
// v = end * td + start * (1 - td)
// const start = td < 0 ? val : (val - td * end) / (1 - td)
// const i = delta / (end - time)
// const m = end - start
// end - time = m - (time - start)
// const duration = end - start
// i = delta * (end - start) / (end - start) * (end - time)
// = dt * 1 / (1 - t * 1)
// = dt * m / (m - (time - start))
// = dt * m / (end - time)
// = dt * (end - start) / (end - time)
// = dt * (time - start) / t(end - time)
Proper implementation
The implementation and type used in threepipe
export interface AnimateTime {t: number, dt: number, rm?: IRenderManager} // rm is used for textures, see below
export function lerpNumber(time: AnimateTime&{from?: number}, val: number, end: number): number {
const {t, dt} = time
let newVal
if (t <= 0) newVal = time.from !== undefined ? time.from : val
else if (t >= 1.) newVal = end
else if (time.from !== undefined) {
newVal = time.from * (1 - t) + end * t
} else {
const l = 1 - t
const k = Math.max(0, Math.min(dt, l) / l)
newVal = val + (end - val) * k
}
return newVal
}
The above function is to be used for numbers, and can be extended for types like Color
, Vector2
, Vector3
, etc. by interpolating their components.
Checkout the threepipe source for other types and arrays - https://github.com/repalash/threepipe/blob/f33b51f69ae35a42ced0a0d1a600fd24eafa6b5e/src/utils/lerp.ts
Blending textures
This technique can also be used to morph textures, either by blending between from
and to
textures, or from the current
texture(on a material) to the to
texture.
There are multiple ways to blend texture, but generally requires multiple render/copy passes. But when interpolating linearly, and can be reduced to form a * t + b * (1-t)
, it's possible to use the default blending in webgl/opengl to just blend once.
The sample code below uses the blit
method from the RenderManager
in threepipe, that copies a texture over a target render target.
export function lerpTexture(time: AnimateTime&{from?: ITexture|null}, val: ITexture|null, end: ITexture|null): ITexture |null {
if (!time.rm) {
console.warn('MaterialManager: RenderManager is required for interpolating textures')
return end
}
const {t, dt} = time
let newVal
const fromVal = time.from !== undefined ? time.from : val
if (t <= 0 || t > 0.99 || fromVal === end) {
newVal = t <= 0 ? fromVal : end
let rt
if (val && (val.userData as any)?._lerpTexture && val.isRenderTargetTexture) {
rt = val._target
}
if (rt) {
time.rm.releaseTempTarget(rt)
}
} else {
const val1 = fromVal || whiteTexture
let needsInit = false
let rt
if (val && (val.userData as any)?._lerpTexture && val.isRenderTargetTexture) {
rt = val._target // get the render target from the texture. (in threepipe its set automatically, otherwise it needs to be added manually in vanilla three.js)
} else {
const anyTex = fromVal || end || whiteTexture
const size = {
width: anyTex.image?.width || anyTex.image?.naturalWidth || anyTex.image?.videoWidth || 1,
height: anyTex.image?.height || anyTex.image?.naturalHeight || anyTex.image?.videoHeight || 1,
}
rt = time.rm.getTempTarget({
size: size,
colorSpace: anyTex.colorSpace,
type: anyTex.type,
format: anyTex.format,
generateMipmaps: anyTex.generateMipmaps,
minFilter: anyTex.minFilter,
magFilter: anyTex.magFilter,
wrapS: anyTex.wrapS,
wrapT: anyTex.wrapT,
});
(rt.texture as Texture).userData._lerpTexture = true
needsInit = true
}
if (rt) {
if (time.from !== undefined || needsInit) {
time.rm.blit(rt, {
source: val1,
respectColorSpace: false,
transparent: true,
clear: true,
})
}
const l = 1 - t
const k = Math.max(0, Math.min(dt, l) / l)
// newVal = val + (end - val) * k
// newVal = val * (1 - k) + end * k
time.rm.blit(rt, {
source: end || whiteTexture,
respectColorSpace: false,
transparent: true,
clear: false,
blendAlpha: time.from !== undefined ? t : k,
// blending: NormalBlending,
})
newVal = rt.texture as ITexture
} else {
newVal = val
}
}
return newVal
}
Animating between materials
Now that we have primitives to animate between numbers, colors, vectors, and textures, we can animate between materials as well.
To do this, collect the properties that need to be animated, and use the above functions to animate them.
In threepipe, the setValues
function in Material
is overloaded to accept an AnimateTime
object, if passed, the values are set based on the state.
Here is a sample using popmotion plugin(that syncs with other plugins properly), but it can be done using any animation loop.
let last = 0
await viewer.getPlugin(PopmotionPlugin).animate({
from: 0,
to: 1,
duration: 2000,
onUpdate: (v) => {
const dv = v - last
last = v
cube.material.setValues({ // this can be an object or another material.
color: new Color(0xffffff),
map: image,
normalMap: normalImage,
metalness: 1,
roughness: 0,
normalScale: new Vector2(1, 1),
}, true, undefined, {
t: v, dt: dv, // 0 to 1
rm: viewer.renderManager,
// optionally pass the `from` params/material.
// from: {
// color: ...
// ...
// }
})
},
}).promise
Checkout the threepipe material properties animation example for sample implementation and demo.