diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 5317f0f88a4a1ca52196d503bb6a993bcd1a7a90..f05cd174b3abde6d5cba2fa64d2a3fb5acf7b7cb 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -15,8 +15,8 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (1, 2, 62), - 'blender': (2, 82, 7), + "version": (1, 2, 63), + 'blender': (2, 83, 9), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', 'warning': '', diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py index f841f0e26c1b9fd571903f300e19aeecf1153f65..22f0bc6de9c4201a3055e314668ffd49f89017a6 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_primitives.py @@ -60,8 +60,6 @@ def gather_primitives( material = gltf2_blender_gather_materials.gather_material(blender_material, double_sided, export_settings) - # NOTE: gather_material may invalidate blender_mesh (see #932), - # so make sure not to access blender_mesh again after this point except IndexError: # no material at that index pass diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_image.py b/io_scene_gltf2/blender/exp/gltf2_blender_image.py index e9db7e6638e73062053df598e4d2e9254f11e8c8..34bb8e879c6a2bea23dda54a03cbddbfa46fdbdf 100644 --- a/io_scene_gltf2/blender/exp/gltf2_blender_image.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_image.py @@ -14,7 +14,7 @@ import bpy import os -from typing import Optional +from typing import Optional, Tuple import numpy as np import tempfile import enum @@ -121,98 +121,8 @@ class ExportImage: return self.__encode_from_image(self.blender_image()) def __encode_unhappy(self) -> bytes: - result = self.__encode_unhappy_with_compositor() - if result is not None: - return result - return self.__encode_unhappy_with_numpy() - - def __encode_unhappy_with_compositor(self) -> bytes: - # Builds a Compositor graph that will build the correct image - # from the description in self.fills. - # - # [ Image ]->[ Sep RGBA ] [ Comb RGBA ] - # [ src_chan]--->[dst_chan ]--->[ Output ] - # - # This is hacky, but is about 4x faster than using - # __encode_unhappy_with_numpy. There are some caveats though: - - # First, we can't handle pre-multiplied alpha. - if Channel.A in self.fills: - return None - - # Second, in order to get the same results as using image.pixels - # (which ignores the colorspace), we need to use the 'Non-Color' - # colorspace for all images and set the output device to 'None'. But - # setting the colorspace on dirty images discards their changes. - # So we can't handle dirty images that aren't already 'Non-Color'. - for fill in self.fills: - if isinstance(fill, FillImage): - if fill.image.is_dirty: - if fill.image.colorspace_settings.name != 'Non-Color': - return None - - tmp_scene = None - orig_colorspaces = {} # remembers original colorspaces - try: - tmp_scene = bpy.data.scenes.new('##gltf-export:tmp-scene##') - tmp_scene.use_nodes = True - node_tree = tmp_scene.node_tree - for node in node_tree.nodes: - node_tree.nodes.remove(node) - - out = node_tree.nodes.new('CompositorNodeComposite') - comb_rgba = node_tree.nodes.new('CompositorNodeCombRGBA') - for i in range(4): - comb_rgba.inputs[i].default_value = 1.0 - node_tree.links.new(out.inputs['Image'], comb_rgba.outputs['Image']) - - img_size = None - for dst_chan, fill in self.fills.items(): - if not isinstance(fill, FillImage): - continue - - img = node_tree.nodes.new('CompositorNodeImage') - img.image = fill.image - sep_rgba = node_tree.nodes.new('CompositorNodeSepRGBA') - node_tree.links.new(sep_rgba.inputs['Image'], img.outputs['Image']) - node_tree.links.new(comb_rgba.inputs[dst_chan], sep_rgba.outputs[fill.src_chan]) - - if fill.image.colorspace_settings.name != 'Non-Color': - if fill.image.name not in orig_colorspaces: - orig_colorspaces[fill.image.name] = \ - fill.image.colorspace_settings.name - fill.image.colorspace_settings.name = 'Non-Color' - - if img_size is None: - img_size = fill.image.size[:2] - else: - # All images should be the same size (should be - # guaranteed by gather_texture_info) - assert img_size == fill.image.size[:2] - - width, height = img_size or (1, 1) - return _render_temp_scene( - tmp_scene=tmp_scene, - width=width, - height=height, - file_format=self.file_format, - color_mode='RGB', - colorspace='None', - ) - - finally: - for img_name, colorspace in orig_colorspaces.items(): - bpy.data.images[img_name].colorspace_settings.name = colorspace - - if tmp_scene is not None: - bpy.data.scenes.remove(tmp_scene, do_unlink=True) - - - def __encode_unhappy_with_numpy(self): - # Read the pixels of each image with image.pixels, put them into a - # numpy, and assemble the desired image that way. This is the slowest - # method, and the conversion to Python data eats a lot of memory, so - # it's only used as a last resort. + # We need to assemble the image out of channels. + # Do it with numpy and image.pixels. result = None img_fills = { @@ -227,42 +137,40 @@ class ExportImage: image = bpy.data.images[image_name] if result is None: - result = np.ones((image.size[0], image.size[1], 4), np.float32) + dim = (image.size[0], image.size[1]) + result = np.ones(dim[0] * dim[1] * 4, np.float32) + tmp_buf = np.empty(dim[0] * dim[1] * 4, np.float32) # Images should all be the same size (should be guaranteed by # gather_texture_info). - assert (image.size[0], image.size[1]) == result.shape[:2] + assert (image.size[0], image.size[1]) == dim - # Slow and eats all your memory. - pixels = np.array(image.pixels[:]) - - pixels = pixels.reshape((image.size[0], image.size[1], image.channels)) + image.pixels.foreach_get(tmp_buf) for dst_chan, img_fill in img_fills.items(): if img_fill.image == image: - result[:, :, dst_chan] = pixels[:, :, img_fill.src_chan] + result[int(dst_chan)::4] = tmp_buf[int(img_fill.src_chan)::4] - pixels = None # GC this please + tmp_buf = None # GC this if result is None: # No ImageFills; use a 1x1 white pixel + dim = (1, 1) result = np.array([1.0, 1.0, 1.0, 1.0]) - result = result.reshape((1, 1, 4)) - return self.__encode_from_numpy_array(result) + return self.__encode_from_numpy_array(result, dim) - def __encode_from_numpy_array(self, array: np.ndarray) -> bytes: + def __encode_from_numpy_array(self, pixels: np.ndarray, dim: Tuple[int, int]) -> bytes: tmp_image = None try: tmp_image = bpy.data.images.new( "##gltf-export:tmp-image##", - width=array.shape[0], - height=array.shape[1], + width=dim[0], + height=dim[1], alpha=Channel.A in self.fills, ) assert tmp_image.channels == 4 # 4 regardless of the alpha argument above. - # Also slow and eats all your memory. - tmp_image.pixels = array.flatten().tolist() + tmp_image.pixels.foreach_set(pixels) return _encode_temp_image(tmp_image, self.file_format) @@ -296,8 +204,13 @@ class ExportImage: try: tmp_image = image.copy() tmp_image.update() + if image.is_dirty: - tmp_image.pixels = image.pixels[:] + # Copy the pixels to get the changes + tmp_buf = np.empty(image.size[0] * image.size[1] * 4, np.float32) + image.pixels.foreach_get(tmp_buf) + tmp_image.pixels.foreach_set(tmp_buf) + tmp_buf = None # GC this return _encode_temp_image(tmp_image, self.file_format) finally: @@ -316,36 +229,3 @@ def _encode_temp_image(tmp_image: bpy.types.Image, file_format: str) -> bytes: with open(tmpfilename, "rb") as f: return f.read() - - -def _render_temp_scene( - tmp_scene: bpy.types.Scene, - width: int, - height: int, - file_format: str, - color_mode: str, - colorspace: str, -) -> bytes: - """Set render settings, render to a file, and read back.""" - tmp_scene.render.resolution_x = width - tmp_scene.render.resolution_y = height - tmp_scene.render.resolution_percentage = 100 - tmp_scene.display_settings.display_device = colorspace - tmp_scene.render.image_settings.color_mode = color_mode - tmp_scene.render.dither_intensity = 0.0 - - # Turn off all metadata (stuff like use_stamp_date, etc.) - for attr in dir(tmp_scene.render): - if attr.startswith('use_stamp_'): - setattr(tmp_scene.render, attr, False) - - with tempfile.TemporaryDirectory() as tmpdirname: - tmpfilename = tmpdirname + "/img" - tmp_scene.render.filepath = tmpfilename - tmp_scene.render.use_file_extension = False - tmp_scene.render.image_settings.file_format = file_format - - bpy.ops.render.render(scene=tmp_scene.name, write_still=True) - - with open(tmpfilename, "rb") as f: - return f.read()