Skip to content

Commit

Permalink
Fix support for luma.gl buffers as external attributes (#4121)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress committed Jan 9, 2020
1 parent 813e1b4 commit 15f69de
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 20 deletions.
2 changes: 2 additions & 0 deletions docs/api-reference/layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,11 @@ Each value in `data.attributes` may be one of the following formats:
- An object containing the following optional fields. For more information, see [WebGL vertex attribute API](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer).
+ `buffer` ([Buffer](https://luma.gl/docs/api-reference/webgl/buffer))
+ `value` (TypedArray)
+ `type` (GLenum) - A WebGL data type, see [vertexAttribPointer](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer#Parameters).
+ `size` (Number) - the number of elements per vertex attribute.
+ `offset` (Number) - offset of the first vertex attribute into the buffer, in bytes
+ `stride` (Number) - the offset between the beginning of consecutive vertex attributes, in bytes
+ `normalized` (Boolean) - whether data values should be normalized. Note that all color attributes in deck.gl layers are normalized by default.

**Remarks**

Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/lib/attribute/attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,10 @@ export default class Attribute extends DataColumn {
if (ArrayBuffer.isView(buffer)) {
buffer = {value: buffer};
}
assert(ArrayBuffer.isView(buffer.value), `invalid ${settings.accessor}`);
const needsUpdate = settings.transform || startIndices !== this.startIndices;

if (needsUpdate) {
assert(ArrayBuffer.isView(buffer.value), `invalid ${settings.accessor}`);
const needsNormalize = buffer.size && buffer.size !== this.size;

state.binaryAccessor = getAccessorFromBuffer(buffer.value, {
Expand Down
4 changes: 2 additions & 2 deletions modules/core/src/lib/attribute/data-column.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export default class DataColumn {
this.value = opts.value;

// Copy the type of the buffer into the accessor
accessor.type = buffer.accessor.type;
accessor.type = opts.type || buffer.accessor.type;
accessor.bytesPerElement = buffer.accessor.BYTES_PER_ELEMENT;
accessor.stride = getStride(accessor);
} else if (opts.value) {
Expand All @@ -243,7 +243,7 @@ export default class DataColumn {
// Hack: force Buffer to infer data type
buffer.setAccessor(null);
buffer.subData({data: value, offset: byteOffset});
accessor.type = buffer.accessor.type;
accessor.type = opts.type || buffer.accessor.type;
}

return true;
Expand Down
25 changes: 16 additions & 9 deletions modules/core/src/utils/tesselator.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {createIterable, getAccessorFromBuffer} from './iterable-utils';
import defaultTypedArrayManager from './typed-array-manager';
import assert from './assert';

import {Buffer} from '@luma.gl/core';

export default class Tesselator {
constructor(opts = {}) {
const {attributes = {}} = opts;
Expand Down Expand Up @@ -60,10 +62,7 @@ export default class Tesselator {

// Handle external logical value
if (geometryBuffer) {
assert(
ArrayBuffer.isView(geometryBuffer.value || geometryBuffer) && data.startIndices,
'invalid geometries'
);
assert(data.startIndices, 'binary data missing startIndices');
this.getGeometry = this.getGeometryFromBuffer(geometryBuffer);

if (!normalize) {
Expand Down Expand Up @@ -101,7 +100,10 @@ export default class Tesselator {
}

getGeometryFromBuffer(geometryBuffer) {
return getAccessorFromBuffer(geometryBuffer.value || geometryBuffer, {
const value = geometryBuffer.value || geometryBuffer;
assert(ArrayBuffer.isView(value), 'cannot read geometries');

return getAccessorFromBuffer(value, {
size: this.positionSize,
offset: geometryBuffer.offset,
stride: geometryBuffer.stride,
Expand Down Expand Up @@ -151,7 +153,7 @@ export default class Tesselator {
}

let {indexStarts, vertexStarts, instanceCount} = this;
const {geometryBuffer} = this;
const {data, geometryBuffer} = this;
const {startRow = 0, endRow = Infinity} = dataRange || {};

if (!dataRange) {
Expand All @@ -169,13 +171,18 @@ export default class Tesselator {
);
// count instances
instanceCount = vertexStarts[vertexStarts.length - 1];
} else if (geometryBuffer.buffer instanceof Buffer) {
const byteStride = geometryBuffer.stride || this.positionSize * 4;
// assume user provided data is already normalized
vertexStarts = data.startIndices;
instanceCount = vertexStarts[data.length] || geometryBuffer.buffer.byteLength / byteStride;
} else {
const bufferValue = geometryBuffer.value || geometryBuffer;
const bufferStride =
const elementStride =
geometryBuffer.stride / bufferValue.BYTES_PER_ELEMENT || this.positionSize;
// assume user provided data is already normalized
vertexStarts = this.data.startIndices;
instanceCount = bufferValue.length / bufferStride;
vertexStarts = data.startIndices;
instanceCount = vertexStarts[data.length] || bufferValue.length / elementStride;
}

// allocate attributes
Expand Down
3 changes: 1 addition & 2 deletions modules/layers/src/solid-polygon-layer/polygon-tesselator.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,8 @@ export default class PolygonTesselator extends Tesselator {
}

getGeometryFromBuffer(buffer) {
const getGeometry = super.getGeometryFromBuffer(buffer);
if (this.normalize || !this.buffers.indices) {
return getGeometry;
return super.getGeometryFromBuffer(buffer);
}
// we don't need to read the positions if no normalization/tesselation
return () => null;
Expand Down
24 changes: 18 additions & 6 deletions test/modules/core/lib/attribute/attribute.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -726,12 +726,24 @@ test('Attribute#setExternalBuffer', t => {
}),
'should set external buffer to attribute descriptor'
);
const attributeAccessor = attribute.getAccessor();
let attributeAccessor = attribute.getAccessor();
t.is(attributeAccessor.offset, 4, 'attribute accessor is updated');
t.is(attributeAccessor.stride, 8, 'attribute accessor is updated');
t.is(attribute.value, value1, 'external value is set');
t.is(attributeAccessor.type, GL.FLOAT, 'attribute type is set correctly');

t.ok(
attribute.setExternalBuffer({
offset: 4,
stride: 8,
value: value1,
type: GL.UNSIGNED_BYTE
}),
'should set external buffer to attribute descriptor'
);
attributeAccessor = attribute.getAccessor();
t.is(attributeAccessor.type, GL.UNSIGNED_BYTE, 'attribute type is set correctly');

buffer.delete();
attribute.delete();

Expand Down Expand Up @@ -831,11 +843,6 @@ test('Attribute#setBinaryValue', t => {
t.is(spy.callCount, 1, 'setData is called only once on the same data');
t.notOk(attribute.needsUpdate(), 'attribute is updated');

t.throws(
() => attribute.setBinaryValue([0, 1, 2, 3]),
'should throw if external value is invalid'
);

spy.reset();
attribute.delete();

Expand Down Expand Up @@ -865,6 +872,11 @@ test('Attribute#setBinaryValue', t => {
t.ok(attribute.state.binaryAccessor, 'binaryAccessor is assigned');
t.ok(attribute.needsUpdate(), 'attribute still needs update');

t.throws(
() => attribute.setBinaryValue([0, 1, 2, 3]),
'should throw if external value is invalid'
);

attribute.delete();
t.end();
});
Expand Down
59 changes: 59 additions & 0 deletions test/modules/layers/polygon-tesselation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import test from 'tape-catch';
import * as Polygon from '@deck.gl/layers/solid-polygon-layer/polygon';
import PolygonTesselator from '@deck.gl/layers/solid-polygon-layer/polygon-tesselator';

import {Buffer} from '@luma.gl/core';
import {gl} from '@deck.gl/test-utils';

const SAMPLE_DATA = [
{polygon: [], name: 'empty array'},
{polygon: [[1, 1]], name: 'too few points', height: 1, color: [255, 0, 0]},
Expand Down Expand Up @@ -340,3 +343,59 @@ test('PolygonTesselator#geometryBuffer', t => {

t.end();
});

test('PolygonTesselator#geometryBuffer#buffer', t => {
const buffer = new Buffer(gl, {
data: new Float32Array([1, 1, 2, 2, 3, 3, 0, 0, 2, 0, 2, 2, 0, 2, 0, 0])
});
const sampleData = {
length: 2,
startIndices: [0, 3],
attributes: {
getPolygon: {buffer, size: 2}
}
};
t.throws(
() =>
new PolygonTesselator({
data: sampleData,
buffers: sampleData.attributes,
geometryBuffer: sampleData.attributes.getPolygon,
positionFormat: 'XY'
}),
'throws on invalid options'
);

t.throws(
() =>
new PolygonTesselator({
data: sampleData,
buffers: sampleData.attributes,
geometryBuffer: sampleData.attributes.getPolygon,
normalize: false,
positionFormat: 'XY'
}),
'throws on invalid options'
);

sampleData.attributes.indices = new Uint16Array([0, 1, 2, 3, 4, 5]);

const tesselator = new PolygonTesselator({
data: sampleData,
buffers: sampleData.attributes,
geometryBuffer: sampleData.attributes.getPolygon,
normalize: false,
positionFormat: 'XY'
});

t.is(tesselator.instanceCount, 8, 'Updated instanceCount from geometryBuffer');
t.notOk(tesselator.get('positions'), 'skipped packing positions');
t.notOk(tesselator.get('indices'), 'skipped packing indices');
t.deepEquals(
tesselator.get('vertexValid').slice(0, 8),
[1, 1, 0, 1, 1, 1, 1, 0],
'vertexValid are populated'
);

t.end();
});

0 comments on commit 15f69de

Please sign in to comment.