Skip to content

Commit 350f7a0

Browse files
authored
Fixes for loading quantized glTF data (playcanvas#3431)
* dequantize animation curves * flatten strided morph data * enable flatten in more places * remove old comment * rename function * dequantize bounding box and morph positions * handle normlized positional data correctly when calculating mesh bounds from bones * lint
1 parent 17197ba commit 350f7a0

File tree

2 files changed

+114
-27
lines changed

2 files changed

+114
-27
lines changed

src/resources/parser/glb-parser.js

Lines changed: 90 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,37 @@ const gltfToEngineSemanticMap = {
147147
'TEXCOORD_1': SEMANTIC_TEXCOORD1
148148
};
149149

150+
// returns a function for dequantizing the data type
151+
const getDequantizeFunc = (srcType) => {
152+
// see https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization#encoding-quantized-data
153+
switch (srcType) {
154+
case TYPE_INT8: return (x) => Math.max(x / 127.0, -1.0);
155+
case TYPE_UINT8: return (x) => x / 255.0;
156+
case TYPE_INT16: return (x) => Math.max(x / 32767.0, -1.0);
157+
case TYPE_UINT16: return (x) => x / 65535.0;
158+
default: return (x) => x;
159+
}
160+
};
161+
162+
// dequantize an array of data
163+
const dequantizeArray = function (dstArray, srcArray, srcType) {
164+
const convFunc = getDequantizeFunc(srcType);
165+
const len = srcArray.length;
166+
for (let i = 0; i < len; ++i) {
167+
dstArray[i] = convFunc(srcArray[i]);
168+
}
169+
return dstArray;
170+
};
171+
150172
// get accessor data, making a copy and patching in the case of a sparse accessor
151-
const getAccessorData = function (gltfAccessor, bufferViews) {
173+
const getAccessorData = function (gltfAccessor, bufferViews, flatten = false) {
152174
const numComponents = getNumComponents(gltfAccessor.type);
153175
const dataType = getComponentDataType(gltfAccessor.componentType);
154176
if (!dataType) {
155177
return null;
156178
}
179+
180+
const bufferView = bufferViews[gltfAccessor.bufferView];
157181
let result;
158182

159183
if (gltfAccessor.sparse) {
@@ -165,15 +189,15 @@ const getAccessorData = function (gltfAccessor, bufferViews) {
165189
count: sparse.count,
166190
type: "SCALAR"
167191
};
168-
const indices = getAccessorData(Object.assign(indicesAccessor, sparse.indices), bufferViews);
192+
const indices = getAccessorData(Object.assign(indicesAccessor, sparse.indices), bufferViews, true);
169193

170194
// data values data
171195
const valuesAccessor = {
172196
count: sparse.count,
173197
type: gltfAccessor.scalar,
174198
componentType: gltfAccessor.componentType
175199
};
176-
const values = getAccessorData(Object.assign(valuesAccessor, sparse.values), bufferViews);
200+
const values = getAccessorData(Object.assign(valuesAccessor, sparse.values), bufferViews, true);
177201

178202
// get base data
179203
if (gltfAccessor.hasOwnProperty('bufferView')) {
@@ -185,7 +209,7 @@ const getAccessorData = function (gltfAccessor, bufferViews) {
185209
type: gltfAccessor.type
186210
};
187211
// make a copy of the base data since we'll patch the values
188-
result = getAccessorData(baseAccessor, bufferViews).slice();
212+
result = getAccessorData(baseAccessor, bufferViews, true).slice();
189213
} else {
190214
// there is no base data, create empty 0'd out data
191215
result = new dataType(gltfAccessor.count * numComponents);
@@ -197,8 +221,23 @@ const getAccessorData = function (gltfAccessor, bufferViews) {
197221
result[targetIndex * numComponents + j] = values[i * numComponents + j];
198222
}
199223
}
224+
} else if (flatten && bufferView.hasOwnProperty('byteStride')) {
225+
// flatten stridden data
226+
const bytesPerElement = numComponents * dataType.BYTES_PER_ELEMENT;
227+
const storage = new ArrayBuffer(gltfAccessor.count * bytesPerElement);
228+
const tmpArray = new Uint8Array(storage);
229+
230+
let dstOffset = 0;
231+
for (let i = 0; i < gltfAccessor.count; ++i) {
232+
// no need to add bufferView.byteOffset because accessor takes this into account
233+
let srcOffset = (gltfAccessor.byteOffset || 0) + i * bufferView.byteStride;
234+
for (let b = 0; b < bytesPerElement; ++b) {
235+
tmpArray[dstOffset++] = bufferView[srcOffset++];
236+
}
237+
}
238+
239+
result = new dataType(storage);
200240
} else {
201-
const bufferView = bufferViews[gltfAccessor.bufferView];
202241
result = new dataType(bufferView.buffer,
203242
bufferView.byteOffset + (gltfAccessor.byteOffset || 0),
204243
gltfAccessor.count * numComponents);
@@ -207,6 +246,42 @@ const getAccessorData = function (gltfAccessor, bufferViews) {
207246
return result;
208247
};
209248

249+
// get accessor data as (unnormalized, unquantized) Float32 data
250+
const getAccessorDataFloat32 = function (gltfAccessor, bufferViews) {
251+
const data = getAccessorData(gltfAccessor, bufferViews, true);
252+
if (data instanceof Float32Array || !gltfAccessor.normalized) {
253+
// if the source data is quantized (say to int16), but not normalized
254+
// then reading the values of the array is the same whether the values
255+
// are stored as float32 or int16. so probably no need to convert to
256+
// float32.
257+
return data;
258+
}
259+
260+
const float32Data = new Float32Array(data.length);
261+
dequantizeArray(float32Data, data, getComponentType(gltfAccessor.componentType));
262+
return float32Data;
263+
};
264+
265+
// returns a dequantized bounding box for the accessor
266+
const getAccessorBoundingBox = function (gltfAccessor) {
267+
let min = gltfAccessor.min;
268+
let max = gltfAccessor.max;
269+
if (!min || !max) {
270+
return null;
271+
}
272+
273+
if (gltfAccessor.normalized) {
274+
const ctype = getComponentType(gltfAccessor.componentType);
275+
min = dequantizeArray([], min, ctype);
276+
max = dequantizeArray([], max, ctype);
277+
}
278+
279+
return new BoundingBox(
280+
new Vec3((max[0] + min[0]) * 0.5, (max[1] + min[1]) * 0.5, (max[2] + min[2]) * 0.5),
281+
new Vec3((max[0] - min[0]) * 0.5, (max[1] - min[1]) * 0.5, (max[2] - min[2]) * 0.5)
282+
);
283+
};
284+
210285
const getPrimitiveType = function (primitive) {
211286
if (!primitive.hasOwnProperty('mode')) {
212287
return PRIMITIVE_TRIANGLES;
@@ -619,7 +694,7 @@ const createSkin = function (device, gltfSkin, accessors, bufferViews, nodes, gl
619694
const ibp = [];
620695
if (gltfSkin.hasOwnProperty('inverseBindMatrices')) {
621696
const inverseBindMatrices = gltfSkin.inverseBindMatrices;
622-
const ibmData = getAccessorData(accessors[inverseBindMatrices], bufferViews);
697+
const ibmData = getAccessorData(accessors[inverseBindMatrices], bufferViews, true);
623698
const ibmValues = [];
624699

625700
for (i = 0; i < numJoints; i++) {
@@ -748,7 +823,7 @@ const createMesh = function (device, gltfMesh, accessors, bufferViews, callback,
748823

749824
// if mesh was not constructed from draco data, use uncompressed
750825
if (!vertexBuffer) {
751-
indices = primitive.hasOwnProperty('indices') ? getAccessorData(accessors[primitive.indices], bufferViews) : null;
826+
indices = primitive.hasOwnProperty('indices') ? getAccessorData(accessors[primitive.indices], bufferViews, true) : null;
752827
vertexBuffer = createVertexBuffer(device, primitive.attributes, indices, accessors, bufferViews, flipV, vertexBufferDict);
753828
primitiveType = getPrimitiveType(primitive);
754829
}
@@ -799,13 +874,7 @@ const createMesh = function (device, gltfMesh, accessors, bufferViews, callback,
799874
mesh.materialIndex = primitive.material;
800875

801876
let accessor = accessors[primitive.attributes.POSITION];
802-
const min = accessor.min;
803-
const max = accessor.max;
804-
const aabb = new BoundingBox(
805-
new Vec3((max[0] + min[0]) / 2, (max[1] + min[1]) / 2, (max[2] + min[2]) / 2),
806-
new Vec3((max[0] - min[0]) / 2, (max[1] - min[1]) / 2, (max[2] - min[2]) / 2)
807-
);
808-
mesh.aabb = aabb;
877+
mesh.aabb = getAccessorBoundingBox(accessor);
809878

810879
// morph targets
811880
if (canUseMorph && primitive.hasOwnProperty('targets')) {
@@ -816,18 +885,16 @@ const createMesh = function (device, gltfMesh, accessors, bufferViews, callback,
816885

817886
if (target.hasOwnProperty('POSITION')) {
818887
accessor = accessors[target.POSITION];
819-
options.deltaPositions = getAccessorData(accessor, bufferViews);
820-
options.deltaPositionsType = getComponentType(accessor.componentType);
821-
if (accessor.hasOwnProperty('min') && accessor.hasOwnProperty('max')) {
822-
options.aabb = new BoundingBox();
823-
options.aabb.setMinMax(new Vec3(accessor.min), new Vec3(accessor.max));
824-
}
888+
options.deltaPositions = getAccessorDataFloat32(accessor, bufferViews);
889+
options.deltaPositionsType = TYPE_FLOAT32;
890+
options.aabb = getAccessorBoundingBox(accessor);
825891
}
826892

827893
if (target.hasOwnProperty('NORMAL')) {
828894
accessor = accessors[target.NORMAL];
829-
options.deltaNormals = getAccessorData(accessor, bufferViews);
830-
options.deltaNormalsType = getComponentType(accessor.componentType);
895+
// NOTE: the morph targets can't currently accept quantized normals
896+
options.deltaNormals = getAccessorDataFloat32(accessor, bufferViews);
897+
options.deltaNormalsType = TYPE_FLOAT32;
831898
}
832899

833900
// name if specified
@@ -1223,9 +1290,7 @@ const createAnimation = function (gltfAnimation, animationIndex, gltfAccessors,
12231290

12241291
// create animation data block for the accessor
12251292
const createAnimData = function (gltfAccessor) {
1226-
const data = getAccessorData(gltfAccessor, bufferViews);
1227-
// TODO: this assumes data is tightly packed, handle the case data is interleaved
1228-
return new AnimData(getNumComponents(gltfAccessor.type), new data.constructor(data));
1293+
return new AnimData(getNumComponents(gltfAccessor.type), getAccessorDataFloat32(gltfAccessor, bufferViews));
12291294
};
12301295

12311296
const interpMap = {

src/scene/mesh.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
INDEXFORMAT_UINT16, INDEXFORMAT_UINT32,
99
PRIMITIVE_LINES, PRIMITIVE_TRIANGLES, PRIMITIVE_POINTS,
1010
SEMANTIC_BLENDINDICES, SEMANTIC_BLENDWEIGHT, SEMANTIC_COLOR, SEMANTIC_NORMAL, SEMANTIC_POSITION, SEMANTIC_TEXCOORD,
11-
TYPE_FLOAT32, TYPE_UINT8,
11+
TYPE_FLOAT32, TYPE_UINT8, TYPE_INT8, TYPE_INT16, TYPE_UINT16,
1212
typedArrayIndexFormats
1313
} from '../graphics/constants.js';
1414
import { IndexBuffer } from '../graphics/index-buffer.js';
@@ -284,7 +284,6 @@ class Mesh extends RefCountedObject {
284284
var weightsElement = iterator.element[SEMANTIC_BLENDWEIGHT];
285285
var indicesElement = iterator.element[SEMANTIC_BLENDINDICES];
286286

287-
288287
// Find bone AABBs of attached vertices
289288
for (j = 0; j < numVerts; j++) {
290289
for (k = 0; k < 4; k++) {
@@ -356,6 +355,29 @@ class Mesh extends RefCountedObject {
356355
iterator.next();
357356
}
358357

358+
// account for normalized positional data
359+
const positionElement = this.vertexBuffer.getFormat().elements.find((e) => e.name === SEMANTIC_POSITION);
360+
if (positionElement && positionElement.normalize) {
361+
const func = (() => {
362+
switch (positionElement.dataType) {
363+
case TYPE_INT8: return (x) => Math.max(x / 127.0, -1.0);
364+
case TYPE_UINT8: return (x) => x / 255.0;
365+
case TYPE_INT16: return (x) => Math.max(x / 32767.0, -1.0);
366+
case TYPE_UINT16: return (x) => x / 65535.0;
367+
default: return (x) => x;
368+
}
369+
})();
370+
371+
for (i = 0; i < numBones; i++) {
372+
if (boneUsed[i]) {
373+
const min = boneMin[i];
374+
const max = boneMax[i];
375+
min.set(func(min.x), func(min.y), func(min.z));
376+
max.set(func(max.x), func(max.y), func(max.z));
377+
}
378+
}
379+
}
380+
359381
// store bone bounding boxes
360382
for (i = 0; i < numBones; i++) {
361383
aabb = new BoundingBox();

0 commit comments

Comments
 (0)