1538 lines
44 KiB
C++
1538 lines
44 KiB
C++
// This file is part of gltfpack; see gltfpack.h for version/license details
|
|
#include "gltfpack.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include <locale.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#ifdef __wasi__
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include "../src/meshoptimizer.h"
|
|
|
|
std::string getVersion()
|
|
{
|
|
char result[32];
|
|
snprintf(result, sizeof(result), "%d.%d", MESHOPTIMIZER_VERSION / 1000, (MESHOPTIMIZER_VERSION % 1000) / 10);
|
|
return result;
|
|
}
|
|
|
|
static void finalizeBufferViews(std::string& json, std::vector<BufferView>& views, std::string& bin, std::string* fallback, size_t& fallback_size)
|
|
{
|
|
for (size_t i = 0; i < views.size(); ++i)
|
|
{
|
|
BufferView& view = views[i];
|
|
|
|
size_t bin_offset = bin.size();
|
|
size_t fallback_offset = fallback_size;
|
|
|
|
size_t count = view.data.size() / view.stride;
|
|
|
|
if (view.compression == BufferView::Compression_None)
|
|
{
|
|
bin += view.data;
|
|
}
|
|
else
|
|
{
|
|
switch (view.compression)
|
|
{
|
|
case BufferView::Compression_Attribute:
|
|
compressVertexStream(bin, view.data, count, view.stride);
|
|
break;
|
|
case BufferView::Compression_Index:
|
|
compressIndexStream(bin, view.data, count, view.stride);
|
|
break;
|
|
case BufferView::Compression_IndexSequence:
|
|
compressIndexSequence(bin, view.data, count, view.stride);
|
|
break;
|
|
default:
|
|
assert(!"Unknown compression type");
|
|
}
|
|
|
|
if (fallback)
|
|
*fallback += view.data;
|
|
fallback_size += view.data.size();
|
|
}
|
|
|
|
size_t raw_offset = (view.compression != BufferView::Compression_None) ? fallback_offset : bin_offset;
|
|
|
|
comma(json);
|
|
writeBufferView(json, view.kind, view.filter, count, view.stride, raw_offset, view.data.size(), view.compression, bin_offset, bin.size() - bin_offset);
|
|
|
|
// record written bytes for statistics
|
|
view.bytes = bin.size() - bin_offset;
|
|
|
|
// align each bufferView by 4 bytes
|
|
bin.resize((bin.size() + 3) & ~3);
|
|
if (fallback)
|
|
fallback->resize((fallback->size() + 3) & ~3);
|
|
fallback_size = (fallback_size + 3) & ~3;
|
|
}
|
|
}
|
|
|
|
static void printMeshStats(const std::vector<Mesh>& meshes, const char* name)
|
|
{
|
|
size_t mesh_triangles = 0;
|
|
size_t mesh_vertices = 0;
|
|
size_t total_triangles = 0;
|
|
size_t total_instances = 0;
|
|
size_t total_draws = 0;
|
|
|
|
for (size_t i = 0; i < meshes.size(); ++i)
|
|
{
|
|
const Mesh& mesh = meshes[i];
|
|
|
|
mesh_triangles += mesh.indices.size() / 3;
|
|
mesh_vertices += mesh.streams.empty() ? 0 : mesh.streams[0].data.size();
|
|
|
|
size_t instances = std::max(size_t(1), mesh.nodes.size() + mesh.instances.size());
|
|
|
|
total_triangles += mesh.indices.size() / 3 * instances;
|
|
total_instances += instances;
|
|
total_draws += std::max(size_t(1), mesh.nodes.size());
|
|
}
|
|
|
|
printf("%s: %d mesh primitives (%d triangles, %d vertices); %d draw calls (%d instances, %lld triangles)\n", name,
|
|
int(meshes.size()), int(mesh_triangles), int(mesh_vertices),
|
|
int(total_draws), int(total_instances), (long long)total_triangles);
|
|
}
|
|
|
|
static void printSceneStats(const std::vector<BufferView>& views, const std::vector<Mesh>& meshes, size_t node_offset, size_t mesh_offset, size_t material_offset, size_t json_size, size_t bin_size)
|
|
{
|
|
size_t bytes[BufferView::Kind_Count] = {};
|
|
|
|
for (size_t i = 0; i < views.size(); ++i)
|
|
{
|
|
const BufferView& view = views[i];
|
|
bytes[view.kind] += view.bytes;
|
|
}
|
|
|
|
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, 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_Instance]),
|
|
int(bytes[BufferView::Kind_Image]));
|
|
}
|
|
|
|
static void printAttributeStats(const std::vector<BufferView>& views, BufferView::Kind kind, const char* name)
|
|
{
|
|
for (size_t i = 0; i < views.size(); ++i)
|
|
{
|
|
const BufferView& view = views[i];
|
|
|
|
if (view.kind != kind)
|
|
continue;
|
|
|
|
const char* variant = "unknown";
|
|
|
|
switch (kind)
|
|
{
|
|
case BufferView::Kind_Vertex:
|
|
variant = attributeType(cgltf_attribute_type(view.variant));
|
|
break;
|
|
|
|
case BufferView::Kind_Index:
|
|
variant = "index";
|
|
break;
|
|
|
|
case BufferView::Kind_Keyframe:
|
|
case BufferView::Kind_Instance:
|
|
variant = animationPath(cgltf_animation_path_type(view.variant));
|
|
break;
|
|
|
|
default:;
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
static void printImageStats(const std::vector<BufferView>& views, TextureKind kind, const char* name)
|
|
{
|
|
size_t bytes = 0;
|
|
size_t count = 0;
|
|
|
|
for (size_t i = 0; i < views.size(); ++i)
|
|
{
|
|
const BufferView& view = views[i];
|
|
|
|
if (view.kind != BufferView::Kind_Image)
|
|
continue;
|
|
|
|
if (view.variant != -1 - kind)
|
|
continue;
|
|
|
|
count += 1;
|
|
bytes += view.data.size();
|
|
}
|
|
|
|
if (count)
|
|
printf("stats: image %s: %d bytes in %d images\n", name, int(bytes), int(count));
|
|
}
|
|
|
|
static bool printReport(const char* path, cgltf_data* data, const std::vector<BufferView>& views, const std::vector<Mesh>& meshes, size_t node_count, size_t mesh_count, size_t material_count, size_t animation_count, size_t json_size, size_t bin_size)
|
|
{
|
|
size_t bytes[BufferView::Kind_Count] = {};
|
|
|
|
for (size_t i = 0; i < views.size(); ++i)
|
|
{
|
|
const BufferView& view = views[i];
|
|
bytes[view.kind] += view.bytes;
|
|
}
|
|
|
|
size_t total_triangles = 0;
|
|
size_t total_instances = 0;
|
|
size_t total_draws = 0;
|
|
|
|
for (size_t i = 0; i < meshes.size(); ++i)
|
|
{
|
|
const Mesh& mesh = meshes[i];
|
|
|
|
size_t instances = std::max(size_t(1), mesh.nodes.size() + mesh.instances.size());
|
|
|
|
total_triangles += mesh.indices.size() / 3 * instances;
|
|
total_instances += instances;
|
|
total_draws += std::max(size_t(1), mesh.nodes.size());
|
|
}
|
|
|
|
FILE* out = fopen(path, "wb");
|
|
if (!out)
|
|
return false;
|
|
|
|
fprintf(out, "{\n");
|
|
fprintf(out, "\t\"generator\": \"gltfpack %s\",\n", getVersion().c_str());
|
|
fprintf(out, "\t\"scene\": {\n");
|
|
fprintf(out, "\t\t\"nodeCount\": %d,\n", int(node_count));
|
|
fprintf(out, "\t\t\"meshCount\": %d,\n", int(mesh_count));
|
|
fprintf(out, "\t\t\"materialCount\": %d,\n", int(material_count));
|
|
fprintf(out, "\t\t\"textureCount\": %d,\n", int(data->textures_count));
|
|
fprintf(out, "\t\t\"animationCount\": %d\n", int(animation_count));
|
|
fprintf(out, "\t},\n");
|
|
fprintf(out, "\t\"render\": {\n");
|
|
fprintf(out, "\t\t\"drawCount\": %d,\n", int(total_draws));
|
|
fprintf(out, "\t\t\"instanceCount\": %d,\n", int(total_instances));
|
|
fprintf(out, "\t\t\"triangleCount\": %lld\n", (long long)total_triangles);
|
|
fprintf(out, "\t},\n");
|
|
fprintf(out, "\t\"data\": {\n");
|
|
fprintf(out, "\t\t\"json\": %d,\n", int(json_size));
|
|
fprintf(out, "\t\t\"binary\": %d,\n", int(bin_size));
|
|
fprintf(out, "\t\t\"buffers\": {\n");
|
|
fprintf(out, "\t\t\t\"vertex\": %d,\n", int(bytes[BufferView::Kind_Vertex]));
|
|
fprintf(out, "\t\t\t\"index\": %d,\n", int(bytes[BufferView::Kind_Index]));
|
|
fprintf(out, "\t\t\t\"animation\": %d,\n", int(bytes[BufferView::Kind_Time] + bytes[BufferView::Kind_Keyframe]));
|
|
fprintf(out, "\t\t\t\"transform\": %d,\n", int(bytes[BufferView::Kind_Skin] + bytes[BufferView::Kind_Instance]));
|
|
fprintf(out, "\t\t\t\"image\": %d\n", int(bytes[BufferView::Kind_Image]));
|
|
fprintf(out, "\t\t}\n");
|
|
fprintf(out, "\t}\n");
|
|
fprintf(out, "}\n");
|
|
|
|
int rc = fclose(out);
|
|
return rc == 0;
|
|
}
|
|
|
|
static bool canTransformMesh(const Mesh& mesh)
|
|
{
|
|
// volume thickness is specified in mesh coordinate space; to avoid modifying materials we prohibit transforming meshes with volume materials
|
|
if (mesh.material && mesh.material->has_volume && mesh.material->volume.thickness_factor > 0.f)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void process(cgltf_data* data, const char* input_path, const char* output_path, const char* report_path, std::vector<Mesh>& meshes, std::vector<Animation>& animations, const Settings& settings, std::string& json, std::string& bin, std::string& fallback, size_t& fallback_size)
|
|
{
|
|
if (settings.verbose)
|
|
{
|
|
printf("input: %d nodes, %d meshes (%d primitives), %d materials, %d skins, %d animations\n",
|
|
int(data->nodes_count), int(data->meshes_count), int(meshes.size()), int(data->materials_count), int(data->skins_count), int(animations.size()));
|
|
printMeshStats(meshes, "input");
|
|
}
|
|
|
|
for (size_t i = 0; i < animations.size(); ++i)
|
|
{
|
|
processAnimation(animations[i], settings);
|
|
}
|
|
|
|
std::vector<NodeInfo> nodes(data->nodes_count);
|
|
|
|
markScenes(data, nodes);
|
|
markAnimated(data, nodes, animations);
|
|
|
|
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 (settings.keep_nodes)
|
|
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;
|
|
|
|
int scene = nodes[mesh.nodes[0] - data->nodes].scene;
|
|
bool any_other_scene = false;
|
|
for (size_t j = 0; j < mesh.nodes.size(); ++j)
|
|
any_other_scene |= scene != nodes[mesh.nodes[j] - data->nodes].scene;
|
|
|
|
// we only merge instances when all nodes have a single consistent scene
|
|
if (scene < 0 || any_other_scene)
|
|
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)
|
|
{
|
|
mesh.instances.resize(mesh.nodes.size());
|
|
|
|
for (size_t j = 0; j < mesh.nodes.size(); ++j)
|
|
cgltf_node_transform_world(mesh.nodes[j], mesh.instances[j].data);
|
|
|
|
mesh.nodes.clear();
|
|
mesh.scene = scene;
|
|
}
|
|
else if (canTransformMesh(mesh))
|
|
{
|
|
mergeMeshInstances(mesh);
|
|
|
|
assert(mesh.nodes.empty());
|
|
mesh.scene = scene;
|
|
}
|
|
}
|
|
|
|
// material information is required for mesh and image processing
|
|
std::vector<MaterialInfo> materials(data->materials_count);
|
|
std::vector<ImageInfo> images(data->images_count);
|
|
|
|
analyzeMaterials(data, materials, images);
|
|
|
|
optimizeMaterials(data, input_path, images);
|
|
|
|
// streams need to be filtered before mesh merging (or processing) to make sure we can merge meshes with redundant streams
|
|
for (size_t i = 0; i < meshes.size(); ++i)
|
|
{
|
|
Mesh& mesh = meshes[i];
|
|
MaterialInfo mi = mesh.material ? materials[mesh.material - data->materials] : MaterialInfo();
|
|
|
|
// merge material requirements across all variants
|
|
for (size_t j = 0; j < mesh.variants.size(); ++j)
|
|
{
|
|
MaterialInfo vi = materials[mesh.variants[j].material - data->materials];
|
|
|
|
mi.needsTangents |= vi.needsTangents;
|
|
mi.textureSetMask |= vi.textureSetMask;
|
|
}
|
|
|
|
filterStreams(mesh, mi);
|
|
}
|
|
|
|
mergeMeshMaterials(data, meshes, settings);
|
|
mergeMeshes(meshes, settings);
|
|
filterEmptyMeshes(meshes);
|
|
|
|
markNeededNodes(data, nodes, meshes, animations, settings);
|
|
markNeededMaterials(data, materials, meshes, settings);
|
|
|
|
#ifndef NDEBUG
|
|
std::vector<Mesh> debug_meshes;
|
|
|
|
for (size_t i = 0; i < meshes.size(); ++i)
|
|
{
|
|
const Mesh& mesh = meshes[i];
|
|
|
|
if (settings.simplify_debug > 0)
|
|
{
|
|
Mesh kinds = {};
|
|
Mesh loops = {};
|
|
debugSimplify(mesh, kinds, loops, settings.simplify_debug);
|
|
debug_meshes.push_back(kinds);
|
|
debug_meshes.push_back(loops);
|
|
}
|
|
|
|
if (settings.meshlet_debug > 0)
|
|
{
|
|
Mesh meshlets = {};
|
|
Mesh bounds = {};
|
|
debugMeshlets(mesh, meshlets, bounds, settings.meshlet_debug, /* scan= */ false);
|
|
debug_meshes.push_back(meshlets);
|
|
debug_meshes.push_back(bounds);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
for (size_t i = 0; i < meshes.size(); ++i)
|
|
{
|
|
processMesh(meshes[i], settings);
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
meshes.insert(meshes.end(), debug_meshes.begin(), debug_meshes.end());
|
|
#endif
|
|
|
|
filterEmptyMeshes(meshes); // some meshes may become empty after processing
|
|
|
|
QuantizationPosition qp = prepareQuantizationPosition(meshes, settings);
|
|
|
|
std::vector<QuantizationTexture> qt_materials(materials.size());
|
|
std::vector<size_t> qt_meshes(meshes.size(), size_t(-1));
|
|
prepareQuantizationTexture(data, qt_materials, qt_meshes, meshes, settings);
|
|
|
|
QuantizationTexture qt_dummy = {};
|
|
qt_dummy.bits = settings.tex_bits;
|
|
|
|
std::string json_images;
|
|
std::string json_samplers;
|
|
std::string json_textures;
|
|
std::string json_materials;
|
|
std::string json_accessors;
|
|
std::string json_meshes;
|
|
std::string json_nodes;
|
|
std::string json_skins;
|
|
std::vector<std::string> json_roots(data->scenes_count);
|
|
std::string json_animations;
|
|
std::string json_cameras;
|
|
std::string json_extensions;
|
|
|
|
std::vector<BufferView> views;
|
|
|
|
bool ext_pbr_specular_glossiness = false;
|
|
bool ext_clearcoat = false;
|
|
bool ext_transmission = false;
|
|
bool ext_ior = false;
|
|
bool ext_specular = false;
|
|
bool ext_sheen = false;
|
|
bool ext_volume = false;
|
|
bool ext_emissive_strength = false;
|
|
bool ext_iridescence = false;
|
|
bool ext_unlit = false;
|
|
bool ext_instancing = false;
|
|
bool ext_texture_transform = false;
|
|
|
|
size_t accr_offset = 0;
|
|
size_t node_offset = 0;
|
|
size_t mesh_offset = 0;
|
|
size_t material_offset = 0;
|
|
|
|
for (size_t i = 0; i < data->samplers_count; ++i)
|
|
{
|
|
const cgltf_sampler& sampler = data->samplers[i];
|
|
|
|
comma(json_samplers);
|
|
append(json_samplers, "{");
|
|
writeSampler(json_samplers, sampler);
|
|
append(json_samplers, "}");
|
|
}
|
|
|
|
std::vector<std::string> encoded_images;
|
|
|
|
#ifdef WITH_BASISU
|
|
if (data->images_count && settings.texture_ktx2)
|
|
{
|
|
encoded_images.resize(data->images_count);
|
|
|
|
encodeImages(encoded_images.data(), data, images, input_path, settings);
|
|
}
|
|
#endif
|
|
|
|
for (size_t i = 0; i < data->images_count; ++i)
|
|
{
|
|
const cgltf_image& image = data->images[i];
|
|
|
|
comma(json_images);
|
|
append(json_images, "{");
|
|
if (encoded_images.size() && !encoded_images[i].empty())
|
|
{
|
|
if (encoded_images[i].compare(0, 5, "error") == 0)
|
|
fprintf(stderr, "Warning: unable to encode image %d (%s), skipping (%s)\n", int(i), image.uri ? image.uri : "?", encoded_images[i].c_str());
|
|
else
|
|
writeEncodedImage(json_images, views, image, encoded_images[i], images[i], output_path, settings);
|
|
|
|
encoded_images[i] = std::string(); // reclaim memory early
|
|
}
|
|
else
|
|
{
|
|
writeImage(json_images, views, image, images[i], i, input_path, settings);
|
|
}
|
|
append(json_images, "}");
|
|
}
|
|
|
|
for (size_t i = 0; i < data->textures_count; ++i)
|
|
{
|
|
const cgltf_texture& texture = data->textures[i];
|
|
|
|
comma(json_textures);
|
|
append(json_textures, "{");
|
|
writeTexture(json_textures, texture, texture.image ? &images[texture.image - data->images] : NULL, data, settings);
|
|
append(json_textures, "}");
|
|
}
|
|
|
|
for (size_t i = 0; i < data->materials_count; ++i)
|
|
{
|
|
MaterialInfo& mi = materials[i];
|
|
|
|
if (!mi.keep)
|
|
continue;
|
|
|
|
const cgltf_material& material = data->materials[i];
|
|
|
|
comma(json_materials);
|
|
append(json_materials, "{");
|
|
writeMaterial(json_materials, data, material, settings.quantize && !settings.pos_float ? &qp : NULL, settings.quantize ? &qt_materials[i] : NULL);
|
|
if (settings.keep_extras)
|
|
writeExtras(json_materials, material.extras);
|
|
append(json_materials, "}");
|
|
|
|
mi.remap = int(material_offset);
|
|
material_offset++;
|
|
|
|
ext_pbr_specular_glossiness = ext_pbr_specular_glossiness || material.has_pbr_specular_glossiness;
|
|
ext_clearcoat = ext_clearcoat || material.has_clearcoat;
|
|
ext_transmission = ext_transmission || material.has_transmission;
|
|
ext_ior = ext_ior || material.has_ior;
|
|
ext_specular = ext_specular || material.has_specular;
|
|
ext_sheen = ext_sheen || material.has_sheen;
|
|
ext_volume = ext_volume || material.has_volume;
|
|
ext_emissive_strength = ext_emissive_strength || material.has_emissive_strength;
|
|
ext_iridescence = ext_iridescence || material.has_iridescence;
|
|
ext_unlit = ext_unlit || material.unlit;
|
|
ext_texture_transform = ext_texture_transform || mi.usesTextureTransform;
|
|
}
|
|
|
|
for (size_t i = 0; i < meshes.size(); ++i)
|
|
{
|
|
const Mesh& mesh = meshes[i];
|
|
|
|
comma(json_meshes);
|
|
append(json_meshes, "{\"primitives\":[");
|
|
|
|
size_t pi = i;
|
|
for (; pi < meshes.size(); ++pi)
|
|
{
|
|
const Mesh& prim = meshes[pi];
|
|
|
|
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))
|
|
break;
|
|
|
|
const QuantizationTexture& qt = qt_meshes[pi] == size_t(-1) ? qt_dummy : qt_materials[qt_meshes[pi]];
|
|
|
|
comma(json_meshes);
|
|
append(json_meshes, "{\"attributes\":{");
|
|
writeMeshAttributes(json_meshes, views, json_accessors, accr_offset, prim, 0, qp, qt, settings);
|
|
append(json_meshes, "}");
|
|
if (prim.type != cgltf_primitive_type_triangles)
|
|
{
|
|
append(json_meshes, ",\"mode\":");
|
|
append(json_meshes, size_t(prim.type));
|
|
}
|
|
if (mesh.targets)
|
|
{
|
|
append(json_meshes, ",\"targets\":[");
|
|
for (size_t j = 0; j < mesh.targets; ++j)
|
|
{
|
|
comma(json_meshes);
|
|
append(json_meshes, "{");
|
|
writeMeshAttributes(json_meshes, views, json_accessors, accr_offset, prim, int(1 + j), qp, qt, settings);
|
|
append(json_meshes, "}");
|
|
}
|
|
append(json_meshes, "]");
|
|
}
|
|
|
|
if (!prim.indices.empty())
|
|
{
|
|
size_t index_accr = writeMeshIndices(views, json_accessors, accr_offset, prim, settings);
|
|
|
|
append(json_meshes, ",\"indices\":");
|
|
append(json_meshes, index_accr);
|
|
}
|
|
|
|
if (prim.material)
|
|
{
|
|
MaterialInfo& mi = materials[prim.material - data->materials];
|
|
|
|
assert(mi.keep);
|
|
append(json_meshes, ",\"material\":");
|
|
append(json_meshes, size_t(mi.remap));
|
|
}
|
|
|
|
if (prim.variants.size())
|
|
{
|
|
append(json_meshes, ",\"extensions\":{\"KHR_materials_variants\":{\"mappings\":[");
|
|
|
|
for (size_t j = 0; j < prim.variants.size(); ++j)
|
|
{
|
|
const cgltf_material_mapping& variant = prim.variants[j];
|
|
MaterialInfo& mi = materials[variant.material - data->materials];
|
|
|
|
assert(mi.keep);
|
|
comma(json_meshes);
|
|
append(json_meshes, "{\"material\":");
|
|
append(json_meshes, size_t(mi.remap));
|
|
append(json_meshes, ",\"variants\":[");
|
|
append(json_meshes, size_t(variant.variant));
|
|
append(json_meshes, "]}");
|
|
}
|
|
|
|
append(json_meshes, "]}}");
|
|
}
|
|
|
|
append(json_meshes, "}");
|
|
}
|
|
|
|
append(json_meshes, "]");
|
|
|
|
if (mesh.target_weights.size())
|
|
{
|
|
append(json_meshes, ",\"weights\":[");
|
|
for (size_t j = 0; j < mesh.target_weights.size(); ++j)
|
|
{
|
|
comma(json_meshes);
|
|
append(json_meshes, mesh.target_weights[j]);
|
|
}
|
|
append(json_meshes, "]");
|
|
}
|
|
|
|
if (mesh.target_names.size())
|
|
{
|
|
append(json_meshes, ",\"extras\":{\"targetNames\":[");
|
|
for (size_t j = 0; j < mesh.target_names.size(); ++j)
|
|
{
|
|
comma(json_meshes);
|
|
append(json_meshes, "\"");
|
|
append(json_meshes, mesh.target_names[j]);
|
|
append(json_meshes, "\"");
|
|
}
|
|
append(json_meshes, "]}");
|
|
}
|
|
|
|
append(json_meshes, "}");
|
|
|
|
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);
|
|
|
|
// if we don't use position quantization, prefer attaching the mesh to its node directly
|
|
if (!ni.has_mesh && (!settings.quantize || settings.pos_float))
|
|
{
|
|
ni.has_mesh = true;
|
|
ni.mesh_index = mesh_offset;
|
|
ni.mesh_skin = mesh.skin;
|
|
}
|
|
else
|
|
{
|
|
ni.mesh_nodes.push_back(node_offset);
|
|
|
|
writeMeshNode(json_nodes, mesh_offset, mesh.nodes[j], mesh.skin, data, settings.quantize && !settings.pos_float ? &qp : NULL);
|
|
|
|
node_offset++;
|
|
}
|
|
}
|
|
}
|
|
else if (mesh.instances.size())
|
|
{
|
|
assert(mesh.scene >= 0);
|
|
comma(json_roots[mesh.scene]);
|
|
append(json_roots[mesh.scene], node_offset);
|
|
|
|
size_t instance_accr = writeInstances(views, json_accessors, accr_offset, mesh.instances, qp, settings);
|
|
|
|
assert(!mesh.skin);
|
|
writeMeshNodeInstanced(json_nodes, mesh_offset, instance_accr);
|
|
|
|
node_offset++;
|
|
}
|
|
else
|
|
{
|
|
assert(mesh.scene >= 0);
|
|
comma(json_roots[mesh.scene]);
|
|
append(json_roots[mesh.scene], node_offset);
|
|
|
|
writeMeshNode(json_nodes, mesh_offset, NULL, mesh.skin, data, settings.quantize && !settings.pos_float ? &qp : NULL);
|
|
|
|
node_offset++;
|
|
}
|
|
|
|
mesh_offset++;
|
|
|
|
// skip all meshes that we've written in this iteration
|
|
assert(pi > i);
|
|
i = pi - 1;
|
|
}
|
|
|
|
remapNodes(data, nodes, node_offset);
|
|
|
|
for (size_t i = 0; i < data->nodes_count; ++i)
|
|
{
|
|
NodeInfo& ni = nodes[i];
|
|
|
|
if (!ni.keep)
|
|
continue;
|
|
|
|
const cgltf_node& node = data->nodes[i];
|
|
|
|
comma(json_nodes);
|
|
append(json_nodes, "{");
|
|
writeNode(json_nodes, node, nodes, data);
|
|
if (settings.keep_extras)
|
|
writeExtras(json_nodes, node.extras);
|
|
append(json_nodes, "}");
|
|
}
|
|
|
|
for (size_t i = 0; i < data->scenes_count; ++i)
|
|
{
|
|
for (size_t j = 0; j < data->scenes[i].nodes_count; ++j)
|
|
{
|
|
NodeInfo& ni = nodes[data->scenes[i].nodes[j] - data->nodes];
|
|
|
|
if (ni.keep)
|
|
{
|
|
comma(json_roots[i]);
|
|
append(json_roots[i], size_t(ni.remap));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (size_t i = 0; i < data->skins_count; ++i)
|
|
{
|
|
const cgltf_skin& skin = data->skins[i];
|
|
|
|
size_t matrix_accr = writeJointBindMatrices(views, json_accessors, accr_offset, skin, qp, settings);
|
|
|
|
writeSkin(json_skins, skin, matrix_accr, nodes, data);
|
|
}
|
|
|
|
for (size_t i = 0; i < animations.size(); ++i)
|
|
{
|
|
const Animation& animation = animations[i];
|
|
|
|
writeAnimation(json_animations, views, json_accessors, accr_offset, animation, i, data, nodes, settings);
|
|
}
|
|
|
|
for (size_t i = 0; i < data->cameras_count; ++i)
|
|
{
|
|
const cgltf_camera& camera = data->cameras[i];
|
|
|
|
writeCamera(json_cameras, camera);
|
|
}
|
|
|
|
if (data->lights_count > 0)
|
|
{
|
|
comma(json_extensions);
|
|
append(json_extensions, "\"KHR_lights_punctual\":{\"lights\":[");
|
|
|
|
for (size_t i = 0; i < data->lights_count; ++i)
|
|
{
|
|
const cgltf_light& light = data->lights[i];
|
|
|
|
writeLight(json_extensions, light);
|
|
}
|
|
|
|
append(json_extensions, "]}");
|
|
}
|
|
|
|
if (data->variants_count > 0)
|
|
{
|
|
comma(json_extensions);
|
|
append(json_extensions, "\"KHR_materials_variants\":{\"variants\":[");
|
|
|
|
for (size_t i = 0; i < data->variants_count; ++i)
|
|
{
|
|
const cgltf_material_variant& variant = data->variants[i];
|
|
|
|
comma(json_extensions);
|
|
append(json_extensions, "{\"name\":\"");
|
|
append(json_extensions, variant.name);
|
|
append(json_extensions, "\"}");
|
|
}
|
|
|
|
append(json_extensions, "]}");
|
|
}
|
|
|
|
append(json, "\"asset\":{");
|
|
append(json, "\"version\":\"2.0\",\"generator\":\"gltfpack ");
|
|
append(json, getVersion());
|
|
append(json, "\"");
|
|
writeExtras(json, data->asset.extras);
|
|
append(json, "}");
|
|
|
|
const ExtensionInfo extensions[] = {
|
|
{"KHR_mesh_quantization", settings.quantize, true},
|
|
{"EXT_meshopt_compression", settings.compress, !settings.fallback},
|
|
{"KHR_texture_transform", (settings.quantize && !json_textures.empty()) || ext_texture_transform, false},
|
|
{"KHR_materials_pbrSpecularGlossiness", ext_pbr_specular_glossiness, false},
|
|
{"KHR_materials_clearcoat", ext_clearcoat, false},
|
|
{"KHR_materials_transmission", ext_transmission, false},
|
|
{"KHR_materials_ior", ext_ior, false},
|
|
{"KHR_materials_specular", ext_specular, false},
|
|
{"KHR_materials_sheen", ext_sheen, false},
|
|
{"KHR_materials_volume", ext_volume, false},
|
|
{"KHR_materials_emissive_strength", ext_emissive_strength, false},
|
|
{"KHR_materials_iridescence", ext_iridescence, false},
|
|
{"KHR_materials_unlit", ext_unlit, false},
|
|
{"KHR_materials_variants", data->variants_count > 0, 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]));
|
|
|
|
std::string json_views;
|
|
finalizeBufferViews(json_views, views, bin, settings.fallback ? &fallback : NULL, fallback_size);
|
|
|
|
writeArray(json, "bufferViews", json_views);
|
|
writeArray(json, "accessors", json_accessors);
|
|
writeArray(json, "samplers", json_samplers);
|
|
writeArray(json, "images", json_images);
|
|
writeArray(json, "textures", json_textures);
|
|
writeArray(json, "materials", json_materials);
|
|
writeArray(json, "meshes", json_meshes);
|
|
writeArray(json, "skins", json_skins);
|
|
writeArray(json, "animations", json_animations);
|
|
writeArray(json, "nodes", json_nodes);
|
|
|
|
if (!json_roots.empty())
|
|
{
|
|
append(json, ",\"scenes\":[");
|
|
|
|
for (size_t i = 0; i < data->scenes_count; ++i)
|
|
writeScene(json, data->scenes[i], json_roots[i]);
|
|
|
|
append(json, "]");
|
|
}
|
|
|
|
writeArray(json, "cameras", json_cameras);
|
|
|
|
if (data->scene)
|
|
{
|
|
append(json, ",\"scene\":");
|
|
append(json, size_t(data->scene - data->scenes));
|
|
}
|
|
|
|
if (!json_extensions.empty())
|
|
{
|
|
append(json, ",\"extensions\":{");
|
|
append(json, json_extensions);
|
|
append(json, "}");
|
|
}
|
|
|
|
if (settings.verbose)
|
|
{
|
|
printMeshStats(meshes, "output");
|
|
printSceneStats(views, meshes, node_offset, mesh_offset, material_offset, json.size(), bin.size());
|
|
}
|
|
|
|
if (settings.verbose > 1)
|
|
{
|
|
printAttributeStats(views, BufferView::Kind_Vertex, "vertex");
|
|
printAttributeStats(views, BufferView::Kind_Index, "index");
|
|
printAttributeStats(views, BufferView::Kind_Keyframe, "keyframe");
|
|
printAttributeStats(views, BufferView::Kind_Instance, "instance");
|
|
|
|
printImageStats(views, TextureKind_Generic, "generic");
|
|
printImageStats(views, TextureKind_Color, "color");
|
|
printImageStats(views, TextureKind_Normal, "normal");
|
|
printImageStats(views, TextureKind_Attrib, "attrib");
|
|
}
|
|
|
|
if (report_path)
|
|
{
|
|
if (!printReport(report_path, data, views, meshes, node_offset, mesh_offset, material_offset, animations.size(), json.size(), bin.size()))
|
|
{
|
|
fprintf(stderr, "Warning: cannot save report to %s\n", report_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void writeU32(FILE* out, uint32_t data)
|
|
{
|
|
fwrite(&data, 4, 1, out);
|
|
}
|
|
|
|
static const char* getBaseName(const char* path)
|
|
{
|
|
const char* slash = strrchr(path, '/');
|
|
const char* backslash = strrchr(path, '\\');
|
|
|
|
const char* rs = slash ? slash + 1 : path;
|
|
const char* bs = backslash ? backslash + 1 : path;
|
|
|
|
return std::max(rs, bs);
|
|
}
|
|
|
|
static std::string getBufferSpec(const char* bin_path, size_t bin_size, const char* fallback_path, size_t fallback_size, bool fallback_ref)
|
|
{
|
|
std::string json;
|
|
append(json, "\"buffers\":[");
|
|
append(json, "{");
|
|
if (bin_path)
|
|
{
|
|
append(json, "\"uri\":\"");
|
|
append(json, bin_path);
|
|
append(json, "\"");
|
|
}
|
|
comma(json);
|
|
append(json, "\"byteLength\":");
|
|
append(json, bin_size);
|
|
append(json, "}");
|
|
if (fallback_ref)
|
|
{
|
|
comma(json);
|
|
append(json, "{");
|
|
if (fallback_path)
|
|
{
|
|
append(json, "\"uri\":\"");
|
|
append(json, fallback_path);
|
|
append(json, "\"");
|
|
}
|
|
comma(json);
|
|
append(json, "\"byteLength\":");
|
|
append(json, fallback_size);
|
|
append(json, ",\"extensions\":{");
|
|
append(json, "\"EXT_meshopt_compression\":{");
|
|
append(json, "\"fallback\":true");
|
|
append(json, "}}");
|
|
append(json, "}");
|
|
}
|
|
append(json, "]");
|
|
|
|
return json;
|
|
}
|
|
|
|
int gltfpack(const char* input, const char* output, const char* report, Settings settings)
|
|
{
|
|
cgltf_data* data = 0;
|
|
std::vector<Mesh> meshes;
|
|
std::vector<Animation> animations;
|
|
|
|
std::string iext = getExtension(input);
|
|
std::string oext = output ? getExtension(output) : "";
|
|
|
|
if (iext == ".gltf" || iext == ".glb")
|
|
{
|
|
const char* error = 0;
|
|
data = parseGltf(input, meshes, animations, &error);
|
|
|
|
if (error)
|
|
{
|
|
fprintf(stderr, "Error loading %s: %s\n", input, error);
|
|
return 2;
|
|
}
|
|
}
|
|
else if (iext == ".obj")
|
|
{
|
|
const char* error = 0;
|
|
data = parseObj(input, meshes, &error);
|
|
|
|
if (!data)
|
|
{
|
|
fprintf(stderr, "Error loading %s: %s\n", input, error);
|
|
return 2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
fprintf(stderr, "Error loading %s: unknown extension (expected .gltf or .glb or .obj)\n", input);
|
|
return 2;
|
|
}
|
|
|
|
#ifndef WITH_BASISU
|
|
if (data->images_count && settings.texture_ktx2)
|
|
{
|
|
fprintf(stderr, "Error: gltfpack was built without BasisU support, texture compression is not available\n");
|
|
#ifdef __wasi__
|
|
fprintf(stderr, "Note: node.js builds do not support BasisU due to lack of platform features; download a native build from https://github.com/zeux/meshoptimizer/releases\n");
|
|
#endif
|
|
return 3;
|
|
}
|
|
#endif
|
|
|
|
if (oext == ".glb")
|
|
{
|
|
settings.texture_embed = true;
|
|
}
|
|
|
|
std::string json, bin, fallback;
|
|
size_t fallback_size = 0;
|
|
process(data, input, output, report, meshes, animations, settings, json, bin, fallback, fallback_size);
|
|
|
|
cgltf_free(data);
|
|
|
|
if (!output)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (oext == ".gltf")
|
|
{
|
|
std::string binpath = output;
|
|
binpath.replace(binpath.size() - 5, 5, ".bin");
|
|
|
|
std::string fbpath = output;
|
|
fbpath.replace(fbpath.size() - 5, 5, ".fallback.bin");
|
|
|
|
FILE* outjson = fopen(output, "wb");
|
|
FILE* outbin = fopen(binpath.c_str(), "wb");
|
|
FILE* outfb = settings.fallback ? fopen(fbpath.c_str(), "wb") : NULL;
|
|
if (!outjson || !outbin || (!outfb && settings.fallback))
|
|
{
|
|
fprintf(stderr, "Error saving %s\n", output);
|
|
return 4;
|
|
}
|
|
|
|
std::string bufferspec = getBufferSpec(getBaseName(binpath.c_str()), bin.size(), settings.fallback ? getBaseName(fbpath.c_str()) : NULL, fallback_size, settings.compress);
|
|
|
|
fprintf(outjson, "{");
|
|
fwrite(bufferspec.c_str(), bufferspec.size(), 1, outjson);
|
|
fprintf(outjson, ",");
|
|
fwrite(json.c_str(), json.size(), 1, outjson);
|
|
fprintf(outjson, "}");
|
|
|
|
fwrite(bin.c_str(), bin.size(), 1, outbin);
|
|
|
|
if (settings.fallback)
|
|
fwrite(fallback.c_str(), fallback.size(), 1, outfb);
|
|
|
|
int rc = 0;
|
|
rc |= fclose(outjson);
|
|
rc |= fclose(outbin);
|
|
if (outfb)
|
|
rc |= fclose(outfb);
|
|
|
|
if (rc)
|
|
{
|
|
fprintf(stderr, "Error saving %s\n", output);
|
|
return 4;
|
|
}
|
|
}
|
|
else if (oext == ".glb")
|
|
{
|
|
std::string fbpath = output;
|
|
fbpath.replace(fbpath.size() - 4, 4, ".fallback.bin");
|
|
|
|
FILE* out = fopen(output, "wb");
|
|
FILE* outfb = settings.fallback ? fopen(fbpath.c_str(), "wb") : NULL;
|
|
if (!out || (!outfb && settings.fallback))
|
|
{
|
|
fprintf(stderr, "Error saving %s\n", output);
|
|
return 4;
|
|
}
|
|
|
|
std::string bufferspec = getBufferSpec(NULL, bin.size(), settings.fallback ? getBaseName(fbpath.c_str()) : NULL, fallback_size, settings.compress);
|
|
|
|
json.insert(0, "{" + bufferspec + ",");
|
|
json.push_back('}');
|
|
|
|
while (json.size() % 4)
|
|
json.push_back(' ');
|
|
|
|
while (bin.size() % 4)
|
|
bin.push_back('\0');
|
|
|
|
writeU32(out, 0x46546C67);
|
|
writeU32(out, 2);
|
|
writeU32(out, uint32_t(12 + 8 + json.size() + 8 + bin.size()));
|
|
|
|
writeU32(out, uint32_t(json.size()));
|
|
writeU32(out, 0x4E4F534A);
|
|
fwrite(json.c_str(), json.size(), 1, out);
|
|
|
|
writeU32(out, uint32_t(bin.size()));
|
|
writeU32(out, 0x004E4942);
|
|
fwrite(bin.c_str(), bin.size(), 1, out);
|
|
|
|
if (settings.fallback)
|
|
fwrite(fallback.c_str(), fallback.size(), 1, outfb);
|
|
|
|
int rc = 0;
|
|
rc |= fclose(out);
|
|
if (outfb)
|
|
rc |= fclose(outfb);
|
|
|
|
if (rc)
|
|
{
|
|
fprintf(stderr, "Error saving %s\n", output);
|
|
return 4;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
fprintf(stderr, "Error saving %s: unknown extension (expected .gltf or .glb)\n", output);
|
|
return 4;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
Settings defaults()
|
|
{
|
|
Settings settings = {};
|
|
settings.quantize = true;
|
|
settings.pos_bits = 14;
|
|
settings.tex_bits = 12;
|
|
settings.nrm_bits = 8;
|
|
settings.col_bits = 8;
|
|
settings.trn_bits = 16;
|
|
settings.rot_bits = 12;
|
|
settings.scl_bits = 16;
|
|
settings.anim_freq = 30;
|
|
settings.simplify_threshold = 1.f;
|
|
settings.texture_scale = 1.f;
|
|
for (int kind = 0; kind < TextureKind__Count; ++kind)
|
|
settings.texture_quality[kind] = 8;
|
|
|
|
return settings;
|
|
}
|
|
|
|
template <typename T>
|
|
T clamp(T v, T min, T max)
|
|
{
|
|
return v < min ? min : v > max ? max : v;
|
|
}
|
|
|
|
unsigned int textureMask(const char* arg)
|
|
{
|
|
unsigned int result = 0;
|
|
|
|
while (arg)
|
|
{
|
|
const char* comma = strchr(arg, ',');
|
|
size_t seg = comma ? comma - arg - 1 : strlen(arg);
|
|
|
|
if (strncmp(arg, "color", seg) == 0)
|
|
result |= 1 << TextureKind_Color;
|
|
else if (strncmp(arg, "normal", seg) == 0)
|
|
result |= 1 << TextureKind_Normal;
|
|
else if (strncmp(arg, "attrib", seg) == 0)
|
|
result |= 1 << TextureKind_Attrib;
|
|
else
|
|
fprintf(stderr, "Warning: unrecognized texture class %.*s\n", int(seg), arg);
|
|
|
|
arg = comma ? comma + 1 : NULL;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
#ifndef __wasi__
|
|
setlocale(LC_ALL, "C"); // disable locale specific convention for number parsing/printing
|
|
#endif
|
|
|
|
meshopt_encodeIndexVersion(1);
|
|
|
|
Settings settings = defaults();
|
|
|
|
const char* input = 0;
|
|
const char* output = 0;
|
|
const char* report = 0;
|
|
bool help = false;
|
|
bool test = false;
|
|
|
|
std::vector<const char*> testinputs;
|
|
|
|
for (int i = 1; i < argc; ++i)
|
|
{
|
|
const char* arg = argv[i];
|
|
|
|
if (strcmp(arg, "-vp") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
settings.pos_bits = clamp(atoi(argv[++i]), 1, 16);
|
|
}
|
|
else if (strcmp(arg, "-vt") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
settings.tex_bits = clamp(atoi(argv[++i]), 1, 16);
|
|
}
|
|
else if (strcmp(arg, "-vn") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
settings.nrm_bits = clamp(atoi(argv[++i]), 1, 16);
|
|
}
|
|
else if (strcmp(arg, "-vc") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
settings.col_bits = clamp(atoi(argv[++i]), 1, 16);
|
|
}
|
|
else if (strcmp(arg, "-vpi") == 0)
|
|
{
|
|
settings.pos_float = false;
|
|
settings.pos_normalized = false;
|
|
}
|
|
else if (strcmp(arg, "-vpn") == 0)
|
|
{
|
|
settings.pos_float = false;
|
|
settings.pos_normalized = true;
|
|
}
|
|
else if (strcmp(arg, "-vpf") == 0)
|
|
{
|
|
settings.pos_float = true;
|
|
}
|
|
else if (strcmp(arg, "-at") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
settings.trn_bits = clamp(atoi(argv[++i]), 1, 24);
|
|
}
|
|
else if (strcmp(arg, "-ar") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
settings.rot_bits = clamp(atoi(argv[++i]), 4, 16);
|
|
}
|
|
else if (strcmp(arg, "-as") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
settings.scl_bits = clamp(atoi(argv[++i]), 1, 24);
|
|
}
|
|
else if (strcmp(arg, "-af") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
settings.anim_freq = clamp(atoi(argv[++i]), 1, 100);
|
|
}
|
|
else if (strcmp(arg, "-ac") == 0)
|
|
{
|
|
settings.anim_const = true;
|
|
}
|
|
else if (strcmp(arg, "-kn") == 0)
|
|
{
|
|
settings.keep_nodes = true;
|
|
}
|
|
else if (strcmp(arg, "-km") == 0)
|
|
{
|
|
settings.keep_materials = true;
|
|
}
|
|
else if (strcmp(arg, "-ke") == 0)
|
|
{
|
|
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 = clamp(float(atof(argv[++i])), 0.f, 1.f);
|
|
}
|
|
else if (strcmp(arg, "-sa") == 0)
|
|
{
|
|
settings.simplify_aggressive = true;
|
|
}
|
|
#ifndef NDEBUG
|
|
else if (strcmp(arg, "-sd") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
settings.simplify_debug = clamp(float(atof(argv[++i])), 0.f, 1.f);
|
|
}
|
|
else if (strcmp(arg, "-md") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
settings.meshlet_debug = clamp(atoi(argv[++i]), 3, 255);
|
|
}
|
|
#endif
|
|
else if (strcmp(arg, "-tu") == 0)
|
|
{
|
|
settings.texture_ktx2 = true;
|
|
|
|
unsigned int mask = ~0u;
|
|
if (i + 1 < argc && isalpha(argv[i + 1][0]))
|
|
mask = textureMask(argv[++i]);
|
|
|
|
for (int kind = 0; kind < TextureKind__Count; ++kind)
|
|
if (mask & (1 << kind))
|
|
settings.texture_mode[kind] = TextureMode_UASTC;
|
|
}
|
|
else if (strcmp(arg, "-tc") == 0)
|
|
{
|
|
settings.texture_ktx2 = true;
|
|
|
|
unsigned int mask = ~0u;
|
|
if (i + 1 < argc && isalpha(argv[i + 1][0]))
|
|
mask = textureMask(argv[++i]);
|
|
|
|
for (int kind = 0; kind < TextureKind__Count; ++kind)
|
|
if (mask & (1 << kind))
|
|
settings.texture_mode[kind] = TextureMode_ETC1S;
|
|
}
|
|
else if (strcmp(arg, "-tq") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
int quality = clamp(atoi(argv[++i]), 1, 10);
|
|
for (int kind = 0; kind < TextureKind__Count; ++kind)
|
|
settings.texture_quality[kind] = quality;
|
|
}
|
|
else if (strcmp(arg, "-tq") == 0 && i + 2 < argc && isalpha(argv[i + 1][0]) && isdigit(argv[i + 2][0]))
|
|
{
|
|
unsigned int mask = textureMask(argv[++i]);
|
|
int quality = clamp(atoi(argv[++i]), 1, 10);
|
|
|
|
for (int kind = 0; kind < TextureKind__Count; ++kind)
|
|
if (mask & (1 << kind))
|
|
settings.texture_quality[kind] = quality;
|
|
}
|
|
else if (strcmp(arg, "-ts") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
settings.texture_scale = clamp(float(atof(argv[++i])), 0.f, 1.f);
|
|
}
|
|
else if (strcmp(arg, "-tl") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
settings.texture_limit = atoi(argv[++i]);
|
|
}
|
|
else if (strcmp(arg, "-tp") == 0)
|
|
{
|
|
settings.texture_pow2 = true;
|
|
}
|
|
else if (strcmp(arg, "-tfy") == 0)
|
|
{
|
|
settings.texture_flipy = true;
|
|
}
|
|
else if (strcmp(arg, "-tj") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
|
|
{
|
|
settings.texture_jobs = clamp(atoi(argv[++i]), 0, 128);
|
|
}
|
|
else if (strcmp(arg, "-noq") == 0)
|
|
{
|
|
// TODO: Warn if -noq is used and suggest -vpf instead; use -noqq to silence
|
|
settings.quantize = false;
|
|
}
|
|
else if (strcmp(arg, "-i") == 0 && i + 1 < argc && !input)
|
|
{
|
|
input = argv[++i];
|
|
}
|
|
else if (strcmp(arg, "-o") == 0 && i + 1 < argc && !output)
|
|
{
|
|
output = argv[++i];
|
|
}
|
|
else if (strcmp(arg, "-r") == 0 && i + 1 < argc && !report)
|
|
{
|
|
report = argv[++i];
|
|
}
|
|
else if (strcmp(arg, "-c") == 0)
|
|
{
|
|
settings.compress = true;
|
|
}
|
|
else if (strcmp(arg, "-cc") == 0)
|
|
{
|
|
settings.compress = true;
|
|
settings.compressmore = true;
|
|
}
|
|
else if (strcmp(arg, "-cf") == 0)
|
|
{
|
|
settings.compress = true;
|
|
settings.fallback = true;
|
|
}
|
|
else if (strcmp(arg, "-v") == 0)
|
|
{
|
|
settings.verbose = 1;
|
|
}
|
|
else if (strcmp(arg, "-vv") == 0)
|
|
{
|
|
settings.verbose = 2;
|
|
}
|
|
else if (strcmp(arg, "-h") == 0)
|
|
{
|
|
help = true;
|
|
}
|
|
else if (strcmp(arg, "-test") == 0)
|
|
{
|
|
test = true;
|
|
}
|
|
else if (arg[0] == '-')
|
|
{
|
|
fprintf(stderr, "Unrecognized option %s\n", arg);
|
|
return 1;
|
|
}
|
|
else if (test)
|
|
{
|
|
testinputs.push_back(arg);
|
|
}
|
|
else
|
|
{
|
|
fprintf(stderr, "Expected option, got %s instead\n", arg);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// shortcut for gltfpack -v
|
|
if (settings.verbose && argc == 2)
|
|
{
|
|
printf("gltfpack %s\n", getVersion().c_str());
|
|
return 0;
|
|
}
|
|
|
|
if (test)
|
|
{
|
|
for (size_t i = 0; i < testinputs.size(); ++i)
|
|
{
|
|
const char* path = testinputs[i];
|
|
|
|
printf("%s\n", path);
|
|
gltfpack(path, NULL, NULL, settings);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
if (!input || !output || help)
|
|
{
|
|
fprintf(stderr, "gltfpack %s\n", getVersion().c_str());
|
|
fprintf(stderr, "Usage: gltfpack [options] -i input -o output\n");
|
|
|
|
if (help)
|
|
{
|
|
fprintf(stderr, "\nBasics:\n");
|
|
fprintf(stderr, "\t-i file: input file to process, .obj/.gltf/.glb\n");
|
|
fprintf(stderr, "\t-o file: output file path, .gltf/.glb\n");
|
|
fprintf(stderr, "\t-c: produce compressed gltf/glb files (-cc for higher compression ratio)\n");
|
|
fprintf(stderr, "\nTextures:\n");
|
|
fprintf(stderr, "\t-tc: convert all textures to KTX2 with BasisU supercompression\n");
|
|
fprintf(stderr, "\t-tu: use UASTC when encoding textures (much higher quality and much larger size)\n");
|
|
fprintf(stderr, "\t-tq N: set texture encoding quality (default: 8; N should be between 1 and 10\n");
|
|
fprintf(stderr, "\t-ts R: scale texture dimensions by the ratio R (default: 1; R should be between 0 and 1)\n");
|
|
fprintf(stderr, "\t-tl N: limit texture dimensions to N pixels (default: 0 = no limit)\n");
|
|
fprintf(stderr, "\t-tp: resize textures to nearest power of 2 to conform to WebGL1 restrictions\n");
|
|
fprintf(stderr, "\t-tfy: flip textures along Y axis during BasisU supercompression\n");
|
|
fprintf(stderr, "\t-tj N: use N threads when compressing textures\n");
|
|
fprintf(stderr, "\tTexture classes:\n");
|
|
fprintf(stderr, "\t-tc C: use ETC1S when encoding textures of class C\n");
|
|
fprintf(stderr, "\t-tu C: use UASTC when encoding textures of class C\n");
|
|
fprintf(stderr, "\t-tq C N: set texture encoding quality for class C\n");
|
|
fprintf(stderr, "\t... where C is a comma-separated list (no spaces) with valid values color,normal,attrib\n");
|
|
fprintf(stderr, "\nSimplification:\n");
|
|
fprintf(stderr, "\t-si R: simplify meshes targeting triangle count ratio R (default: 1; R should be between 0 and 1)\n");
|
|
fprintf(stderr, "\t-sa: aggressively simplify to the target ratio disregarding quality\n");
|
|
fprintf(stderr, "\nVertices:\n");
|
|
fprintf(stderr, "\t-vp N: use N-bit quantization for positions (default: 14; N should be between 1 and 16)\n");
|
|
fprintf(stderr, "\t-vt N: use N-bit quantization for texture coordinates (default: 12; N should be between 1 and 16)\n");
|
|
fprintf(stderr, "\t-vn N: use N-bit quantization for normals and tangents (default: 8; N should be between 1 and 16)\n");
|
|
fprintf(stderr, "\t-vc N: use N-bit quantization for colors (default: 8; N should be between 1 and 16)\n");
|
|
fprintf(stderr, "\nVertex positions:\n");
|
|
fprintf(stderr, "\t-vpi: use integer attributes for positions (default)\n");
|
|
fprintf(stderr, "\t-vpn: use normalized attributes for positions\n");
|
|
fprintf(stderr, "\t-vpf: use floating point attributes for positions\n");
|
|
fprintf(stderr, "\nAnimations:\n");
|
|
fprintf(stderr, "\t-at N: use N-bit quantization for translations (default: 16; N should be between 1 and 24)\n");
|
|
fprintf(stderr, "\t-ar N: use N-bit quantization for rotations (default: 12; N should be between 4 and 16)\n");
|
|
fprintf(stderr, "\t-as N: use N-bit quantization for scale (default: 16; N should be between 1 and 24)\n");
|
|
fprintf(stderr, "\t-af N: resample animations at N Hz (default: 30)\n");
|
|
fprintf(stderr, "\t-ac: keep constant animation tracks even if they don't modify the node transform\n");
|
|
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-km: keep named materials and disable named material merging\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");
|
|
fprintf(stderr, "\t-v: verbose output (print version when used without other options)\n");
|
|
fprintf(stderr, "\t-r file: output a JSON report to file\n");
|
|
fprintf(stderr, "\t-h: display this help and exit\n");
|
|
}
|
|
else
|
|
{
|
|
fprintf(stderr, "\nBasics:\n");
|
|
fprintf(stderr, "\t-i file: input file to process, .obj/.gltf/.glb\n");
|
|
fprintf(stderr, "\t-o file: output file path, .gltf/.glb\n");
|
|
fprintf(stderr, "\t-c: produce compressed gltf/glb files (-cc for higher compression ratio)\n");
|
|
fprintf(stderr, "\t-tc: convert all textures to KTX2 with BasisU supercompression\n");
|
|
fprintf(stderr, "\t-si R: simplify meshes targeting triangle count ratio R (default: 1; R should be between 0 and 1)\n");
|
|
fprintf(stderr, "\nRun gltfpack -h to display a full list of options\n");
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
if (settings.texture_limit && !settings.texture_ktx2)
|
|
{
|
|
fprintf(stderr, "Option -tl is only supported when -tc is set as well\n");
|
|
return 1;
|
|
}
|
|
|
|
if (settings.texture_pow2 && (settings.texture_limit & (settings.texture_limit - 1)) != 0)
|
|
{
|
|
fprintf(stderr, "Option -tp requires the limit specified via -tl to be a power of 2\n");
|
|
return 1;
|
|
}
|
|
|
|
if (settings.texture_scale < 1 && !settings.texture_ktx2)
|
|
{
|
|
fprintf(stderr, "Option -ts is only supported when -tc is set as well\n");
|
|
return 1;
|
|
}
|
|
|
|
if (settings.texture_pow2 && !settings.texture_ktx2)
|
|
{
|
|
fprintf(stderr, "Option -tp is only supported when -tc is set as well\n");
|
|
return 1;
|
|
}
|
|
|
|
if (settings.texture_flipy && !settings.texture_ktx2)
|
|
{
|
|
fprintf(stderr, "Option -tfy is only supported when -tc is set as well\n");
|
|
return 1;
|
|
}
|
|
|
|
if (settings.fallback && settings.compressmore)
|
|
{
|
|
fprintf(stderr, "Option -cf can not be used together with -cc\n");
|
|
return 1;
|
|
}
|
|
|
|
if (settings.fallback && settings.pos_float)
|
|
{
|
|
fprintf(stderr, "Option -cf can not be used together with -vpf\n");
|
|
return 1;
|
|
}
|
|
|
|
return gltfpack(input, output, report, settings);
|
|
}
|
|
|
|
#ifdef __wasi__
|
|
extern "C" int pack(int argc, char** argv)
|
|
{
|
|
chdir("/gltfpack-$pwd");
|
|
|
|
int result = main(argc, argv);
|
|
fflush(NULL);
|
|
return result;
|
|
}
|
|
#endif
|