diff --git a/demo/GLTFLoader.js b/demo/GLTFLoader.js index 1ae8b57bd..a200e786c 100644 --- a/demo/GLTFLoader.js +++ b/demo/GLTFLoader.js @@ -208,6 +208,10 @@ THREE.GLTFLoader = ( function () { extensions[ extensionName ] = new GLTFMeshQuantizationExtension(); break; + case EXTENSIONS.EXT_MESH_GPU_INSTANCING: + extensions[ extensionName ] = new GLTFMeshGPUInstancingExtension( json ); + break; + case EXTENSIONS.MESHOPT_COMPRESSION: extensions[ extensionName ] = new GLTFMeshoptCompressionExtension( this.meshoptDecoder ); break; @@ -291,6 +295,7 @@ THREE.GLTFLoader = ( function () { KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform', KHR_MESH_QUANTIZATION: 'KHR_mesh_quantization', MSFT_TEXTURE_DDS: 'MSFT_texture_dds', + EXT_MESH_GPU_INSTANCING: 'EXT_mesh_gpu_instancing', MESHOPT_COMPRESSION: 'MESHOPT_compression', }; @@ -926,6 +931,112 @@ THREE.GLTFLoader = ( function () { } + /** + * Instancing Extension + * + * Specification: https://github.com/KhronosGroup/glTF/pull/1691 + */ + function GLTFMeshGPUInstancingExtension() { + + this.name = EXTENSIONS.EXT_MESH_GPU_INSTANCING; + + } + + GLTFMeshGPUInstancingExtension.prototype.createInstancedMesh = function ( parser, nodeDef, node ) { + + var extensionDef = nodeDef.extensions[ this.name ]; + var pending = []; + + // If present, extensionDef.mesh overrides nodeDef.mesh. + var meshIndex = extensionDef.mesh !== undefined ? extensionDef.mesh : nodeDef.mesh; + var meshPromise = parser.getDependency( 'mesh', meshIndex ); + + // Load buffers for instance TRS properties. All are optional. + var translationPromise = extensionDef.attributes.TRANSLATION !== undefined + ? parser.getDependency( 'accessor', extensionDef.attributes.TRANSLATION ) + : null; + var rotationPromise = extensionDef.attributes.ROTATION !== undefined + ? parser.getDependency( 'accessor', extensionDef.attributes.ROTATION ) + : null; + var scalePromise = extensionDef.attributes.SCALE !== undefined + ? parser.getDependency( 'accessor', extensionDef.attributes.SCALE ) + : null; + + pending.push( meshPromise, translationPromise, rotationPromise, scalePromise ); + + // Load custom instance attributes. + var customAttributeNames = []; + for ( var attributeName in extensionDef.attributes ) { + + if ( attributeName[ 0 ] === '_' ) { + + customAttributeNames.push( attributeName.toLowerCase() ); + + pending.push( parser.getDependency( 'accessor', extensionDef.attributes[ attributeName ] ) ); + + } + + } + + return Promise.all( pending ) + .then( function ( dependencies ) { + + var mesh = dependencies[ 0 ]; + var translation = dependencies[ 1 ]; + var rotation = dependencies[ 2 ]; + var scale = dependencies[ 3 ]; + + var template = translation || rotation || scale || dependencies[ 4 ]; + + // Geometry must be cloned here, before we modify it. Material will be cloned if + // necessary, when assignFinalMaterial() is called below. + var instancedGeometry = customAttributeNames.length > 0 ? mesh.geometry.clone() : mesh.geometry; + + var instancedMesh = new THREE.InstancedMesh( instancedGeometry, mesh.material, template.count ); + var matrix = new THREE.Matrix4(); + + var t = new THREE.Vector3( 0, 0, 0 ); + var r = new THREE.Quaternion( 0, 0, 0, 1 ); + var s = new THREE.Vector3( 1, 1, 1 ); + + // Set instance transforms. + for ( var i = 0; i < template.count; i ++ ) { + + if ( translation ) t.fromBufferAttribute( translation, i ); + if ( rotation ) quaternionFromBufferAttribute( r, rotation, i ); + if ( scale ) s.fromBufferAttribute( scale, i ); + + instancedMesh.setMatrixAt( i, matrix.compose( t, r, s ) ); + + } + + // Set custom instance attributes. + for ( var i = 0; i < customAttributeNames.length; i ++ ) { + + var attributeSource = dependencies[ 4 + i ]; + + instancedGeometry.setAttribute( customAttributeNames[ i ], new THREE.InstancedBufferAttribute( + + attributeSource.array, + attributeSource.itemSize, + attributeSource.normalized + + ) ); + + } + + // Copy mesh properties first, then node properties. These may be the same object. + THREE.Object3D.prototype.copy.call( instancedMesh, mesh ); + THREE.Object3D.prototype.copy.call( instancedMesh, node ); + + parser.assignFinalMaterial( instancedMesh ); + + return instancedMesh; + + } ); + + } + /** * meshoptimizer Compression Extension */ @@ -1384,6 +1495,21 @@ THREE.GLTFLoader = ( function () { } + function quaternionFromBufferAttribute( quaternion, attribute, index ) { + + var scale = attribute.normalized ? 1 / 32767 : 1; + + return quaternion.set( + + attribute.getX( index ) * scale, + attribute.getY( index ) * scale, + attribute.getZ( index ) * scale, + attribute.getW( index ) * scale + + ); + + } + /* GLTF PARSER */ function GLTFParser( json, extensions, options ) { @@ -2027,6 +2153,7 @@ THREE.GLTFLoader = ( function () { var useVertexColors = geometry.attributes.color !== undefined; var useFlatShading = geometry.attributes.normal === undefined; var useSkinning = mesh.isSkinnedMesh === true; + var useInstancing = mesh.isInstancedMesh === true; var useMorphTargets = Object.keys( geometry.morphAttributes ).length > 0; var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined; @@ -2071,12 +2198,13 @@ THREE.GLTFLoader = ( function () { } // Clone the material if it will be modified - if ( useVertexTangents || useVertexColors || useFlatShading || useSkinning || useMorphTargets ) { + if ( useVertexTangents || useVertexColors || useFlatShading || useSkinning || useInstancing || useMorphTargets ) { var cacheKey = 'ClonedMaterial:' + material.uuid + ':'; if ( material.isGLTFSpecularGlossinessMaterial ) cacheKey += 'specular-glossiness:'; if ( useSkinning ) cacheKey += 'skinning:'; + if ( useInstancing ) cacheKey += 'instancing:'; if ( useVertexTangents ) cacheKey += 'vertex-tangents:'; if ( useVertexColors ) cacheKey += 'vertex-colors:'; if ( useFlatShading ) cacheKey += 'flat-shading:'; @@ -3141,6 +3269,20 @@ THREE.GLTFLoader = ( function () { } + // Apply instancing last, as it requires asynchronous resources. + if ( nodeDef.extensions && nodeDef.extensions[ EXTENSIONS.EXT_MESH_GPU_INSTANCING ] !== undefined ) { + + if ( ! node.isMesh && node.children.length > 0 ) { + + console.warn( 'THREE.GLTFLoader: Multi-primitive instanced meshes not yet supported.' ); + return node; + + } + + return extensions[ EXTENSIONS.EXT_MESH_GPU_INSTANCING ].createInstancedMesh( parser, nodeDef, node ); + + } + return node; } ); @@ -3290,4 +3432,4 @@ THREE.GLTFLoader = ( function () { return GLTFLoader; -} )(); \ No newline at end of file +} )(); diff --git a/gltf/gltfpack.cpp b/gltf/gltfpack.cpp index 973c2a6fc..baac51e64 100644 --- a/gltf/gltfpack.cpp +++ b/gltf/gltfpack.cpp @@ -75,6 +75,7 @@ static void printMeshStats(const std::vector& meshes, const char* name) { size_t triangles = 0; size_t vertices = 0; + size_t instanced = 0; for (size_t i = 0; i < meshes.size(); ++i) { @@ -82,9 +83,13 @@ static void printMeshStats(const std::vector& meshes, const char* name) triangles += mesh.indices.size() / 3; vertices += mesh.streams.empty() ? 0 : mesh.streams[0].data.size(); + + size_t instances = std::max(size_t(1), mesh.nodes.size() + mesh.instances.size()); + + instanced += mesh.indices.size() / 3 * instances; } - printf("%s: %d triangles, %d vertices\n", name, int(triangles), int(vertices)); + printf("%s: %d triangles (%lld instanced), %d vertices\n", name, int(triangles), (long long)instanced, int(vertices)); } static void printSceneStats(const std::vector& views, const std::vector& meshes, size_t node_offset, size_t mesh_offset, size_t material_offset, size_t json_size, size_t bin_size) @@ -99,9 +104,10 @@ static void printSceneStats(const std::vector& views, const std::vec printf("output: %d nodes, %d meshes (%d primitives), %d materials\n", int(node_offset), int(mesh_offset), int(meshes.size()), int(material_offset)); printf("output: JSON %d bytes, buffers %d bytes\n", int(json_size), int(bin_size)); - printf("output: buffers: vertex %d bytes, index %d bytes, skin %d bytes, time %d bytes, keyframe %d bytes, image %d bytes\n", + printf("output: buffers: vertex %d bytes, index %d bytes, skin %d bytes, time %d bytes, keyframe %d bytes, instance %d bytes, image %d bytes\n", int(bytes[BufferView::Kind_Vertex]), int(bytes[BufferView::Kind_Index]), int(bytes[BufferView::Kind_Skin]), - int(bytes[BufferView::Kind_Time]), int(bytes[BufferView::Kind_Keyframe]), int(bytes[BufferView::Kind_Image])); + int(bytes[BufferView::Kind_Time]), int(bytes[BufferView::Kind_Keyframe]), int(bytes[BufferView::Kind_Instance]), + int(bytes[BufferView::Kind_Image])); } static void printAttributeStats(const std::vector& views, BufferView::Kind kind, const char* name) @@ -126,6 +132,7 @@ static void printAttributeStats(const std::vector& views, BufferView break; case BufferView::Kind_Keyframe: + case BufferView::Kind_Instance: variant = animationPath(cgltf_animation_path_type(view.variant)); break; @@ -135,12 +142,9 @@ static void printAttributeStats(const std::vector& views, BufferView size_t count = view.data.size() / view.stride; printf("stats: %s %s: compressed %d bytes (%.1f bits), raw %d bytes (%d bits)\n", - name, - variant, - int(view.bytes), - double(view.bytes) / double(count) * 8, - int(view.data.size()), - int(view.stride * 8)); + name, variant, + int(view.bytes), double(view.bytes) / double(count) * 8, + int(view.data.size()), int(view.stride * 8)); } } @@ -165,24 +169,51 @@ static void process(cgltf_data* data, const char* input_path, const char* output for (size_t i = 0; i < meshes.size(); ++i) { Mesh& mesh = meshes[i]; + assert(mesh.instances.empty()); + + // mesh is already world space, skip + if (mesh.nodes.empty()) + continue; // note: when -kn is specified, we keep mesh-node attachment so that named nodes can be transformed - if (mesh.node && !settings.keep_named) + if (settings.keep_named) + continue; + + // we keep skinned meshes or meshes with morph targets as is + // in theory we could transform both, but in practice transforming morph target meshes is more involved, + // and reparenting skinned meshes leads to incorrect bounding box generated in three.js + if (mesh.skin || mesh.targets) + continue; + + bool any_animated = false; + for (size_t j = 0; j < mesh.nodes.size(); ++j) + any_animated |= nodes[mesh.nodes[j] - data->nodes].animated; + + // animated meshes will be anchored to the same node that they used to be in to retain the animation + if (any_animated) + continue; + + // we only merge multiple instances together if requested + // this often makes the scenes faster to render by reducing the draw call count, but can result in larger files + if (mesh.nodes.size() > 1 && !settings.mesh_merge && !settings.mesh_instancing) + continue; + + // prefer instancing if possible, use merging otherwise + if (mesh.nodes.size() > 1 && settings.mesh_instancing) { - NodeInfo& ni = nodes[mesh.node - data->nodes]; + mesh.instances.resize(mesh.nodes.size()); - // we transform all non-skinned non-animated meshes to world space - // this makes sure that quantization doesn't introduce gaps if the original scene was watertight - if (!ni.animated && !mesh.skin && mesh.targets == 0) - { - transformMesh(mesh, mesh.node); - mesh.node = 0; - } + for (size_t j = 0; j < mesh.nodes.size(); ++j) + cgltf_node_transform_world(mesh.nodes[j], mesh.instances[j].data); - // skinned and animated meshes will be anchored to the same node that they used to be in - // for animated meshes, this is important since they need to be transformed by the same animation - // for skinned meshes, in theory this isn't important since the transform of the skinned node doesn't matter; in practice this affects generated bounding box in three.js + mesh.nodes.clear(); } + else + { + mergeMeshInstances(mesh); + } + + assert(mesh.nodes.empty()); } mergeMeshMaterials(data, meshes, settings); @@ -231,6 +262,7 @@ static void process(cgltf_data* data, const char* input_path, const char* output bool ext_pbr_specular_glossiness = false; bool ext_clearcoat = false; bool ext_unlit = false; + bool ext_instancing = false; size_t accr_offset = 0; size_t node_offset = 0; @@ -301,7 +333,13 @@ static void process(cgltf_data* data, const char* input_path, const char* output { const Mesh& prim = meshes[pi]; - if (prim.node != mesh.node || prim.skin != mesh.skin || prim.targets != mesh.targets) + if (prim.skin != mesh.skin || prim.targets != mesh.targets) + break; + + if (pi > i && (mesh.instances.size() || prim.instances.size())) + break; + + if (!compareMeshNodes(mesh, prim)) break; if (!compareMeshTargets(mesh, prim)) @@ -377,22 +415,45 @@ static void process(cgltf_data* data, const char* input_path, const char* output append(json_meshes, "}"); - writeMeshNode(json_nodes, mesh_offset, mesh, data, settings.quantize ? &qp : NULL); + assert(mesh.nodes.empty() || mesh.instances.empty()); + ext_instancing = ext_instancing || !mesh.instances.empty(); + + if (mesh.nodes.size()) + { + for (size_t j = 0; j < mesh.nodes.size(); ++j) + { + NodeInfo& ni = nodes[mesh.nodes[j] - data->nodes]; + + assert(ni.keep); + ni.meshes.push_back(node_offset); + + writeMeshNode(json_nodes, mesh_offset, mesh.nodes[j], mesh.skin, data, settings.quantize ? &qp : NULL); - if (mesh.node) + node_offset++; + } + } + else if (mesh.instances.size()) { - NodeInfo& ni = nodes[mesh.node - data->nodes]; + comma(json_roots); + append(json_roots, node_offset); + + size_t instance_accr = writeInstances(views, json_accessors, accr_offset, mesh.instances, qp, settings); - assert(ni.keep); - ni.meshes.push_back(node_offset); + assert(!mesh.skin); + writeMeshNodeInstanced(json_nodes, mesh_offset, instance_accr); + + node_offset++; } else { comma(json_roots); append(json_roots, node_offset); + + writeMeshNode(json_nodes, mesh_offset, NULL, mesh.skin, data, settings.quantize ? &qp : NULL); + + node_offset++; } - node_offset++; mesh_offset++; // skip all meshes that we've written in this iteration @@ -466,6 +527,7 @@ static void process(cgltf_data* data, const char* input_path, const char* output {"KHR_materials_unlit", ext_unlit, false}, {"KHR_lights_punctual", data->lights_count > 0, false}, {"KHR_texture_basisu", !json_textures.empty() && settings.texture_ktx2, true}, + {"EXT_mesh_gpu_instancing", ext_instancing, true}, }; writeExtensions(json, extensions, sizeof(extensions) / sizeof(extensions[0])); @@ -515,6 +577,7 @@ static void process(cgltf_data* data, const char* input_path, const char* output printAttributeStats(views, BufferView::Kind_Vertex, "vertex"); printAttributeStats(views, BufferView::Kind_Index, "index"); printAttributeStats(views, BufferView::Kind_Keyframe, "keyframe"); + printAttributeStats(views, BufferView::Kind_Instance, "instance"); } } @@ -784,6 +847,14 @@ int main(int argc, char** argv) { settings.keep_extras = true; } + else if (strcmp(arg, "-mm") == 0) + { + settings.mesh_merge = true; + } + else if (strcmp(arg, "-mi") == 0) + { + settings.mesh_instancing = true; + } else if (strcmp(arg, "-si") == 0 && i + 1 < argc && isdigit(argv[i + 1][0])) { settings.simplify_threshold = float(atof(argv[++i])); @@ -925,6 +996,8 @@ int main(int argc, char** argv) fprintf(stderr, "\nScene:\n"); fprintf(stderr, "\t-kn: keep named nodes and meshes attached to named nodes so that named nodes can be transformed externally\n"); fprintf(stderr, "\t-ke: keep extras data\n"); + fprintf(stderr, "\t-mm: merge instances of the same mesh together when possible\n"); + fprintf(stderr, "\t-mi: use EXT_mesh_gpu_instancing when serializing multiple mesh instances\n"); fprintf(stderr, "\nMiscellaneous:\n"); fprintf(stderr, "\t-cf: produce compressed gltf/glb files with fallback for loaders that don't support compression\n"); fprintf(stderr, "\t-noq: disable quantization; produces much larger glTF files with no extensions\n"); diff --git a/gltf/gltfpack.h b/gltf/gltfpack.h index 04577adee..564525a2b 100644 --- a/gltf/gltfpack.h +++ b/gltf/gltfpack.h @@ -32,9 +32,15 @@ struct Stream std::vector data; }; +struct Transform +{ + float data[16]; +}; + struct Mesh { - cgltf_node* node; + std::vector nodes; + std::vector instances; cgltf_material* material; cgltf_skin* skin; @@ -90,6 +96,9 @@ struct Settings bool keep_named; bool keep_extras; + bool mesh_merge; + bool mesh_instancing; + float simplify_threshold; bool simplify_aggressive; @@ -181,6 +190,7 @@ struct BufferView Kind_Skin, Kind_Time, Kind_Keyframe, + Kind_Instance, Kind_Image, Kind_Count }; @@ -224,8 +234,9 @@ cgltf_data* parseGltf(const char* path, std::vector& meshes, std::vector& meshes, const Settings& settings); void filterEmptyMeshes(std::vector& meshes); @@ -242,6 +253,7 @@ std::string basisToKtx(const std::string& data, bool srgb, bool uastc); void markAnimated(cgltf_data* data, std::vector& nodes, const std::vector& animations); void markNeededNodes(cgltf_data* data, std::vector& nodes, const std::vector& meshes, const std::vector& animations, const Settings& settings); void remapNodes(cgltf_data* data, std::vector& nodes, size_t& node_offset); +void decomposeTransform(float translation[3], float rotation[4], float scale[3], const float* transform); QuantizationPosition prepareQuantizationPosition(const std::vector& meshes, const Settings& settings); void prepareQuantizationTexture(cgltf_data* data, std::vector& result, const std::vector& meshes, const Settings& settings); @@ -275,7 +287,9 @@ void writeTexture(std::string& json, const cgltf_texture& texture, cgltf_data* d void writeMeshAttributes(std::string& json, std::vector& views, std::string& json_accessors, size_t& accr_offset, const Mesh& mesh, int target, const QuantizationPosition& qp, const QuantizationTexture& qt, const Settings& settings); size_t writeMeshIndices(std::vector& views, std::string& json_accessors, size_t& accr_offset, const Mesh& mesh, const Settings& settings); size_t writeJointBindMatrices(std::vector& views, std::string& json_accessors, size_t& accr_offset, const cgltf_skin& skin, const QuantizationPosition& qp, const Settings& settings); -void writeMeshNode(std::string& json, size_t mesh_offset, const Mesh& mesh, cgltf_data* data, const QuantizationPosition* qp); +size_t writeInstances(std::vector& views, std::string& json_accessors, size_t& accr_offset, const std::vector& transforms, const QuantizationPosition& qp, const Settings& settings); +void writeMeshNode(std::string& json, size_t mesh_offset, cgltf_node* node, cgltf_skin* skin, cgltf_data* data, const QuantizationPosition* qp); +void writeMeshNodeInstanced(std::string& json, size_t mesh_offset, size_t accr_offset); void writeSkin(std::string& json, const cgltf_skin& skin, size_t matrix_accr, const std::vector& nodes, cgltf_data* data); void writeNode(std::string& json, const cgltf_node& node, const std::vector& nodes, cgltf_data* data); void writeAnimation(std::string& json, std::vector& views, std::string& json_accessors, size_t& accr_offset, const Animation& animation, size_t i, cgltf_data* data, const std::vector& nodes, const Settings& settings); diff --git a/gltf/mesh.cpp b/gltf/mesh.cpp index 25e6e0c65..20a8e5149 100644 --- a/gltf/mesh.cpp +++ b/gltf/mesh.cpp @@ -39,18 +39,18 @@ static float inverseTranspose(float* result, const float* transform) return det; } -static void transformPosition(float* ptr, const float* transform) +static void transformPosition(float* res, const float* ptr, const float* transform) { float x = ptr[0] * transform[0] + ptr[1] * transform[4] + ptr[2] * transform[8] + transform[12]; float y = ptr[0] * transform[1] + ptr[1] * transform[5] + ptr[2] * transform[9] + transform[13]; float z = ptr[0] * transform[2] + ptr[1] * transform[6] + ptr[2] * transform[10] + transform[14]; - ptr[0] = x; - ptr[1] = y; - ptr[2] = z; + res[0] = x; + res[1] = y; + res[2] = z; } -static void transformNormal(float* ptr, const float* transform) +static void transformNormal(float* res, const float* ptr, const float* transform) { float x = ptr[0] * transform[0] + ptr[1] * transform[4] + ptr[2] * transform[8]; float y = ptr[0] * transform[1] + ptr[1] * transform[5] + ptr[2] * transform[9]; @@ -59,13 +59,17 @@ static void transformNormal(float* ptr, const float* transform) float l = sqrtf(x * x + y * y + z * z); float s = (l == 0.f) ? 0.f : 1 / l; - ptr[0] = x * s; - ptr[1] = y * s; - ptr[2] = z * s; + res[0] = x * s; + res[1] = y * s; + res[2] = z * s; } -void transformMesh(Mesh& mesh, const cgltf_node* node) +// assumes mesh & target are structurally identical +static void transformMesh(Mesh& target, const Mesh& mesh, const cgltf_node* node) { + assert(target.streams.size() == mesh.streams.size()); + assert(target.indices.size() == mesh.indices.size()); + float transform[16]; cgltf_node_transform_world(node, transform); @@ -74,30 +78,34 @@ void transformMesh(Mesh& mesh, const cgltf_node* node) for (size_t si = 0; si < mesh.streams.size(); ++si) { - Stream& stream = mesh.streams[si]; + const Stream& source = mesh.streams[si]; + Stream& stream = target.streams[si]; + + assert(source.type == stream.type); + assert(source.data.size() == stream.data.size()); if (stream.type == cgltf_attribute_type_position) { for (size_t i = 0; i < stream.data.size(); ++i) - transformPosition(stream.data[i].f, transform); + transformPosition(stream.data[i].f, source.data[i].f, transform); } else if (stream.type == cgltf_attribute_type_normal) { for (size_t i = 0; i < stream.data.size(); ++i) - transformNormal(stream.data[i].f, transforminvt); + transformNormal(stream.data[i].f, source.data[i].f, transforminvt); } else if (stream.type == cgltf_attribute_type_tangent) { for (size_t i = 0; i < stream.data.size(); ++i) - transformNormal(stream.data[i].f, transform); + transformNormal(stream.data[i].f, source.data[i].f, transform); } } if (det < 0 && mesh.type == cgltf_primitive_type_triangles) { // negative scale means we need to flip face winding - for (size_t i = 0; i < mesh.indices.size(); i += 3) - std::swap(mesh.indices[i + 0], mesh.indices[i + 1]); + for (size_t i = 0; i < target.indices.size(); i += 3) + std::swap(target.indices[i + 0], target.indices[i + 1]); } } @@ -123,35 +131,58 @@ bool compareMeshTargets(const Mesh& lhs, const Mesh& rhs) return true; } -static bool canMergeMeshes(const Mesh& lhs, const Mesh& rhs, const Settings& settings) +bool compareMeshNodes(const Mesh& lhs, const Mesh& rhs) { - if (lhs.node != rhs.node) - { - if (!lhs.node || !rhs.node) - return false; + if (lhs.nodes.size() != rhs.nodes.size()) + return false; - if (lhs.node->parent != rhs.node->parent) + for (size_t i = 0; i < lhs.nodes.size(); ++i) + if (lhs.nodes[i] != rhs.nodes[i]) return false; - bool lhs_transform = lhs.node->has_translation | lhs.node->has_rotation | lhs.node->has_scale | lhs.node->has_matrix | (!!lhs.node->weights); - bool rhs_transform = rhs.node->has_translation | rhs.node->has_rotation | rhs.node->has_scale | rhs.node->has_matrix | (!!rhs.node->weights); + return true; +} - if (lhs_transform || rhs_transform) - return false; +static bool canMergeMeshNodes(cgltf_node* lhs, cgltf_node* rhs, const Settings& settings) +{ + if (lhs == rhs) + return true; - if (settings.keep_named) - { - if (lhs.node->name && *lhs.node->name) - return false; + if (lhs->parent != rhs->parent) + return false; - if (rhs.node->name && *rhs.node->name) - return false; - } + bool lhs_transform = lhs->has_translation | lhs->has_rotation | lhs->has_scale | lhs->has_matrix | (!!lhs->weights); + bool rhs_transform = rhs->has_translation | rhs->has_rotation | rhs->has_scale | rhs->has_matrix | (!!rhs->weights); + + if (lhs_transform || rhs_transform) + return false; + + if (settings.keep_named) + { + if (lhs->name && *lhs->name) + return false; - // we can merge nodes that don't have transforms of their own and have the same parent - // this is helpful when instead of splitting mesh into primitives, DCCs split mesh into mesh nodes + if (rhs->name && *rhs->name) + return false; } + // we can merge nodes that don't have transforms of their own and have the same parent + // this is helpful when instead of splitting mesh into primitives, DCCs split mesh into mesh nodes + return true; +} + +static bool canMergeMeshes(const Mesh& lhs, const Mesh& rhs, const Settings& settings) +{ + if (lhs.nodes.size() != rhs.nodes.size()) + return false; + + for (size_t i = 0; i < lhs.nodes.size(); ++i) + if (!canMergeMeshNodes(lhs.nodes[i], rhs.nodes[i], settings)) + return false; + + if (lhs.instances.size() || rhs.instances.size()) + return false; + if (lhs.material != rhs.material) return false; @@ -195,6 +226,40 @@ static void mergeMeshes(Mesh& target, const Mesh& mesh) target.indices[index_offset + i] = unsigned(vertex_offset + mesh.indices[i]); } +void mergeMeshInstances(Mesh& mesh) +{ + if (mesh.nodes.empty()) + return; + + // fast-path: for single instance meshes we transform in-place + if (mesh.nodes.size() == 1) + { + transformMesh(mesh, mesh, mesh.nodes[0]); + mesh.nodes.clear(); + return; + } + + Mesh base = mesh; + Mesh transformed = base; + + for (size_t i = 0; i < mesh.streams.size(); ++i) + { + mesh.streams[i].data.clear(); + mesh.streams[i].data.reserve(base.streams[i].data.size() * mesh.nodes.size()); + } + + mesh.indices.clear(); + mesh.indices.reserve(base.indices.size() * mesh.nodes.size()); + + for (size_t i = 0; i < mesh.nodes.size(); ++i) + { + transformMesh(transformed, base, mesh.nodes[i]); + mergeMeshes(mesh, transformed); + } + + mesh.nodes.clear(); +} + void mergeMeshes(std::vector& meshes, const Settings& settings) { for (size_t i = 0; i < meshes.size(); ++i) @@ -236,6 +301,8 @@ void mergeMeshes(std::vector& meshes, const Settings& settings) mesh.streams.clear(); mesh.indices.clear(); + mesh.nodes.clear(); + mesh.instances.clear(); } } diff --git a/gltf/node.cpp b/gltf/node.cpp index 043505b76..769cb6d3c 100644 --- a/gltf/node.cpp +++ b/gltf/node.cpp @@ -1,6 +1,9 @@ // This file is part of gltfpack; see gltfpack.h for version/license details #include "gltfpack.h" +#include +#include + void markAnimated(cgltf_data* data, std::vector& nodes, const std::vector& animations) { for (size_t i = 0; i < animations.size(); ++i) @@ -69,9 +72,9 @@ void markNeededNodes(cgltf_data* data, std::vector& nodes, const std:: { const Mesh& mesh = meshes[i]; - if (mesh.node) + for (size_t j = 0; j < mesh.nodes.size(); ++j) { - NodeInfo& ni = nodes[mesh.node - data->nodes]; + NodeInfo& ni = nodes[mesh.nodes[j] - data->nodes]; ni.keep = true; } @@ -128,3 +131,76 @@ void remapNodes(cgltf_data* data, std::vector& nodes, size_t& node_off } } } + +void decomposeTransform(float translation[3], float rotation[4], float scale[3], const float* transform) +{ + float m[4][4] = {}; + memcpy(m, transform, 16 * sizeof(float)); + + translation[0] = m[3][0]; + translation[1] = m[3][1]; + translation[2] = m[3][2]; + + float det = + m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2]) - + m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]); + + float sign = (det < 0.f) ? -1.f : 1.f; + + scale[0] = sqrtf(m[0][0] * m[0][0] + m[1][0] * m[1][0] + m[2][0] * m[2][0]) * sign; + scale[1] = sqrtf(m[0][1] * m[0][1] + m[1][1] * m[1][1] + m[2][1] * m[2][1]) * sign; + scale[2] = sqrtf(m[0][2] * m[0][2] + m[1][2] * m[1][2] + m[2][2] * m[2][2]) * sign; + + float rsx = (scale[0] == 0.f) ? 0.f : 1.f / scale[0]; + float rsy = (scale[1] == 0.f) ? 0.f : 1.f / scale[1]; + float rsz = (scale[2] == 0.f) ? 0.f : 1.f / scale[2]; + + float r00 = m[0][0] * rsx, r10 = m[1][0] * rsx, r20 = m[2][0] * rsx; + float r01 = m[0][1] * rsy, r11 = m[1][1] * rsy, r21 = m[2][1] * rsy; + float r02 = m[0][2] * rsz, r12 = m[1][2] * rsz, r22 = m[2][2] * rsz; + + float qt = 1.f; + + if (r22 < 0) + { + if (r00 > r11) + { + rotation[0] = qt = 1.f + r00 - r11 - r22; + rotation[1] = r01 + r10; + rotation[2] = r20 + r02; + rotation[3] = r12 - r21; + } + else + { + rotation[0] = r01 + r10; + rotation[1] = qt = 1.f - r00 + r11 - r22; + rotation[2] = r12 + r21; + rotation[3] = r20 - r02; + } + } + else + { + if (r00 < -r11) + { + rotation[0] = r20 + r02; + rotation[1] = r12 + r21; + rotation[2] = qt = 1.f - r00 - r11 + r22; + rotation[3] = r01 - r10; + } + else + { + rotation[0] = r12 - r21; + rotation[1] = r20 - r02; + rotation[2] = r01 - r10; + rotation[3] = qt = 1.f + r00 + r11 + r22; + } + } + + float qs = 0.5f / sqrtf(qt); + + rotation[0] *= qs; + rotation[1] *= qs; + rotation[2] *= qs; + rotation[3] *= qs; +} diff --git a/gltf/parsegltf.cpp b/gltf/parsegltf.cpp index 66c407ae3..d4e0ae7da 100644 --- a/gltf/parsegltf.cpp +++ b/gltf/parsegltf.cpp @@ -127,15 +127,9 @@ static void fixupIndices(std::vector& indices, cgltf_primitive_typ static void parseMeshesGltf(cgltf_data* data, std::vector& meshes) { - for (size_t ni = 0; ni < data->nodes_count; ++ni) + for (size_t mi = 0; mi < data->meshes_count; ++mi) { - cgltf_node& node = data->nodes[ni]; - - if (!node.mesh) - continue; - - const cgltf_mesh& mesh = *node.mesh; - int mesh_id = int(&mesh - data->meshes); + const cgltf_mesh& mesh = data->meshes[mi]; for (size_t pi = 0; pi < mesh.primitives_count; ++pi) { @@ -143,16 +137,13 @@ static void parseMeshesGltf(cgltf_data* data, std::vector& meshes) if (primitive.type == cgltf_primitive_type_points && primitive.indices) { - fprintf(stderr, "Warning: ignoring primitive %d of mesh %d because indexed points are not supported\n", int(pi), mesh_id); + fprintf(stderr, "Warning: ignoring primitive %d of mesh %d because indexed points are not supported\n", int(pi), int(mi)); continue; } Mesh result = {}; - result.node = &node; - result.material = primitive.material; - result.skin = node.skin; result.type = primitive.type; @@ -180,7 +171,7 @@ static void parseMeshesGltf(cgltf_data* data, std::vector& meshes) if (attr.type == cgltf_attribute_type_invalid) { - fprintf(stderr, "Warning: ignoring unknown attribute %s in primitive %d of mesh %d\n", attr.name, int(pi), mesh_id); + fprintf(stderr, "Warning: ignoring unknown attribute %s in primitive %d of mesh %d\n", attr.name, int(pi), int(mi)); continue; } @@ -206,7 +197,7 @@ static void parseMeshesGltf(cgltf_data* data, std::vector& meshes) if (attr.type == cgltf_attribute_type_invalid) { - fprintf(stderr, "Warning: ignoring unknown attribute %s in morph target %d of primitive %d of mesh %d\n", attr.name, int(ti), int(pi), mesh_id); + fprintf(stderr, "Warning: ignoring unknown attribute %s in morph target %d of primitive %d of mesh %d\n", attr.name, int(ti), int(pi), int(mi)); continue; } @@ -226,6 +217,46 @@ static void parseMeshesGltf(cgltf_data* data, std::vector& meshes) } } +static void parseMeshNodesGltf(cgltf_data* data, std::vector& meshes) +{ + for (size_t i = 0; i < data->nodes_count; ++i) + { + cgltf_node& node = data->nodes[i]; + if (!node.mesh) + continue; + + Mesh& mesh = meshes[node.mesh - data->meshes]; + + if (mesh.nodes.empty() || mesh.skin == node.skin) + { + mesh.nodes.push_back(&node); + mesh.skin = node.skin; + } + else + { + // this should be extremely rare - if the same mesh is used with different skins, we need to duplicate it + // in this case we don't spend any effort on keeping the number of duplicates to the minimum, because this + // should really never happen. + meshes.push_back(mesh); + + meshes.back().nodes.push_back(&node); + meshes.back().skin = node.skin; + } + } + + for (size_t i = 0; i < meshes.size(); ++i) + { + Mesh& mesh = meshes[i]; + + // because the rest of gltfpack assumes that empty nodes array = world-space mesh, we need to filter unused meshes + if (mesh.nodes.empty()) + { + mesh.streams.clear(); + mesh.indices.clear(); + } + } +} + static void parseAnimationsGltf(cgltf_data* data, std::vector& animations) { for (size_t i = 0; i < data->animations_count; ++i) @@ -336,6 +367,7 @@ cgltf_data* parseGltf(const char* path, std::vector& meshes, std::vector& views, std::string& json_ return matrix_accr; } -void writeMeshNode(std::string& json, size_t mesh_offset, const Mesh& mesh, cgltf_data* data, const QuantizationPosition* qp) +static void writeInstanceData(std::vector& views, std::string& json_accessors, cgltf_animation_path_type type, const std::vector& data, const Settings& settings) +{ + BufferView::Compression compression = settings.compress ? BufferView::Compression_Attribute : BufferView::Compression_None; + + std::string scratch; + StreamFormat format = writeKeyframeStream(scratch, type, data, settings); + + size_t view = getBufferView(views, BufferView::Kind_Instance, format.filter, compression, format.stride, type); + size_t offset = views[view].data.size(); + views[view].data += scratch; + + comma(json_accessors); + writeAccessor(json_accessors, view, offset, format.type, format.component_type, format.normalized, data.size()); +} + +size_t writeInstances(std::vector& views, std::string& json_accessors, size_t& accr_offset, const std::vector& transforms, const QuantizationPosition& qp, const Settings& settings) +{ + std::vector position, rotation, scale; + position.resize(transforms.size()); + rotation.resize(transforms.size()); + scale.resize(transforms.size()); + + for (size_t i = 0; i < transforms.size(); ++i) + { + decomposeTransform(position[i].f, rotation[i].f, scale[i].f, transforms[i].data); + + if (settings.quantize) + { + const float* transform = transforms[i].data; + + float node_scale = qp.scale / float((1 << qp.bits) - 1); + + // pos_offset has to be applied first, thus it results in an offset rotated by the instance matrix + position[i].f[0] += qp.offset[0] * transform[0] + qp.offset[1] * transform[4] + qp.offset[2] * transform[8]; + position[i].f[1] += qp.offset[0] * transform[1] + qp.offset[1] * transform[5] + qp.offset[2] * transform[9]; + position[i].f[2] += qp.offset[0] * transform[2] + qp.offset[1] * transform[6] + qp.offset[2] * transform[10]; + + // node_scale will be applied before the rotation/scale from transform + scale[i].f[0] *= node_scale; + scale[i].f[1] *= node_scale; + scale[i].f[2] *= node_scale; + } + } + + writeInstanceData(views, json_accessors, cgltf_animation_path_type_translation, position, settings); + writeInstanceData(views, json_accessors, cgltf_animation_path_type_rotation, rotation, settings); + writeInstanceData(views, json_accessors, cgltf_animation_path_type_scale, scale, settings); + + size_t result = accr_offset; + accr_offset += 3; + return result; +} + +void writeMeshNode(std::string& json, size_t mesh_offset, cgltf_node* node, cgltf_skin* skin, cgltf_data* data, const QuantizationPosition* qp) { comma(json); append(json, "{\"mesh\":"); append(json, mesh_offset); - if (mesh.skin) + if (skin) { comma(json); append(json, "\"skin\":"); - append(json, size_t(mesh.skin - data->skins)); + append(json, size_t(skin - data->skins)); } if (qp) { @@ -840,19 +893,42 @@ void writeMeshNode(std::string& json, size_t mesh_offset, const Mesh& mesh, cglt append(json, node_scale); append(json, "]"); } - if (mesh.node && mesh.node->weights_count) + if (node && node->weights_count) { append(json, ",\"weights\":["); - for (size_t j = 0; j < mesh.node->weights_count; ++j) + for (size_t j = 0; j < node->weights_count; ++j) { comma(json); - append(json, mesh.node->weights[j]); + append(json, node->weights[j]); } append(json, "]"); } append(json, "}"); } +void writeMeshNodeInstanced(std::string& json, size_t mesh_offset, size_t accr_offset) +{ + comma(json); + append(json, "{\"mesh\":"); + append(json, mesh_offset); + append(json, ",\"extensions\":{\"EXT_mesh_gpu_instancing\":{\"attributes\":{"); + + comma(json); + append(json, "\"TRANSLATION\":"); + append(json, accr_offset + 0); + + comma(json); + append(json, "\"ROTATION\":"); + append(json, accr_offset + 1); + + comma(json); + append(json, "\"SCALE\":"); + append(json, accr_offset + 2); + + append(json, "}}}"); + append(json, "}"); +} + void writeSkin(std::string& json, const cgltf_skin& skin, size_t matrix_accr, const std::vector& nodes, cgltf_data* data) { comma(json);