mirror of
https://git.mirrors.martin98.com/https://github.com/google/draco
synced 2025-06-04 11:25:44 +08:00
A couple of more bug fixes to be included in the 1.5.7 release.
This commit is contained in:
parent
dcce9d0b3a
commit
c4ec84680d
@ -34,12 +34,6 @@
|
||||
#include <iostream>
|
||||
namespace draco {
|
||||
|
||||
#ifndef DISALLOW_COPY_AND_ASSIGN
|
||||
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
|
||||
TypeName(const TypeName &) = delete; \
|
||||
void operator=(const TypeName &) = delete;
|
||||
#endif // DISALLOW_COPY_AND_ASSIGN
|
||||
|
||||
#ifndef FALLTHROUGH_INTENDED
|
||||
#if defined(__clang__) && defined(__has_warning)
|
||||
#if __has_feature(cxx_attributes) && __has_warning("-Wimplicit-fallthrough")
|
||||
@ -67,6 +61,12 @@ namespace draco {
|
||||
|
||||
} // namespace draco
|
||||
|
||||
#ifndef DISALLOW_COPY_AND_ASSIGN
|
||||
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
|
||||
TypeName(const TypeName &) = delete; \
|
||||
void operator=(const TypeName &) = delete;
|
||||
#endif // DISALLOW_COPY_AND_ASSIGN
|
||||
|
||||
#ifdef DRACO_DEBUG
|
||||
#define DRACO_DCHECK(x) (assert(x));
|
||||
#define DRACO_DCHECK_EQ(a, b) assert((a) == (b));
|
||||
|
@ -313,7 +313,7 @@ Status CopyDataFromBufferView(const tinygltf::Model &model, int buffer_view_id,
|
||||
const uint8_t *const data_start = buffer.data.data() + buffer_view.byteOffset;
|
||||
|
||||
data->resize(buffer_view.byteLength);
|
||||
memcpy(&(*data)[0], data_start, buffer_view.byteLength);
|
||||
memcpy(data->data(), data_start, buffer_view.byteLength);
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
|
@ -1268,6 +1268,31 @@ TEST(GltfDecoderTest, DecodeMeshWithMeshFeaturesWithStructuralMetadata) {
|
||||
GltfTestHelper::CheckBoxMetaStructuralMetadata(*mesh, use_case);
|
||||
}
|
||||
|
||||
TEST(GltfDecoderTest, DecodeMeshWithStructuralMetadataWithEmptyStringBuffer) {
|
||||
// Checks that the decoder correctly handles 0-length buffers. An example case
|
||||
// where this could happen is an EXT_structural_metadata extension with a
|
||||
// buffer containing an empty string.
|
||||
const auto path =
|
||||
GetTestFileFullPath("ZeroLengthBufferView/ZeroLengthBufferView.gltf");
|
||||
GltfTestHelper::UseCase use_case;
|
||||
use_case.has_mesh_features = true;
|
||||
use_case.has_structural_metadata = true;
|
||||
|
||||
draco::GltfDecoder decoder;
|
||||
DRACO_ASSIGN_OR_ASSERT(auto mesh, decoder.DecodeFromFile(path));
|
||||
ASSERT_NE(mesh, nullptr);
|
||||
ASSERT_EQ(mesh->GetStructuralMetadata().NumPropertyTables(), 1);
|
||||
ASSERT_EQ(mesh->GetStructuralMetadata().GetPropertyTable(0).GetCount(), 1);
|
||||
ASSERT_EQ(mesh->GetStructuralMetadata().GetPropertyTable(0).NumProperties(),
|
||||
1);
|
||||
ASSERT_EQ(mesh->GetStructuralMetadata()
|
||||
.GetPropertyTable(0)
|
||||
.GetProperty(0)
|
||||
.GetData()
|
||||
.data.size(),
|
||||
0);
|
||||
}
|
||||
|
||||
TEST(GltfDecoderTest, DecodeMeshWithMeshFeaturesWithDracoCompression) {
|
||||
// Checks decoding of a simple glTF with mesh features compressed with Draco
|
||||
// as draco::Mesh.
|
||||
|
@ -387,10 +387,8 @@ class GltfAsset {
|
||||
std::unordered_map<int, int> *feature_id_name_indices);
|
||||
|
||||
// Iterate through the materials that are associated with |mesh| and add them
|
||||
// to the asset. Returns true if |mesh| does not contain any materials or all
|
||||
// the materials are supported. Returns false if |mesh| contains materials
|
||||
// that are not supported.
|
||||
bool AddMaterials(const Mesh &mesh);
|
||||
// to the asset.
|
||||
void AddMaterials(const Mesh &mesh);
|
||||
|
||||
// Checks whether a given Draco |attribute| has data of expected |data_type|
|
||||
// and whether the data has one of expected |num_components|. Returns true
|
||||
@ -428,10 +426,8 @@ class GltfAsset {
|
||||
Status AddSceneNode(const Scene &scene, SceneNodeIndex scene_node_index);
|
||||
|
||||
// Iterate through the materials that are associated with |scene| and add them
|
||||
// to the asset. Returns true if |scene| does not contain any materials or all
|
||||
// the materials are supported. Returns false if |scene| contains materials
|
||||
// that are not supported.
|
||||
bool AddMaterials(const Scene &scene);
|
||||
// to the asset.
|
||||
void AddMaterials(const Scene &scene);
|
||||
|
||||
// Iterate through the animations that are associated with |scene| and add
|
||||
// them to the asset. Returns OkStatus() if |scene| does not contain any
|
||||
@ -736,9 +732,7 @@ bool GltfAsset::AddDracoMesh(const Mesh &mesh) {
|
||||
if (scene_index < 0) {
|
||||
return false;
|
||||
}
|
||||
if (!AddMaterials(mesh)) {
|
||||
return false;
|
||||
}
|
||||
AddMaterials(mesh);
|
||||
|
||||
GltfMesh gltf_mesh;
|
||||
meshes_.push_back(gltf_mesh);
|
||||
@ -1286,10 +1280,13 @@ int GltfAsset::AddDracoNormals(const Mesh &mesh, int num_encoded_points) {
|
||||
int GltfAsset::AddDracoColors(const Mesh &mesh, int num_encoded_points) {
|
||||
const PointAttribute *const att =
|
||||
mesh.GetNamedAttribute(GeometryAttribute::COLOR);
|
||||
// TODO(b/200302561): Add support for DT_UINT16 with COLOR.
|
||||
if (!CheckDracoAttribute(att, {DT_UINT8, DT_FLOAT32}, {3, 4})) {
|
||||
if (!CheckDracoAttribute(att, {DT_UINT8, DT_UINT16, DT_FLOAT32}, {3, 4})) {
|
||||
return -1;
|
||||
}
|
||||
if (att->data_type() == DT_UINT16) {
|
||||
return AddAttribute<uint16_t>(*att, mesh.num_points(), num_encoded_points,
|
||||
mesh.IsCompressionEnabled());
|
||||
}
|
||||
if (att->data_type() == DT_FLOAT32) {
|
||||
return AddAttribute<float>(*att, mesh.num_points(), num_encoded_points,
|
||||
mesh.IsCompressionEnabled());
|
||||
@ -1460,12 +1457,10 @@ std::vector<std::pair<std::string, int>> GltfAsset::AddDracoGenerics(
|
||||
return attrs;
|
||||
}
|
||||
|
||||
bool GltfAsset::AddMaterials(const Mesh &mesh) {
|
||||
if (mesh.GetMaterialLibrary().NumMaterials() == 0) {
|
||||
return true;
|
||||
void GltfAsset::AddMaterials(const Mesh &mesh) {
|
||||
if (mesh.GetMaterialLibrary().NumMaterials()) {
|
||||
material_library_.Copy(mesh.GetMaterialLibrary());
|
||||
}
|
||||
material_library_.Copy(mesh.GetMaterialLibrary());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GltfAsset::CheckDracoAttribute(const PointAttribute *attribute,
|
||||
@ -1589,9 +1584,7 @@ Status GltfAsset::AddScene(const Scene &scene) {
|
||||
if (scene_index < 0) {
|
||||
return Status(Status::DRACO_ERROR, "Error creating a new scene.");
|
||||
}
|
||||
if (!AddMaterials(scene)) {
|
||||
return Status(Status::DRACO_ERROR, "Error adding materials to the scene.");
|
||||
}
|
||||
AddMaterials(scene);
|
||||
AddStructuralMetadata(scene);
|
||||
|
||||
// Initialize base mesh transforms that may be needed when the base meshes are
|
||||
@ -1695,12 +1688,10 @@ Status GltfAsset::AddSceneNode(const Scene &scene,
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
bool GltfAsset::AddMaterials(const Scene &scene) {
|
||||
if (scene.GetMaterialLibrary().NumMaterials() == 0) {
|
||||
return true;
|
||||
void GltfAsset::AddMaterials(const Scene &scene) {
|
||||
if (scene.GetMaterialLibrary().NumMaterials()) {
|
||||
material_library_.Copy(scene.GetMaterialLibrary());
|
||||
}
|
||||
material_library_.Copy(scene.GetMaterialLibrary());
|
||||
return true;
|
||||
}
|
||||
|
||||
Status GltfAsset::AddAnimations(const Scene &scene) {
|
||||
|
@ -407,7 +407,8 @@ bool ObjEncoder::EncodeFaceCorner(PointIndex vert_index) {
|
||||
}
|
||||
|
||||
void ObjEncoder::EncodeFloat(float val) {
|
||||
snprintf(num_buffer_, sizeof(num_buffer_), "%f", val);
|
||||
// Use %F instead of %f to make the floating point non-locale aware.
|
||||
snprintf(num_buffer_, sizeof(num_buffer_), "%F", val);
|
||||
buffer()->Encode(num_buffer_, strlen(num_buffer_));
|
||||
}
|
||||
|
||||
|
@ -32,8 +32,15 @@ class StlEncoderTest : public ::testing::Test {
|
||||
ASSERT_EQ(mesh0->num_faces(), mesh1->num_faces());
|
||||
ASSERT_EQ(mesh0->num_attributes(), mesh1->num_attributes());
|
||||
for (size_t att_id = 0; att_id < mesh0->num_attributes(); ++att_id) {
|
||||
ASSERT_EQ(mesh0->attribute(att_id)->size(),
|
||||
mesh1->attribute(att_id)->size());
|
||||
ASSERT_EQ(mesh0->attribute(att_id)->attribute_type(),
|
||||
mesh1->attribute(att_id)->attribute_type());
|
||||
// Normals are recomputed during STL encoding and they may not
|
||||
// correspond to the source ones.
|
||||
if (mesh0->attribute(att_id)->attribute_type() !=
|
||||
GeometryAttribute::NORMAL) {
|
||||
ASSERT_EQ(mesh0->attribute(att_id)->size(),
|
||||
mesh1->attribute(att_id)->size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,7 @@ class MeshSplitterInternal {
|
||||
struct WorkData : public MeshSplitter::WorkData {
|
||||
// TriangleSoupMeshBuilder or PointCloudBuilder.
|
||||
std::vector<BuilderT> builders;
|
||||
std::vector<int> *att_id_map;
|
||||
};
|
||||
|
||||
// Computes number of elements (faces or points) for each sub-mesh.
|
||||
@ -112,7 +113,8 @@ StatusOr<MeshSplitter::MeshVector> MeshSplitter::SplitMeshInternal(
|
||||
// Create the sub-meshes.
|
||||
work_data.builders.resize(num_out_meshes);
|
||||
// Map between attribute ids of the input and output meshes.
|
||||
work_data.att_id_map.resize(mesh.num_attributes(), -1);
|
||||
att_id_map_.resize(mesh.num_attributes(), -1);
|
||||
work_data.att_id_map = &att_id_map_;
|
||||
const int ignored_att_id =
|
||||
(!preserve_split_attribute ? split_attribute_id : -1);
|
||||
for (int mi = 0; mi < num_out_meshes; ++mi) {
|
||||
@ -185,10 +187,10 @@ void MeshSplitterInternal<BuilderT>::InitializeBuilder(
|
||||
continue;
|
||||
}
|
||||
const GeometryAttribute *const src_att = mesh.attribute(ai);
|
||||
work_data->att_id_map[ai] = work_data->builders[b_index].AddAttribute(
|
||||
(*work_data->att_id_map)[ai] = work_data->builders[b_index].AddAttribute(
|
||||
src_att->attribute_type(), src_att->num_components(),
|
||||
src_att->data_type(), src_att->normalized());
|
||||
work_data->builders[b_index].SetAttributeName(work_data->att_id_map[ai],
|
||||
work_data->builders[b_index].SetAttributeName(work_data->att_id_map->at(ai),
|
||||
src_att->name());
|
||||
}
|
||||
}
|
||||
@ -228,7 +230,7 @@ void AddElementToBuilder(
|
||||
const auto &face = mesh.face(source_i);
|
||||
for (int ai = 0; ai < mesh.num_attributes(); ++ai) {
|
||||
const PointAttribute *const src_att = mesh.attribute(ai);
|
||||
const int target_att_id = work_data->att_id_map[ai];
|
||||
const int target_att_id = work_data->att_id_map->at(ai);
|
||||
if (target_att_id == -1) {
|
||||
continue;
|
||||
}
|
||||
@ -245,7 +247,7 @@ void AddElementToBuilder(
|
||||
MeshSplitterInternal<PointCloudBuilder>::WorkData *work_data) {
|
||||
for (int ai = 0; ai < mesh.num_attributes(); ++ai) {
|
||||
const PointAttribute *const src_att = mesh.attribute(ai);
|
||||
const int target_att_id = work_data->att_id_map[ai];
|
||||
const int target_att_id = work_data->att_id_map->at(ai);
|
||||
if (target_att_id == -1) {
|
||||
continue;
|
||||
}
|
||||
@ -391,7 +393,7 @@ StatusOr<MeshSplitter::MeshVector> MeshSplitter::FinalizeMeshes(
|
||||
|
||||
// Copy over attribute unique ids.
|
||||
for (int att_id = 0; att_id < mesh.num_attributes(); ++att_id) {
|
||||
const int mapped_att_id = work_data.att_id_map[att_id];
|
||||
const int mapped_att_id = att_id_map_[att_id];
|
||||
if (mapped_att_id == -1) {
|
||||
continue;
|
||||
}
|
||||
@ -431,6 +433,14 @@ StatusOr<MeshSplitter::MeshVector> MeshSplitter::FinalizeMeshes(
|
||||
// Create a copy of source mesh features.
|
||||
std::unique_ptr<MeshFeatures> mf(new MeshFeatures());
|
||||
mf->Copy(mesh.GetMeshFeatures(mfi));
|
||||
|
||||
// Update mesh features attribute index if used.
|
||||
if (mf->GetAttributeIndex() != -1) {
|
||||
const int new_mf_attribute_index =
|
||||
att_id_map_[mf->GetAttributeIndex()];
|
||||
mf->SetAttributeIndex(new_mf_attribute_index);
|
||||
}
|
||||
|
||||
const MeshFeaturesIndex new_mfi =
|
||||
out_meshes[mi]->AddMeshFeatures(std::move(mf));
|
||||
if (work_data.split_by_materials && !preserve_materials_) {
|
||||
@ -548,7 +558,8 @@ StatusOr<MeshSplitter::MeshVector> MeshSplitter::SplitMeshToComponents(
|
||||
typename MeshSplitterInternal<TriangleSoupMeshBuilder>::WorkData work_data;
|
||||
work_data.builders.resize(num_out_meshes);
|
||||
work_data.num_sub_mesh_elements.resize(num_out_meshes, 0);
|
||||
work_data.att_id_map.resize(mesh.num_attributes(), -1);
|
||||
att_id_map_.resize(mesh.num_attributes(), -1);
|
||||
work_data.att_id_map = &att_id_map_;
|
||||
for (int mi = 0; mi < num_out_meshes; ++mi) {
|
||||
const int num_faces = connected_components.NumConnectedComponentFaces(mi);
|
||||
work_data.num_sub_mesh_elements[mi] = num_faces;
|
||||
@ -572,5 +583,9 @@ StatusOr<MeshSplitter::MeshVector> MeshSplitter::SplitMeshToComponents(
|
||||
return FinalizeMeshes(mesh, work_data, std::move(out_meshes));
|
||||
}
|
||||
|
||||
int MeshSplitter::GetSplitMeshAttributeIndex(int source_mesh_att_index) const {
|
||||
return att_id_map_[source_mesh_att_index];
|
||||
}
|
||||
|
||||
} // namespace draco
|
||||
#endif // DRACO_TRANSCODER_SUPPORTED
|
||||
|
@ -98,10 +98,13 @@ class MeshSplitter {
|
||||
StatusOr<MeshVector> SplitMeshToComponents(
|
||||
const Mesh &mesh, const MeshConnectedComponents &connected_components);
|
||||
|
||||
// Returns attribute index on each split mesh that corresponds to the
|
||||
// |source_mesh_att_index| of the source Mesh.
|
||||
// Must be called after SplitMesh() or SplitMeshToComponents().
|
||||
int GetSplitMeshAttributeIndex(int source_mesh_att_index) const;
|
||||
|
||||
private:
|
||||
struct WorkData {
|
||||
// Map between attribute ids of the input and output meshes.
|
||||
std::vector<int> att_id_map;
|
||||
std::vector<int> num_sub_mesh_elements;
|
||||
bool split_by_materials = false;
|
||||
};
|
||||
@ -120,6 +123,9 @@ class MeshSplitter {
|
||||
bool preserve_structural_metadata_;
|
||||
bool deduplicate_vertices_;
|
||||
|
||||
// Map between attribute ids of the input and output meshes.
|
||||
std::vector<int> att_id_map_;
|
||||
|
||||
template <typename BuilderT>
|
||||
friend class MeshSplitterInternal;
|
||||
};
|
||||
|
@ -317,6 +317,17 @@ StatusOr<std::unique_ptr<Scene>> SceneUtils::MeshToScene(
|
||||
// Copy over mesh features that were associated with the |material_index|.
|
||||
Mesh &scene_mesh = scene->GetMesh(mesh_index);
|
||||
Mesh::CopyMeshFeaturesForMaterial(*mesh, &scene_mesh, material_index);
|
||||
|
||||
// Update mesh features attribute indices if needed.
|
||||
for (MeshFeaturesIndex mfi(0); mfi < scene_mesh.NumMeshFeatures();
|
||||
++mfi) {
|
||||
auto &mesh_features = scene_mesh.GetMeshFeatures(mfi);
|
||||
if (mesh_features.GetAttributeIndex() != -1) {
|
||||
mesh_features.SetAttributeIndex(splitter.GetSplitMeshAttributeIndex(
|
||||
mesh_features.GetAttributeIndex()));
|
||||
}
|
||||
}
|
||||
|
||||
UpdateMeshFeaturesTexturesOnMesh(old_texture_to_index_map,
|
||||
&scene->GetNonMaterialTextureLibrary(),
|
||||
&scene_mesh);
|
||||
|
@ -403,6 +403,39 @@ TEST(SceneUtilsTest, TestMeshToSceneMultipleMeshFeatures) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SceneUtilsTest, TestMeshToSceneMeshFeaturesWithAttributes) {
|
||||
// Tests that converting a mesh into scene properly updates mesh features
|
||||
// attribute indices.
|
||||
auto mesh =
|
||||
draco::ReadMeshFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf");
|
||||
ASSERT_NE(mesh, nullptr);
|
||||
|
||||
// Add a new dummy mesh features and mesh features attribute to the mesh.
|
||||
std::unique_ptr<draco::PointAttribute> mf_att(new draco::PointAttribute());
|
||||
mf_att->Init(draco::GeometryAttribute::GENERIC, 1, draco::DT_FLOAT32, false,
|
||||
mesh->num_points());
|
||||
const int mf_att_id = mesh->AddAttribute(std::move(mf_att));
|
||||
std::unique_ptr<draco::MeshFeatures> mf(new draco::MeshFeatures());
|
||||
mf->SetAttributeIndex(mf_att_id);
|
||||
mesh->AddMeshFeatures(std::move(mf));
|
||||
|
||||
// Convert the mesh into a scene.
|
||||
DRACO_ASSIGN_OR_ASSERT(const std::unique_ptr<draco::Scene> scene_from_mesh,
|
||||
draco::SceneUtils::MeshToScene(std::move(mesh)));
|
||||
ASSERT_NE(scene_from_mesh, nullptr);
|
||||
|
||||
// Ensure the attribute indices on the meshes from scene are decremented by
|
||||
// one because the material attribute was removed.
|
||||
ASSERT_EQ(scene_from_mesh->NumMeshes(), 4);
|
||||
for (draco::MeshIndex mi(0); mi < scene_from_mesh->NumMeshes(); ++mi) {
|
||||
for (draco::MeshFeaturesIndex mfi(0);
|
||||
mfi < scene_from_mesh->GetMesh(mi).NumMeshFeatures(); ++mfi) {
|
||||
const auto &mf = scene_from_mesh->GetMesh(mi).GetMeshFeatures(mfi);
|
||||
ASSERT_EQ(mf.GetAttributeIndex(), mf_att_id - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SceneUtilsTest, TestMeshToSceneStructuralMetadata) {
|
||||
const std::string filename = "cube_att.obj";
|
||||
std::unique_ptr<draco::Mesh> mesh = draco::ReadMeshFromTestFile(filename);
|
||||
|
BIN
testdata/ZeroLengthBufferView/ZeroLengthBufferView.bin
vendored
Normal file
BIN
testdata/ZeroLengthBufferView/ZeroLengthBufferView.bin
vendored
Normal file
Binary file not shown.
130
testdata/ZeroLengthBufferView/ZeroLengthBufferView.gltf
vendored
Normal file
130
testdata/ZeroLengthBufferView/ZeroLengthBufferView.gltf
vendored
Normal file
@ -0,0 +1,130 @@
|
||||
{
|
||||
"asset": {
|
||||
"generator": "by_hand",
|
||||
"version": "2.0"
|
||||
},
|
||||
"scenes": [
|
||||
{
|
||||
"nodes": [
|
||||
0
|
||||
]
|
||||
}
|
||||
],
|
||||
"scene": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"mesh": 0
|
||||
}
|
||||
],
|
||||
"meshes": [
|
||||
{
|
||||
"primitives": [
|
||||
{
|
||||
"attributes": {
|
||||
"POSITION": 1
|
||||
},
|
||||
"indices": 0,
|
||||
"mode": 4,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"materials": [
|
||||
{
|
||||
"pbrMetallicRoughness": {
|
||||
"baseColorFactor": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"metallicFactor": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"accessors": [
|
||||
{
|
||||
"bufferView": 0,
|
||||
"componentType": 5121,
|
||||
"count": 6,
|
||||
"type": "SCALAR"
|
||||
},
|
||||
{
|
||||
"bufferView": 1,
|
||||
"componentType": 5126,
|
||||
"count": 4,
|
||||
"max": [
|
||||
1,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"type": "VEC3"
|
||||
}
|
||||
],
|
||||
"extensions": {
|
||||
"EXT_structural_metadata": {
|
||||
"schema": {
|
||||
"classes": {
|
||||
"class": {
|
||||
"properties": {
|
||||
"entity_id": {
|
||||
"type": "STRING"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"propertyTables": [
|
||||
{
|
||||
"class": "class",
|
||||
"count": 1,
|
||||
"properties": {
|
||||
"entity_id": {
|
||||
"values": 2,
|
||||
"stringOffsetType": "UINT8",
|
||||
"stringOffsets": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"propertyAttributes": []
|
||||
}
|
||||
},
|
||||
"bufferViews": [
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteOffset": 0,
|
||||
"byteLength": 8
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteOffset": 8,
|
||||
"byteLength": 48
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteOffset": 56,
|
||||
"byteLength": 0
|
||||
},
|
||||
{
|
||||
"buffer": 0,
|
||||
"byteOffset": 56,
|
||||
"byteLength": 4
|
||||
}
|
||||
],
|
||||
"buffers": [
|
||||
{
|
||||
"byteLength": 60,
|
||||
"uri": "ZeroLengthBufferView.bin"
|
||||
}
|
||||
],
|
||||
"extensionsUsed": [
|
||||
"EXT_structural_metadata"
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user