glTF exporter: Draco compression

Note that blender must use this patch to use Draco compression: https://developer.blender.org/D4501
Currenlty, this patch is not merged yet into master
This commit is contained in:
Julien Duroure 2019-04-02 22:13:43 +02:00
parent 8521155924
commit 0a26f6cce9
4 changed files with 294 additions and 3 deletions

View File

@ -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')

View File

@ -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:

View File

@ -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.

View File

@ -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