diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 7318487e49dac4701e8146c152a34b48049f57f6..8c4d8db4039fee189fb68973a4a3667f597efb86 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -14,7 +14,7 @@ bl_info = { 'name': 'glTF 2.0 format', - 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin SchmithĂĽsen', + 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin SchmithĂĽsen, Jim Eckerlein', "version": (0, 0, 1), 'blender': (2, 80, 0), 'location': 'File > Import-Export', @@ -26,6 +26,7 @@ bl_info = { 'category': 'Import-Export', } + # # Script reloading (if the user calls 'Reload Scripts' from Blender) # @@ -33,6 +34,7 @@ bl_info = { def reload_package(module_dict_main): import importlib from pathlib import Path + def reload_package_recursive(current_dir, module_dict): for path in current_dir.iterdir(): if "__init__" in str(path) or path.stem not in module_dict: @@ -56,6 +58,7 @@ from bpy.props import (StringProperty, IntProperty) from bpy.types import Operator from bpy_extras.io_utils import ImportHelper, ExportHelper +from io_scene_gltf2.io.exp import gltf2_io_draco_compression_extension # @@ -64,9 +67,11 @@ from bpy_extras.io_utils import ImportHelper, ExportHelper class ExportGLTF2_Base: - # TODO: refactor to avoid boilerplate + def __init__(self): + self.is_draco_available = gltf2_io_draco_compression_extension.dll_exists() + bl_options = {'UNDO', 'PRESET'} export_format: EnumProperty( @@ -114,6 +119,44 @@ class ExportGLTF2_Base: default=True ) + export_draco_mesh_compression_enable: BoolProperty( + name='Draco mesh compression', + description='Compress mesh using Draco', + default=False + ) + + export_draco_mesh_compression_level: IntProperty( + name='Compression level', + description='Compression level (0 = most speed, 6 = most compression, higher values currently not supported)', + default=6, + min=0, + max=6 + ) + + export_draco_position_quantization: IntProperty( + name='Position quantization bits', + description='Quantization bits for position values (0 = no quantization)', + default=14, + min=0, + max=30 + ) + + export_draco_normal_quantization: IntProperty( + name='Normal quantization bits', + description='Quantization bits for normal values (0 = no quantization)', + default=10, + min=0, + max=30 + ) + + export_draco_texcoord_quantization: IntProperty( + name='Texcoord quantization bits', + description='Quantization bits for texture coordinate values (0 = no quantization)', + default=12, + min=0, + max=30 + ) + export_tangents: BoolProperty( name='Tangents', description='Export vertex tangents with meshes', @@ -309,11 +352,21 @@ class ExportGLTF2_Base: export_settings['gltf_texcoords'] = self.export_texcoords export_settings['gltf_normals'] = self.export_normals export_settings['gltf_tangents'] = self.export_tangents and self.export_normals + + if self.is_draco_available: + export_settings['gltf_draco_mesh_compression'] = self.export_draco_mesh_compression_enable + export_settings['gltf_draco_mesh_compression_level'] = self.export_draco_mesh_compression_level + export_settings['gltf_draco_position_quantization'] = self.export_draco_position_quantization + export_settings['gltf_draco_normal_quantization'] = self.export_draco_normal_quantization + export_settings['gltf_draco_texcoord_quantization'] = self.export_draco_texcoord_quantization + else: + export_settings['gltf_draco_mesh_compression'] = False + export_settings['gltf_materials'] = self.export_materials export_settings['gltf_colors'] = self.export_colors export_settings['gltf_cameras'] = self.export_cameras export_settings['gltf_selected'] = self.export_selected - export_settings['gltf_layers'] = True #self.export_layers + export_settings['gltf_layers'] = True # self.export_layers export_settings['gltf_extras'] = self.export_extras export_settings['gltf_yup'] = self.export_yup export_settings['gltf_apply'] = self.export_apply @@ -385,6 +438,17 @@ class ExportGLTF2_Base: col.prop(self, 'export_colors') col.prop(self, 'export_materials') + # Add Draco compression option only if the DLL could be found. + if self.is_draco_available: + col.prop(self, 'export_draco_mesh_compression_enable') + + # Display options when Draco compression is enabled. + if self.export_draco_mesh_compression_enable: + col.prop(self, 'export_draco_mesh_compression_level') + col.prop(self, 'export_draco_position_quantization') + col.prop(self, 'export_draco_normal_quantization') + col.prop(self, 'export_draco_texcoord_quantization') + def draw_object_settings(self): col = self.layout.box().column() col.prop(self, 'export_cameras') diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_export.py b/io_scene_gltf2/blender/exp/gltf2_blender_export.py index 071b488466fbc5c31ae53cd1101a4303b6825f08..ad08a85101d633224e7d61e14382fe0839b3028f 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_export.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_export.py @@ -23,6 +23,7 @@ from io_scene_gltf2.blender.exp import gltf2_blender_gather from io_scene_gltf2.blender.exp.gltf2_blender_gltf2_exporter import GlTF2Exporter from io_scene_gltf2.io.com.gltf2_io_debug import print_console, print_newline from io_scene_gltf2.io.exp import gltf2_io_export +from io_scene_gltf2.io.exp import gltf2_io_draco_compression_extension def save(context, export_settings): @@ -66,6 +67,11 @@ def __get_copyright(export_settings): def __gather_gltf(exporter, export_settings): scenes, animations = gltf2_blender_gather.gather_gltf2(export_settings) + + if export_settings['gltf_draco_mesh_compression']: + gltf2_io_draco_compression_extension.compress_scene_primitives(scenes, export_settings) + exporter.add_draco_extension() + for scene in scenes: exporter.add_scene(scene) for animation in animations: diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py index ea26eb1cbf398f53bb1f5b745dfcd4798cd148a9..7d4d3a80d44ed4c0800d57b96f855e076004786b 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gltf2_exporter.py @@ -135,6 +135,15 @@ class GlTF2Exporter: if is_glb: return self.__buffer.to_bytes() + def add_draco_extension(self): + """ + Register Draco extension as *used* and *required*. + + :return: + """ + self.__gltf.extensions_required.append('KHR_draco_mesh_compression') + self.__gltf.extensions_used.append('KHR_draco_mesh_compression') + def finalize_images(self, output_path): """ Write all images. diff --git a/io_scene_gltf2/io/exp/gltf2_io_draco_compression_extension.py b/io_scene_gltf2/io/exp/gltf2_io_draco_compression_extension.py new file mode 100644 index 0000000000000000000000000000000000000000..845e2d2d5b8ba029af8d5465b003238394753e37 --- /dev/null +++ b/io_scene_gltf2/io/exp/gltf2_io_draco_compression_extension.py @@ -0,0 +1,212 @@ +import bpy +import sys +from ctypes import * +from pathlib import Path + +from io_scene_gltf2.io.exp.gltf2_io_binary_data import BinaryData + + +def dll_path() -> Path: + """ + Get the DLL path depending on the underlying platform. + :return: DLL path. + """ + lib_name = 'extern_draco' + blender_root = Path(bpy.app.binary_path).parent + python_lib = Path('2.80/python/lib') + paths = { + 'win32': blender_root/python_lib/'site-packages'/'{}.dll'.format(lib_name), + 'linux': blender_root/python_lib/'python3.7'/'site-packages'/'lib{}.so'.format(lib_name), + 'darwin': blender_root.parent/'Resources'/python_lib/'python3.7'/'site-packages'/'lib{}.dylib'.format(lib_name) + } + + path = paths.get(sys.platform) + return path if path is not None else '' + + +def dll_exists() -> bool: + """ + Checks whether the DLL path exists. + :return: True if the DLL exists. + """ + exists = dll_path().exists() + print("'{}' ".format(dll_path().absolute()) + ("exists, draco mesh compression is available" if exists else + "does not exist, draco mesh compression not available")) + return exists + + +def compress_scene_primitives(scenes, export_settings): + """ + Handles draco compression. + Invoked after data has been gathered, but before scenes get traversed. + Moves position, normal and texture coordinate attributes into a Draco compressed buffer. + """ + + # Load DLL and setup function signatures. + # Nearly all functions take the compressor as the first argument. + dll = cdll.LoadLibrary(str(dll_path().resolve())) + + dll.createCompressor.restype = c_void_p + dll.createCompressor.argtypes = [] + + dll.setCompressionLevel.restype = None + dll.setCompressionLevel.argtypes = [c_void_p, c_uint32] + + dll.setPositionQuantizationBits.restype = None + dll.setPositionQuantizationBits.argtypes = [c_void_p, c_uint32] + + dll.setNormalQuantizationBits.restype = None + dll.setNormalQuantizationBits.argtypes = [c_void_p, c_uint32] + + dll.setTexCoordQuantizationBits.restype = None + dll.setTexCoordQuantizationBits.argtypes = [c_void_p, c_uint32] + + dll.compress.restype = c_bool + dll.compress.argtypes = [c_void_p] + + dll.compressedSize.restype = c_uint64 + dll.compressedSize.argtypes = [c_void_p] + + dll.disposeCompressor.restype = None + dll.disposeCompressor.argtypes = [c_void_p] + + dll.setFaces.restype = None + dll.setFaces.argtypes = [c_void_p, c_uint32, c_uint32, c_void_p] + + dll.addPositionAttribute.restype = None + dll.addPositionAttribute.argtypes = [c_void_p, c_uint32, c_char_p] + + dll.addNormalAttribute.restype = None + dll.addNormalAttribute.argtypes = [c_void_p, c_uint32, c_char_p] + + dll.addTexCoordAttribute.restype = None + dll.addTexCoordAttribute.argtypes = [c_void_p, c_uint32, c_char_p] + + dll.copyToBytes.restype = None + dll.copyToBytes.argtypes = [c_void_p, c_char_p] + + dll.getTexCoordAttributeIdCount.restype = c_uint32 + dll.getTexCoordAttributeIdCount.argtypes = [c_void_p] + + dll.getTexCoordAttributeId.restype = c_uint32 + dll.getTexCoordAttributeId.argtypes = [c_void_p, c_uint32] + + dll.getPositionAttributeId.restype = c_uint32 + dll.getPositionAttributeId.argtypes = [c_void_p] + + dll.getNormalAttributeId.restype = c_uint32 + dll.getNormalAttributeId.argtypes = [c_void_p] + + dll.setCompressionLevel.restype = None + dll.setCompressionLevel.argtypes = [c_void_p, c_uint32] + + dll.setPositionQuantizationBits.restype = None + dll.setPositionQuantizationBits.argtypes = [c_void_p, c_uint32] + + dll.setNormalQuantizationBits.restype = None + dll.setNormalQuantizationBits.argtypes = [c_void_p, c_uint32] + + dll.setTexCoordQuantizationBits.restype = None + dll.setTexCoordQuantizationBits.argtypes = [c_void_p, c_uint32] + + for scene in scenes: + for node in scene.nodes: + __traverse_node(node, dll, export_settings) + + +def __traverse_node(node, dll, export_settings): + if not (node.mesh is None): + print("Compressing mesh " + node.name) + for primitive in node.mesh.primitives: + __compress_primitive(primitive, dll, export_settings) + + if not (node.children is None): + for child in node.children: + __traverse_node(child, dll, export_settings) + + +def __compress_primitive(primitive, dll, export_settings): + attributes = primitive.attributes + + # Begin mesh. + compressor = dll.createCompressor() + + # Process position attributes. + dll.addPositionAttribute(compressor, attributes['POSITION'].count, attributes['POSITION'].buffer_view.data) + + # Process normal attributes. + dll.addNormalAttribute(compressor, attributes['NORMAL'].count, attributes['NORMAL'].buffer_view.data) + + # Process texture coordinate attributes. + for attribute in [attributes[attr] for attr in attributes if attr.startswith('TEXCOORD_')]: + dll.addTexCoordAttribute(compressor, attribute.count, attribute.buffer_view.data) + + # Process faces. + index_byte_length = { + 'Byte': 1, + 'UnsignedByte': 1, + 'Short': 2, + 'UnsignedShort': 2, + 'UnsignedInt': 4, + } + indices = primitive.indices + dll.setFaces(compressor, indices.count, index_byte_length[indices.component_type.name], indices.buffer_view.data) + indices.buffer_view = None + + # Set compression parameters. + dll.setCompressionLevel(compressor, export_settings['gltf_draco_mesh_compression_level']) + dll.setPositionQuantizationBits(compressor, export_settings['gltf_draco_position_quantization']) + dll.setNormalQuantizationBits(compressor, export_settings['gltf_draco_normal_quantization']) + dll.setTexCoordQuantizationBits(compressor, export_settings['gltf_draco_texcoord_quantization']) + + # After all point and connectivity data has been written to the compressor, + # it can finally be compressed. + if dll.compress(compressor): + + # Compression was successfull. + # Move compressed data into a bytes object, + # which is referenced by a 'gltf2_io_binary_data.BinaryData': + # + # "KHR_draco_mesh_compression": { + # .... + # "buffer_view": Compressed data inside a 'gltf2_io_binary_data.BinaryData'. + # } + + # Query size necessary to hold all the compressed data. + compression_size = dll.compressedSize(compressor) + + # Allocate byte buffer and write compressed data to it. + compressed_data = bytes(compression_size) + dll.copyToBytes(compressor, compressed_data) + + if primitive.extensions is None: + primitive.extensions = {} + + tex_coord_ids = {} + for id in range(0, dll.getTexCoordAttributeIdCount(compressor)): + tex_coord_ids["TEXCOORD_" + str(id)] = dll.getTexCoordAttributeId(compressor, id) + + # Register draco compression extension into primitive. + primitive.extensions["KHR_draco_mesh_compression"] = { + 'bufferView': BinaryData(compressed_data), + 'attributes': { + 'POSITION': dll.getPositionAttributeId(compressor), + 'NORMAL': dll.getNormalAttributeId(compressor), + **tex_coord_ids, + } + } + + # Set to triangle list mode. + primitive.mode = 4 + + # Remove buffers from attribute, since the data now resides inside the compressed Draco buffer. + attributes['POSITION'].buffer_view = None + attributes['NORMAL'].buffer_view = None + for attribute in [attributes[attr] for attr in attributes if attr.startswith('TEXCOORD_')]: + attribute.buffer_view = None + + # Afterwards, the compressor can be released. + dll.disposeCompressor(compressor) + + pass +