Skip to content

Commit

Permalink
refactor interpolation transition (#3540)
Browse files Browse the repository at this point in the history
* refactor interpolation transition

* fix transition setting normalization

* move padBuffer back to attribute-transition-utils with comment

* update tests
  • Loading branch information
Taylor Baldwin committed Sep 10, 2019
1 parent 757f704 commit 05cb2d9
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 283 deletions.
206 changes: 33 additions & 173 deletions modules/core/src/lib/attribute-transition-manager.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import GL from '@luma.gl/constants';
import {Buffer, Transform} from '@luma.gl/core';
import {getShaders, getBuffers, padBuffer} from './attribute-transition-utils';
import Attribute from './attribute';
import BaseAttribute from './base-attribute';
import Transition from '../transitions/transition';
import {Transform} from '@luma.gl/core';
import GPUInterpolationTransition from '../transitions/gpu-interpolation-transition';
import log from '../utils/log';
import assert from '../utils/assert';

export default class AttributeTransitionManager {
constructor(gl, {id, timeline}) {
Expand All @@ -14,9 +9,8 @@ export default class AttributeTransitionManager {
this.timeline = timeline;

this.transitions = {};
this.transforms = {};
this.needsRedraw = false;
this.numInstances = 0;
this.numInstances = 1;

if (Transform.isSupported(gl)) {
this.isSupported = true;
Expand All @@ -37,27 +31,24 @@ export default class AttributeTransitionManager {
// Called when attribute manager updates
// Check the latest attributes for updates.
update({attributes, transitions = {}, numInstances}) {
this.opts = transitions;
// Transform class will crash if elementCount is 0
this.numInstances = numInstances || 1;

if (!this.isSupported) {
return;
}

const changedTransitions = {};

for (const attributeName in attributes) {
const hasChanged = this._updateAttribute(attributeName, attributes[attributeName]);
const attribute = attributes[attributeName];
const settings = attribute.getTransitionSetting(transitions);

if (hasChanged) {
changedTransitions[attributeName] = this.transitions[attributeName];
}
// this attribute might not support transitions?
if (!settings) continue; // eslint-disable-line no-continue
this._updateAttribute(attributeName, attribute, settings);
}

for (const attributeName in this.transitions) {
const attribute = attributes[attributeName];

if (!attribute || !attribute.supportsTransition()) {
// Animated attribute has been removed
this._removeTransition(attributeName);
Expand All @@ -76,9 +67,8 @@ export default class AttributeTransitionManager {

for (const attributeName in this.transitions) {
const transition = this.transitions[attributeName];

if (transition.buffer) {
animatedAttributes[attributeName] = transition.attributeInTransition;
if (transition.isTransitioning()) {
animatedAttributes[attributeName] = transition.getTransitioningAttribute();
}
}

Expand All @@ -93,177 +83,47 @@ export default class AttributeTransitionManager {
return false;
}

const {transitions, transforms} = this;
let needsRedraw = this.needsRedraw;
this.needsRedraw = false;

for (const attributeName in transitions) {
const transition = transitions[attributeName];
const updated = transition.update();
for (const attributeName in this.transitions) {
const updated = this.transitions[attributeName].update();
if (updated) {
transforms[attributeName].run({
uniforms: {time: transition.time}
});
needsRedraw = true;
this.needsRedraw = true;
}
}

const needsRedraw = this.needsRedraw;
this.needsRedraw = false;
return needsRedraw;
}
/* eslint-enable max-statements */

/* Private methods */
_createTransition(attributeName, attribute) {
let transition = this.transitions[attributeName];
if (!transition) {
transition = new Transition({
name: attributeName,
timeline: this.timeline,
attribute,
// `attribute.userData` is the original options passed when constructing the attribute.
// This ensures that we set the proper `doublePrecision` flag and shader attributes.
attributeInTransition: new Attribute(this.gl, attribute.userData),
bufferLayout: attribute.bufferLayout
});
this.transitions[attributeName] = transition;
return transition;
}
return null;
}

_removeTransition(attributeName) {
const transition = this.transitions[attributeName];
if (transition) {
transition.cancel();
this.transforms[attributeName].delete();
if (transition.buffer) {
transition.buffer.delete();
}
if (transition._swapBuffer) {
transition._swapBuffer.delete();
}
delete this.transforms[attributeName];
delete this.transitions[attributeName];
}
this.transitions[attributeName].cancel();
delete this.transitions[attributeName];
}

// Check an attributes for updates
// Returns a transition object if a new transition is triggered.
_updateAttribute(attributeName, attribute) {
const settings = attribute.getTransitionSetting(this.opts);

if (settings) {
let hasChanged;
let transition = this.transitions[attributeName];
if (transition) {
hasChanged = attribute.needsRedraw();
_updateAttribute(attributeName, attribute, settings) {
let isNew = false;
if (!this.transitions[attributeName]) {
if (settings.type === 'interpolation') {
this.transitions[attributeName] = new GPUInterpolationTransition({
attribute,
timeline: this.timeline,
gl: this.gl
});
} else {
// New animated attributes have been added
transition = this._createTransition(attributeName, attribute);
hasChanged = true;
throw new Error(
`AttributeTransitionManager: unsupported transition type '${settings.type}'`
);
}

if (hasChanged) {
this._triggerTransition(transition, settings);
return true;
}
}

return false;
}

// get current values of an attribute, clipped/padded to the size of the new buffer
_getNextTransitionStates(transition, settings) {
const {attribute} = transition;
const {size, normalized} = attribute;
const multiplier = attribute.doublePrecision ? 2 : 1;

let toState;
if (attribute.constant) {
toState = new BaseAttribute(this.gl, {constant: true, value: attribute.value, size});
} else {
toState = new BaseAttribute(this.gl, {
constant: false,
buffer: attribute.getBuffer(),
divisor: 0,
size,
normalized
});
}
const fromState = transition.buffer || toState;
const toLength =
(attribute.userData.noAlloc ? attribute.value.length : this.numInstances * size) * multiplier;
const fromLength = transition.length || toLength;

// Alternate between two buffers when new transitions start.
// Last destination buffer is used as an attribute (from state),
// And the other buffer is now the destination buffer.
let buffer = transition._swapBuffer;
transition._swapBuffer = transition.buffer;

if (!buffer) {
buffer = new Buffer(this.gl, {
data: new Float32Array(toLength),
usage: GL.DYNAMIC_COPY
});
} else if (buffer.getElementCount() < toLength) {
// Pad buffers to be the same length with 32-bit floats
buffer.reallocate(toLength * 4);
isNew = true;
}

transition.length = toLength;
transition.attributeInTransition.update({
buffer,
// Hack: Float64Array is required for double-precision attributes
// to generate correct shader attributes
value: attribute.value
});

padBuffer({
fromState,
toState,
fromLength,
toLength,
size: size * multiplier,
fromBufferLayout: transition.bufferLayout,
toBufferLayout: attribute.bufferLayout,
offset: attribute.elementOffset * multiplier,
getData: settings.enter
});

transition.bufferLayout = attribute.bufferLayout;

return {fromState, toState, buffer};
}

// Start a new transition using the current settings
// Updates transition state and from/to buffer
_triggerTransition(transition, settings) {
// Check if settings is valid
assert(settings && settings.duration > 0);

this.needsRedraw = true;

// Attribute descriptor to transition from
transition.start(
Object.assign({}, this._getNextTransitionStates(transition, settings), settings)
);
let transform = this.transforms[transition.name];
const elementCount = Math.floor(transition.length / transition.attribute.size);

if (transform) {
transform.update({
...getBuffers(transition),
elementCount
});
} else {
// Buffers must be supplied to the transform constructor
transform = new Transform(this.gl, {
elementCount,
...getShaders(transition),
...getBuffers(transition)
});
this.transforms[transition.name] = transform;
if (isNew || attribute.needsRedraw()) {
this.needsRedraw = true;
this.transitions[attributeName].start(this.gl, settings, this.numInstances);
}
}
}
77 changes: 20 additions & 57 deletions modules/core/src/lib/attribute-transition-utils.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,37 @@
import {Buffer} from '@luma.gl/core';
import {padArray} from '../utils/array-utils';

const ATTRIBUTE_MAPPING = {
1: 'float',
2: 'vec2',
3: 'vec3',
4: 'vec4'
};

const noop = () => {};
const DEFAULT_TRANSITION_SETTINGS = {
type: 'interpolation',
duration: 0,
easing: t => t,
onStart: noop,
onEnd: noop,
onInterrupt: noop
onInterrupt: noop,
enter: x => x
};

export function normalizeTransitionSettings(settings) {
if (!settings) {
export function normalizeTransitionSettings(userSettings, layerSettings = {}) {
if (!userSettings) {
return null;
}
if (Number.isFinite(settings)) {
settings = {duration: settings};
}
if (!settings.duration) {
return null;
if (Number.isFinite(userSettings)) {
userSettings = {
type: 'interpolation',
duration: userSettings
};
}
return Object.assign({}, DEFAULT_TRANSITION_SETTINGS, settings);
}

const vs = `
#define SHADER_NAME feedback-vertex-shader
uniform float time;
attribute ATTRIBUTE_TYPE aFrom;
attribute ATTRIBUTE_TYPE aTo;
varying ATTRIBUTE_TYPE vCurrent;
void main(void) {
vCurrent = mix(aFrom, aTo, time);
gl_Position = vec4(0.0);
}
`;

export function getShaders(transition) {
const attributeType = ATTRIBUTE_MAPPING[transition.attribute.size];

return {
vs,
defines: {
ATTRIBUTE_TYPE: attributeType
},
varyings: ['vCurrent']
};
}

export function getBuffers(transition) {
return {
sourceBuffers: {
aFrom: transition.fromState,
aTo: transition.toState
},
feedbackBuffers: {
vCurrent: transition.buffer
}
};
return Object.assign({}, DEFAULT_TRANSITION_SETTINGS, layerSettings, userSettings);
}

// This helper is used when transitioning attributes from a set of values in one buffer layout
// to a set of values in a different buffer layout. (Buffer layouts are used when attribute values
// within a buffer should be grouped for drawElements, like the Polygon layer.) For example, a
// buffer layout of [3, 4] might have data [A1, A2, A3, B1, B2, B3, B4]. If it needs to transition
// to a buffer layout of [4, 2], it should produce a buffer, using the transition setting's `enter`
// function, that looks like this: [A1, A2, A3, A4 (user `enter` fn), B1, B2, 0]. Note: the final
// 0 in this buffer is because we never shrink buffers, only grow them, for performance reasons.
export function padBuffer({
fromState,
toState,
Expand All @@ -89,15 +53,14 @@ export function padBuffer({
const data = new Float32Array(toLength);
const fromData = fromState.getData({length: fromLength});

const {constant} = toState;
const toData = constant ? toState.getValue() : toState.getBuffer().getData({});
const toData = toState.constant ? toState.getValue() : toState.getBuffer().getData({});

if (toState.normalized) {
const getter = getData;
getData = (value, chunk) => toState._normalizeConstant(getter(value, chunk));
}

const getMissingData = constant
const getMissingData = toState.constant
? (i, chunk) => getData(toData, chunk)
: (i, chunk) => getData(toData.subarray(i, i + size), chunk);

Expand Down

0 comments on commit 05cb2d9

Please sign in to comment.